Cap. 13 - Archivos

13.1) Qué son los archivos

Mientras se ejecuta un programa, sus datos se guardan en la RAM (random access memory), la cual es rápida y poco costosa, pero también volátil, lo que significa que cuando el programa termina, o la computadora se apaga, los datos desaparecen de la RAM. Si se quiere que los datos estén disponibles la próxima vez que se prenda la computadora o se ejecute el programa, deben ser escritos a un medio de almacenamiento no volátil, como un disco duro, un pendrive USB, etc.

Los datos que se envían a medios de almacenamiento no volátiles se guardan en ubicaciones con nombre propio que llamamos archivos. Al leer o escribir archivos, los programas pueden acceder a información escrita previamente por otro programa, o guardar información para ser utilizada por futuros programas.

Trabajar con archivos se parece a trabajar con un cuaderno de apuntes. Para usarlo, hay que empezar por abrirlo. Una vez se usó, hay que volver a cerrarlo. Cuando el cuaderno está abierto, se puede leer de él, o escribir en él. En ambos casos, el dueño del cuaderno sabe dónde están los datos. Puede leerlos en orden o ir a la parte que más le interese.

Todo lo anterior se aplica a los archivos de la misma manera. Para abrir un archivo, especificamos su nombre e indicamos si queremos leer o escribir.

13.2) Escribiendo nuestro primer archivo

Comencemos con un programa sencillo que escribe tres líneas de texto en un archivo:

mi_archivo = open("test.txt", "w")
mi_archivo.write("Mi primer archivo escrito con Python\n")
mi_archivo.write("---------------------------------\n")
mi_archivo.write("Hola, mundo!!\n")
mi_archivo.close()

Al abrir un archivo se crea lo que llamamos un manejador (handle) del archivo. En este ejemplo, la variable mi_archivo se refiere a dicho manejador. Nuestro programa llama a métodos del manejador, y esto provoca cambios en el archivo en sí, el cual habitualmente estará ubicado en nuestro disco duro.

Un manejador es en cierto sentido como un control remoto.

Todos conocemos bien los controles remotos de TVs y otros aparatos eléctricos. Operamos sobre el control remoto - cambiando canales, cambiando el volumen, etc. - pero las acciones reales ocurren en el televisor. Por analogía podríamos llamar a nuestro control remoto el manejador (handle) de nuestra TV.

A veces queremos enfatizar la diferencia - el manejador del archivo no es lo mismo que el archivo, y el control remoto no es lo mismo que la TV. Pero en otros casos preferimos tratarlos como si fueran parte de una misma cosa o abstracción mental, y simplemente decimos "cerrar el archivo" o "cambiar el canal de la TV" - en vez de decir "decirle al manejador que cierre el archivo" o "hacer que el control remoto cambie el canal de la TV".

13.3) Leyendo una línea por vez

Ahora que el archivo existe en nuestro disco, podemos abrirlo, esta vez para lectura, y leer todas las líneas del archivo, una por vez. Esta vez, el argumento mode es "r" (reading, lectura).

mi_manejador = open("test.txt", "r")
while True:                            # Leer para siempre
    linea = mi_manejador.readline()    # Tratar de leer la próxima línea
    if len(linea) == 0:                # Si no hay más líneas
        break                          #     Abandonar el Loop

    # Ahora procesamos la línea que acaabamos de leer (línea actual)
    print(linea, end="")

mi_manejador.close()

Este es un patrón que será útil para nuestra caja de herramientas. En programas más grandes, el procesamiento de cada línea será más complejo (en vez de un simple print) - por ejemplo, si cada línea del archivo contiene el nombre y el email de uno de nuestros amigos, tal vez dividiremos la línea en partes para llamar con esas piezas a una función que envíe a ese amigo una invitación para una fiesta.

En la línea 8 eliminamos el carácter nueva línea que habitualmente print agrega a nuestros strings. Por qué? Porque el string ya tiene su propio carácter nueva línea: el método readline de la línea 3 retorna todo lo que encuentra en la línea, incluyendo el carácter de nueva línea. Esto también explica nuestra lógica para la detección de fin de archivo: cuando ya no quedan líneas para leer del archivo, readline devuelve un string vacío - uno que ni siquiera tiene un carácter de nueva línea al final, y por lo tanto su largo es 0.

Primero fallar...

En nuestro ejemplo, tenemos tres líneas en el archivo, pero entramos cuatro veces al loop. En Python, sólo se puede saber que el archivo no tiene más líneas a través de un fallo al leer la siguiente línea. En algunos otros lenguajes (como por ejemplo Pascal) las cosas son distintas: allí se leerían 3 líneas, y se tiene lo que se llama mirar para adelante (look ahead) - al terminar de leer la tercera línea ya sabríamos que no hay más líneas en el archivo. Ni siquiera está permitido tratar de leer la siguiente línea del archivo.

Así que las plantillas (templates) para trabajar línea por línea en Pascal y Python son diferentes!

Cuando transfieras tus habilidades adquiridas en Python a otros lenguajes de programación, asegúrate de verificar cómo se manejan los archivos en esos lenguajes: son del estilo "inténtalo, y luego de fallar sabrás" o bien el estilo "mirar adelante"?

Si intentamos abrir un archivo que no existe, recibimos un mensaje de error:

>>> mi_manejador = open("blablabla.txt", "r")
IOError: [Errno 2] No such file or directory: "blablabla.txt"

13.4) Convirtiendo un archivo en una lista de líneas

Con frecuencia es útil obtener datos de un archivo en disco y convertirlos en una lista de líneas. Supongamos que tenemos un archivo que contiene los nombres de nuestros amigos con sus direcciones de email, una por línea en el archivo. Pero querríamos que las líneas estuvieran ordenadas alfabéticamente. Un buen plan sería este: leer todo en una lista de líneas, luego ordenar la lista, y luego escribir la lista ordenada en otro archivo.

f = open("amigos.txt", "r")
xs = f.readlines()
f.close()

xs.sort()

g = open("amigos_ordenados.txt", "w")
for v in xs:
    g.write(v)
g.close()

El método readlines de la línea 2 lee todas las líneas y devuelve una lista de strings.

Podríamos haber usado la plantilla de la sección previa para leer cada línea una por una, e ir construyendo la lista nosotros mismos, pero es mucho más fácil usar un método que la propia implementación de Python ya nos da.

13.5) Leyendo el archivo completo de una

Otra forma de trabajar con archivos de texto es leer el contenido completo del archivo en un string, y luego usar nuestras habilidades para procesar strings para trabajar con su contenido.

Usaríamos este método en casos en que no nos interesara la estructura en líneas del archivo. Por ejemplo, ya vimos el método split de strings que permite dividir a un string en palabras. Utilizándolo, podemos calcular así la cantidad de palabras que hay en un archivo:

    f = open("test.txt")
    contenido = f.read()
    f.close()

    palabras = contenido.split()
    print(palabras)
    print("Hay {0} palabras en el archivo.".format(len(palabras)))

Observar aquí que no pasamos el modo "r" en la línea 1. Por defecto, si no se pasa el modo como parámetro, Python abre el archivo para lectura.

Los caminos (paths) a tus archivos deben ser nombrados explícitamente (en caso de ser necesarios).

En el ejemplo anterior, hemos supuesto que el archivo "test.txt" está en el mismo directorio que el código fuente de Python que estamos ejecutando. Si ese no es el caso, deberás proveer un camino completo o un camino relativo al archivo.

  • En Windows, un camino completo se ve así: "C:\\temp\\test.txt".
  • Mientras que en un sistema Unix el camino completo sería: "/home/alicia/somefile.txt".

Volveremos a hablar del tema más adelante en este capítulo.

13.6) Trabajando con archivos binarios

Los archivos que contienen fotografías, videos, contenido comprimido (zip, rar), ejecutables, etc. se llaman archivos binarios: no están organizados por líneas, y no pueden abrirse con un editor de texto normal. En Python es sencillo trabajar con estos archivos, pero cuando leemos de los mismos obtendremos bytes en vez de un string. En este ejemplo copiaremos un archivo binario en otro:

f = open("test_imagen.png", "rb")
g = open("copia_imagen.png", "wb")

while True:
    buf = f.read(1024)
    if len(buf) == 0:
         break
    g.write(buf)

f.close()
g.close()

Vemos algunas cosas nuevas aquí. En las líneas 1 y 2 agregamos una "b" al modo para indicarle a Python que los archivos son binarios en vez de textuales. En la línea 5 vemos que read puede recibir un argumento que indica cuántos bytes debe intentar leer del archivo. Aquí elegimos leer y escribir de a 1024 bytes en cada iteración del loop. Cuando recibimos un buffer vacío de nuestro intento de leer, sabemos que podemos salir del loop (con un break) y cerrar los dos archivos.

Si ponemos un breakpoint en la línea 6 (o si imprimimos type(buf) en esa línea) veremos que el tipo de buf es bytes. No es un tipo que vayamos a utilizar mucho en este curso, pero es importante saber que existe.

13.7) Un ejemplo

Hay un tipo especial de programas de procesamiento línea por línea que son muy útiles y que se limitan a leer el archivo (línea por línea) y realizar cierto mínimo procesamiento a medida que van escribiendo las líneas a un archivo de salida. Pueden por ejemplo numerar las líneas en el archivo de salida, o insertar líneas vacías cada 60 líneas para hacer que el archivo se imprima correctamente en hojas de papel, o extraer algunas columnas específicas de cada línea o sólo imprimir las líneas que contengan un cierto substring. A esta clase de programas se los llama filtros.

Aquí hay un filtro que copia un archivo a otro, omitiendo todas las líneas que comiencen por #:

def filtrar(viejo_archivo, nuevo_archivo):
     manejador_in = open(viejo_archivo, "r")
     manejador_out = open(nuevo_archivo, "w")
     while True:
         texto = manejador_in.readline()
         if len(texto) == 0:
            break
         if texto[0] == "#":
            continue

         # Enviamos aquí las líneas deseadas al archivo de salida
         manejador_out.write(texto)

     manejador_in.close()
     manejador_out.close()

La sentencia continue en la línea 9 salta el resto de las líneas del loop, pero el loop continúa iterando. Este estilo puede parecer un poco forzado, pero es de uso habitual cuando se quiere decir "detecta temprano las líneas que no nos interesan para saltearlas, y con las que nos interesan completa el procedimiento de cada paso de la iteración".

Así, si texto es el string vacío, el loop se termina. Si el primer carácter de texto es un numeral, el flujo de ejecución vuelve al inicio del loop, pronto para procesar la línea siguiente. Sólo si fallan las dos condiciones es que haremos el procesamiento principal del loop (el de la línea 11), en este ejemplo, escribir el texto al archivo de salida.

Consideremos otro caso: supongamos que nuestro archivo de entrada contuviese sólo líneas vacías. El chequeo de línea vacía de la línea 6 no interrumpiría el loop, porque vimos que readline siempre incluye el carácter de fin de línea en el string que devuelve. Es sólo cuando intentamos leer más allá del final del archivo que obtenemos un string realmente vacío cuyo largo es cero, y se interrumpe el loop.

13.8) Directorios

Los archivos en medios de almacenamiento no volátil se organizan según una serie de reglas conocidas como sistema de archivos. Los sistemas de archivos están compuestos de archivos y directorios, que son contenedores de archivos y de otros directorios.

Cuando creamos un nuevo archivo (abriéndolo y escribiendo en él), éste se guarda en el directorio actual (que es el lugar en el que estamos ubicados cuando ejecutamos el programa). Del mismo modo, cuando abrimos un archivo para leer, Python lo busca en el directorio actual.

Si queremos abrir un archivo que esté en otra ubicación, tenemos que especificar el camino (path) al archivo, que es el nombre del directorio (o carpeta) en que está ubicado el archivo:

>>> archivo_palabras = open("/usr/share/dict/words", "r")
>>> lista_palabras = archivo_palabras.readlines()
>>> print(lista_palabras[:6])
['\n', 'A\n', "A's\n", 'AOL\n', "AOL's\n", 'Aachen\n']

Este ejemplo (en Unix) abre un archivo llamado words que está en un directorio llamado dict que está en el directorio share que está en usr, que a su vez está en el directorio raíz del sistema, llamado /. Luego lee cada línea de ese archivo en una lista de strings usando el método readlines, e imprime los primeros 6 elementos de esa lista.

Un camino en Windows tendrá la forma "c:/temp/words.txt" o "c:\\temp\\words.txt". Dado que las barras invertidas se usan para escapar caracteres especiales como nuevas líneas o tabuladores, necesitamos escribir dos barras invertidas en un string literal para obtener una. Por lo tanto, el largo de los dos strings anteriores es el mismo!

El archivo /usr/share/dict/words debería existir en sistemas Unix (o basados en Unix) y contiene una lista de palabras en orden alfabético.

13.9) Cómo se puede obtener un archivo en la web?

Algunas librerías Python son más confusas de lo que sería deseable. Pero aquí hay un ejemplo muy simple que copia el contenido de una URL web a un archivo local:

import urllib.request

url = "https://gatoconbota.com/favicon.png"
archivo_destino = "favicon_local.png"

urllib.request.urlretrieve(url, archivo_destino)

La función urlretrieve (que sólo se llama una vez) permite bajar cualquier tipo de contenido de la web.

Vamos a tener que cumplir con algunos requerimientos antes de que esto funcione:

Aquí hay un ejemplo ligeramente distinto. En vez de guardar el recurso web a nuestro disco local, lo guardamos en un string, y lo devolvemos:

def bajar_de_web_a_string(url):
    """ Obtiene el contenido de una página web.
        El contenido es convertido en un string antes de ser retornado.
    """
    mi_socket = urllib.request.urlopen(url)
    datos = str(mi_socket.read())
    mi_socket.close()
    return datos

el_texto = bajar_de_web_a_string("https://www.gatoconbota.com/python")
print(el_texto)

Al abrir el url remoto obtenemos lo que se llama un socket (que podríamos traducir por "enchufe", pero es mejor decir "socket" en inglés). Este es un manejador, hasta el final de nuestra conexión entre nuestro programa y el servidor web remoto. Podemos llamar a métodos para leer, escribir y cerrar el objeto socket casi en la misma forma en que trabajaríamos con un manejador de archivos.

13.10) Glosario

13.11) Ejercicios

1) Escribe un programa que lea un archivo y escriba un nuevo archivo con las líneas en orden inverso (es decir que la primera línea del archivo viejo se convierte en la última línea del archivo nuevo). (hacerlo)

 

2) Escribe un programa que lea un archivo y escriba sólo las líneas que contengan el substring gato. (hacerlo)

 

3) Escribe un programa que lea un archivo de texto y produzca un archivo de salida que sea una copia del original, excepto por el hecho de que las primeras 5 columnas del nuevo archivo contienen un número de 4 cifras seguido de un espacio. Comienza numerando la primera línea con el número 0001. Asegúrate de que cada número esté formateado con el mismo ancho en el archivo de salida. Puedes usar uno de tus programas Python como archivo de entrada para este ejercicio: el output debería ser el mismo programa con sus líneas numeradas. (hacerlo)

 

4) Escribe un programa que deshaga la numeración del ejercicio anterior: debería leer un archivo con líneas numeradas y producir otro archivo con líneas sin numerar. (hacerlo)