martes, 22 de febrero de 2022

Trabajar con archivos externos. Persistencia de datos.

imagen de entrada conteniendo archivos


En este post vamos a aprender como guardar nuestros datos en un archivo en el disco. Hasta ahora todo lo que hacíamos en Python se guardaba en memoria, pero si apagamos el equipo todo nuestro trabajo o datos se pierden. Así que vamos a aprender como conservar esos datos a través de un archivo de texto.


Crear Archivos.

Puede parecer poco lógico, pero para leer un archivo existente o crear uno nuevo, tienes que crear primero un objeto, al cual puedes llamar como quieras. Yo lo llamaré "file". Pero no te confundas, crear este objeto no es crear el archivo.

Aquí tienes un ejemplo de código que abre un archivo (foo.txt) para lectura, para ir familiarizándonos con la forma.

> file = open('foo.txt', 'r')
> print(file.read())
El resultado son las líneas de texto que contenga el archivo.

Aquí haremos un inciso. Si intentas abrir para escritura un archivo que no existe, lógicamente Python te mostrará un error. Cuando la apertura de un archivo falla, el objeto de Exception IOerror tiene una propiedad llamada errno, que contiene el código de finalización de la operación fallida. Podemos usar este valor para diagnosticar el problema. 

Veámoslo con un ejemplo. Asegúrate de que en el directorio en el que estar trabajando no haya ningún archivo llamado foo.txt. Ahora ejecuta este código:

> >>> open('foo.txt','r')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'foo.txt'

Para saber cual es el error número 2 podemos usar strerror del módulo os de esta manera.

>>> print(strerror(2))
No such file or directory
El código elegante para capturar el error sería este:

from os import strerror

try:
    s = open("foo.txt", "rt")
    # El procesamiento va aquí.
    s.close()
except Exception as exc:
    print("El archivo no pudo ser abierto:", strerror(exc.errno))

Salida:

El archivo no pudo ser abierto: No such file or directory

Puedes encontrar más información sobre los errores de entrada/salida en https://docs.python.org/3.10/library/errno.html o importando el modulo errno y buscando en la ayuda (help(errno)).

Dicho lo cual continuamos.

Hemos creado el objeto "file" usando la función incorporada open(). La sintaxis global de open() es:

open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

Solamente el nombre del archivo es obligatorio, todos los demás son optativos. Vamos a centrarnos un poco en los valores que puede tener el segundo parámetro:


Modo Descripción
'r' Es la opción por defecto. Abre el archivo en modo lectura. (read)
'w Abre un archivo para escribir en él. Ojo porque crea un nuevo archivo si no existe, pero si existe lo borra y remplaza su contenido. (write)
'a' Abre el archivo para añadir contenido al final del mismo, pero sin borrar lo que ya tenga. (append). Si el archivo no existe crea uno nuevo.
'x' Abre un archivo exclusivamente para crearlo. Si el archivo ya existe devuelve un error. 
 'b'  Modo binario o para trabajar con archivos binarios.
 't  Modo texto. (el que viene por defecto)
 '+'  Abre el archivo para actualizarlo(lectura y escritura) r+ 

En cuanto al tercer parámetro encoding=None por defecto es utf.8. Como nota a mayores si quisiéramos saber el encoding por defecto del sistema con el que trabajamos (ascii, utf-8...) podemos usar el siguiente código de Python:

> import locale
> print(locale.getpreferredencoding())
UTF-8


Volviendo al ejemplo previo, hemos especificado que nos gustaría usar "open()" en el archivo "foo.txt" en modo lectura y poder hacer referencia  a ese nuevo objeto (de archivo de lectura) con la variable "file". Una vez que tenemos el objeto 'file' creado, podemos usar el método read() que lee todo el contenido del archivo.

Crear un nuevo archivo para escritura es muy similar a la forma que hemos usado para la lectura. En lugar de usar el parámetro 'r', utilizamos una 'w'. Vamos a crear un nuevo archivo que no existe y grabar el texto "Hola Mundo". Ojo que si el nombre del archivo ya existiese y pusiéramos el mismo nombre en nuestro ejemplo el nuevo contenido lo guardaría encima, borrando el texto que ya tuviésemos.

> file = open('nuevo.txt', 'w')
> file.write('Hola Mundo')
10
> file.close()

nota: la función write() devuelve el número de bytes escritos, en nuestro caso se corresponde con 10 caracteres.

En este ejemplo, especificamos que nos gustaría usar open() en el archivo "nuevo.txt" en modo de escritura y poder referirnos a ese nuevo objeto de archivo en el que se puede escribir con la variable "file".  Una vez que tenemos el objeto "file", podemos write() "escribir" algo de texto y close() "cerrar" el archivo.

Siempre tenemos que cerrar los archivos abiertos para que no se corrompan. Esto lo podemos hacer de dos maneras.

La primera de ellas es rodear el comando open() con un bloque try/finally, especialmente cuando escribamos datos al archivo con la llamada write(). Aquí tienes un ejemplo de un archivo en el que se puede escribir situado en un bloque try/finally:

try:
    file = open('nuevo.txt', 'w')
    file.write('otro texto')
finally:
    file.close()

Esta forma de escribir archivos hace que el método close() se invoque cuando ocurre ocurre alguna excepción  en algún lugar del bloque try. En realidad, el archivo se cierra siempre ya que los bloques finally se ejecutan después de completarse los bloques try, tanto si se encuentra una excepción como si no.

La segunda forma es usando la sentencia with que nos permite utilizar lo que se denomina un gestor de contexto. Cuando se completa el bloque with, tanto si ocurre una excepción como sino, el método close() del objeto se invoca implícitamente cerrando automáticamente el archivo. 

with open('nuevo.txt','w') as file:
    file.write('Estamos usando la intrucción with')

Date cuenta de como, usando esta forma ahorramos código y nos aseguramos de que el archivo no se corrompa en caso de que ocurra un error. Aunque no invocamos la llamada close() en el objeto 'file', el gestor de contexto lo hace automáticamente por nosotros después de salir del bloque with.

Es una buena práctica asegurarnos de gestionar las posibles excepciones que se puedan generar y asegurarnos de que los objetos 'file' se cierran cuando se espera que lo hagan.

Dos aclaraciones antes de pasar al siguiente punto. 

Lo que estamos escribiendo en el archivo son cadenas de texto. Si queremos guardar algo que no sean cadenas de texto en este tipo de archivos, deberemos convertirlo en cadenas primero. 

La segunda cuestión es que con lo que hemos visto hasta ahora todo el texto se guarda como una enorme línea de texto, todo seguido. Si queremos poner saltos de línea tenemos que introducirlos nosotros utilizando el carácter de escape \n


Leer Archivos.

Una vez que tienes un objeto archivo de lectura 'file', que has abierto con la etiqueta r, existen tres métodos que son de utilidad para obtener los datos contenidos en el archivo: read(), readline() y readlines(). Pero antes de eso, vamos a ver como este objeto se puede recorrer directamente con un bucle for como cualquier string o lista de python.

Pasemos a verlo con un ejemplo.

Dado el siguiente archivo de texto (texto.txt) cuyo contenido es:

archivo de texto con tres líneas de texto


with open('texto.txt', 'r') as file:
    for linea in file:
        print(linea)

Primera línea.

Segunda línea.

Tercera línea.

Esto sin embargo nos deja unas líneas vacías. Esto es porque aunque no lo veamos al final de cada línea hay un carácter no imprimible (\n) que es el salto de línea. Por eso cuando Python lee el archivo línea a línea este se ejecuta ya que aunque no lo veamos directamente. Si queremos eliminarlo podemos usar el método strip() que nos elimina espacios en blanco, comillas y saltos de línea al principio y final de cada una. Lo haríamos de esta forma.

with open('texto.txt', 'r') as file:
    for linea in file:
        print(linea.strip())

 que nos devuelve:

Primera línea.
Segunda línea.
Tercera línea.


Una vez visto lo anterior vamos a ver los métodos de los que hablábamos.

- read():  Lee todo el fichero tal cual está, es decir nos devuelve un objeto string del archivo abierto. Podemos pasarle un parámetro opcional que es el numero de bytes (caracteres) a leer. Si especificamos más bytes de los que hay en el archivo, read() leerá hasta el final del archivo y devolverá los bytes que haya leído.

Pasemos a verlo con el ejemplo anterior.

read() funciona en un archivo de esta forma.

with open('texto.txt', 'r') as file:
    print(file.read())
Primera línea.
Segunda línea.
Tercera línea.

Observa que cada frase se muestra en una línea distinta al usar \n al final de la misma.

Y si solamente quisiéramos los 7 primeros bytes del archivo, podríamos hacer algo como esto:

with open('texto.txt', 'r') as file:
    print(file.read(7))
Primera

Ahora el puntero esta en el byte 7, si le pidiésemos que leyera otros 3 bytes más, esto es lo que obtendríamos

   print(file.read(3)
 li

Como vemos el puntero se desplaza a medida que vamos avanzando en la lectura del archivo.

El siguiente método para obtener texto de un archivo es el método readline(). La finalidad de readline() es leer una línea de texto de cada vez, de un archivo. Es decir, lee todo el texto existente hasta que se encuentra un salto de línea. Podemos especificar el número máximo de bytes que readline() leerá antes de devolver una cadena, tanto si ha llegado al final de la línea como si no.

En el siguiente ejemplo, el programa leerá la primera línea y luego leerá los 7 primeros bytes de la segunda, seguido por lo que queda de la segunda línea. Para finalizar leerá la tercera.

>>> file = open('texto.txt')
>>> file.readline()
'Primera línea.\n'
>>> file.readline(7)
'Segunda'
>>> file.readline()
' línea.\n'
>>> file.readline()
'Tercera línea.'


El último método readlines(), en plural, devuelve una lista con todas las líneas del archivo.

>>> file = open('texto.txt')
>>> file.readlines()
['Primera línea.\n', 'Segunda línea.\n', 'Tercera línea.']
>>> 


Escribir Archivos.

Algunas veces necesitamos hacer algo más con los archivos que leer datos de los mismos; Seguramente necesitaremos crear nuestros propios archivos y escribir datos en ellos. Existen dos métodos que nos valen para escribir datos en archivos. El primer método, que ya hemos visto antes, es write().

file.write(cadena)

 Este toma como parámetro la cadena a escribir en el archivo. Aquí tenemos un ejemplo de datos que se escriben en un archivo utilizando el método write():

file = open("algun_texto.txt", "w")
file.write('Probando\nel archivo\n')
file.close()

file_read = open('algun_texto.txt')
print(file_read.read())
Probando
el archivo

En la primera línea, abrimos el archivo con el modo 'w', que significa "write" escritura. En la segunda línea se escriben dos líneas en el archivo. En la cuarta línea, utilizamos el nombre de variable file_read esta vez para asignarle el objeto archivo, aunque podíamos haber utilizado 'file' de nuevo. En la última línea imprimimos los datos para comprobar que son los mismos que escribimos primeramente en el mismo.

El siguiente método común de escribir datos es writelines() que escribe el contenido de una lista (que deben ser strings) en un archivo, línea por línea. Es como la inversa de readlines()

file.writelines(iterable_cadenas)

Writelines() tiene un parámetro obligatorio: una secuencia que writelines() escribirá en el archivo abierto. La secuencia puede ser cualquier tipo de objetos por los que se puede iterar (una lista, tupla, cadena, un generador). 

 Aquí vamos a ver un ejemplo de bucle for utilizado para escribir datos en un archivo utilizando el método write() que ya vimos.

file = open('archivo_writelines.txt', 'w')
for i in range(10):
    file.write(f'{i}\n')
file.close()

g = open('archivo_writelines.txt')
print(g.read())
0
1
2
3
4
5
6
7
8
9
y ahora vamos a ver el mismo ejemplo pero usando writelines().

serie = []
file = open('archivo_writelines.txt', 'w')
for numero in range(10):
    serie.append(str(numero)+'\n')
# imprimimos la serie para ver lo que vamos a guardar
print(serie)

# guardamos la lista de una vez.
file.writelines(serie)
file.close()

g = open('archivo_writelines.txt')
print(g.read())

['0\n', '1\n', '2\n', '3\n', '4\n', '5\n', '6\n', '7\n', '8\n', '9\n']
0
1
2
3
4
5
6
7
8
9


ES MUY IMPORTANTE indicar que writelines() no nos escribe una nueva línea (\n); tenemos que proporcionarla nosotros en la secuencia que le pasemos.

Otros atributos que podemos utilizar en el objeto 'file' son:

file.name -> nos devuelve el nombre del archivo.
file.mode -> nos devuelve el modo con el que se ha abierto el archivo.
file.encoding ->  nos devuelve el tipo de codificación del fichero.
file.closed -> nos devuelve True si el fichero está abierto y False si esta cerrado.
file.tell() -> nos indica en que posición se encuentra el puntero en el archivo
file.seek(n) -> posiciona el puntero del archivo en la posición n

Un ejemplo de todas estas llamadas podría ser el siguiente. Consideremos un archivo de texto llamado texto.txt con el siguiente contenido:

'Primera línea.
Segunda línea.
Tercera línea.'

>>> file = open('texto.txt')
>>> file.name
'texto.txt'
>>> file.mode
'r'
>>> file.encoding
'UTF-8'
>>> file.closed
False
>>> file.tell() #el puntero está en el primer elemento ya que no hemos leído ningún dato.
0
>>> file.read()
'Primera línea.\nSegunda línea.\nTercera línea.\n'
>>> file.tell() #ahora el puntero del archivo está en el último elemento.
48
>>> # colocamos en puntero en el primer elemento 
>>> # para volver a leer el archivo
>>> file.seek(0)
0

En nuestros programas cuando almacenemos el valor de una línea en una variable, esta se guardará con el salto de línea incluido. Para quitarlo utilizamos el método .strip() que quita los espacios en blanco, saltos de línea... existentes al principio y final de una cadena.

>>> # leemos la primera linea del archivo
>>> file.readline()
'Primera línea.\n'
>>> # Como vemos incluye el salto de línea al final del mismo. 
>>> # Para quitarlo utilizamos .strip
>>> # Vuelvo a poner el puntero al principio del archivo con file.seek(0)
>>> file.readline().strip()
'Primera línea.'


Comentar también que se puede imprimir en un archivo de Python usando el comando print que habitualmente usamos para imprimir cosas por pantalla. Para ello tenemos que abrir el archivo en modo de escritura y pasar el argumento file al comando print. Aquí te muestro un ejemplo:

# Abre el archivo en modo escritura
with open('archivo.txt', 'w') as f:
    # Utiliza print con el argumento file para escribir en el archivo
    print('Hola mundo!', file=f)
    print('Esta es otra línea.', file=f)

También podemos mandar texto a la salida stderr de una forma similar a grabar datos en un archivo. Pero tanto stdin, stderr y stdout están siempre abiertos, no hace falta abrirlos así que para escribir solo tenemos que hacer:

import sys
sys.stderr.write("Mensaje de Error")


Persistencia con archivo CSV

Los archivos Csv ('valores separados por comas') son un tipo particular de los archivos de texto. En ellos, los datos se separan normalmente por comas y cada línea de datos finaliza con un retorno \n. Son muy utilizados como forma de guardar e importar archivos de datos en hojas de calculo y bases de datos. Puedes abrir un archivo CSV con EXCEL por ejemplo. 

Para que te hagas una idea, si abres con un procesador de texto uno de estos archivos tiene la siguiente forma:

nombre, edad, grupo
pepe, 12, A
Maria, 32, B  

Como puedes ver, los elementos de un archivo csv están separados por comas. En este ejemplo la coma es un delimitador pero podríamos usar otros, como por ejemplo un tabulador.

Internamente el archivo seria una cadena de texto con estos datos:

'nombre, edad, grupo\npepe, 12, A\nMaria, 32, B\n'

Para ver como funcionan este tipo de archivos, podemos abrir un procesador de textos cualquiera y escribimos un archivo con los siguientes datos.

nano ejemplo.csv


Trabajando con archivos CSV en Python. Módulos de Lectura y Escritura.

Si bien podríamos usar la función open() incorporada en Python para trabajar con estos tipos de archivos, que no dejan de ser archivos de texto, hay un módulo csv que facilita mucho el trabajo con los mismos. 

Pero antes de usar los métodos de lectura y escritura, debemos importar el módulo csv de la siguiente forma:

import csv


Leyendo archivos csv con el método csv.reader() 

El módulo csv.reader() nos va a servir para crear un objeto con la información del archivo que queramos procesar. Su funcionamiento es similar a lo que ya vimos para leer archivos de texto.

import csv
with open('ejemplo.csv', 'r') as file:
    datos = csv.reader(file)
    for i in datos:
        print(i)
['nombre', 'edad', 'grupo']
['Pepe', '12', 'A']
['María', '34', 'B']
['Antonio', '56', 'A']
['Eugenia', '34', 'B']

En este ejemplo hemos abierto el archivo 'ejemplo.csv' en modo de lectura usando:

with open('ejemplo.csv', 'r') as file:
    .. ... ..

Después, hemos usado csv.reader() para leer el archivo, el cual nos devuelve un objeto iterable que guardamos en la variable 'datos'. Como vemos el objeto 'datos' al recorrerlo con un bucle for, nos devuelve cada una de las filas del archivo en forma de lista.

El módulo CSV también contiene la función next(), que devuelve la siguiente línea del archivo cuando se le pasa el objeto lector. Esto nos evita tener que usar un bucle for para recorrerlo sin no queremos usarlo:

import csv
with open('ejemplo.csv','r') as file:
    datos = csv.reader(file) # contiene una lista por cada fila de datos.
    linea_datos = next(datos)
    print(linea_datos)


En el ejemplo anterior hemos usado csv.reader() en su forma estandar, usando las comas como delimitador. Sin embargo esta función es más personalizable. Supongamos que en el archivo 'ejemplo.csv' en vez de comas, hubiésemos usado tabuladores como delimitador de datos. Para trabajar con el archivo podríamos haber usado el siguiente código:

import csv
with open('ejemplo.csv','r') as file:
    datos = csv.reader(file, delimiter='\t')
    for i in datos:
        print(i)

Date cuenta como hemos usado el parámetro opcional delimiter='\t'

La sintaxis completa de la función csv.reader() es:

csv.reader(csvfile, dialect='excel', **optional_parameters)

Para ver muchos más detallas sobre 'dialect' y los parámetros opcionales que admite esta función puedes encontrarla en el siguiente link.


Escribiendo archivos CSV con csv.writer()

Se utiliza para escribir datos en un archivo csv. Como ya tenemos creado el archivo 'ejemplo.csv' vamos a abrirlo para añadir una nueva fila de datos. Realizaremos la apertura con el parámetro 'a' , de append o añadir, lo que nos servirá para añadir datos al final del archivo. Si solo utilizáramos 'w' borraría los datos del archivo y nos añadiría las nuevas líneas.

import csv
with open('ejemplo.csv', 'a') as file:
    datos_a_escribir = csv.writer(file)
    datos_a_escribir.writerow(['Lis', '32' ,'C'])
    datos_a_escribir.writerow(['Alex', '18', 'A'])
nombre,edad,grupo
Pepe,12,A
María,34,B
Antonio,56,A
Eugenia,34,B
Lis,32,C
Alex,18,A

Hemos pasado los nuevos datos a grabar como una lista. Esta lista fue convertida a un string por el programa y añadida al archivo csv con la función writerow().

Si quisiéramos añadir varias listas al contenido del archivo podemos hacerlo con la función writerows().

import csv
with open('ejemplo.csv', 'a') as file:
    datos_a_escribir = csv.writer(file)
    datos_a_escribir.writerows([['Lucas','15','C'],['Dylan','43','B']]) 
Aquí le hemos pasado una lista, que a su vez contiene otras dos con los datos que queremos añadir al archivo, usando el método writerows() para escribirlos en el archivo.


Usando la clase csv.DictReader()

El objeto devuelto por la clase csv.DictReader() puede ser usado para leer un archivo como si fuera un diccionario. Seguimos usando el archivo 'ejemplo.csv'

import csv
with open('ejemplo.csv','r') as file:
    datos = csv.DictReader(file)
    for fila in datos:
        print(fila)  
{'nombre': 'Pepe', 'edad': '12', 'grupo': 'A'}
{'nombre': 'María', 'edad': '34', 'grupo': 'B'}
{'nombre': 'Antonio', 'edad': '56', 'grupo': 'A'}
{'nombre': 'Eugenia', 'edad': '34', 'grupo': 'B'}
{'nombre': 'Lis', 'edad': '32', 'grupo': 'C'}
{'nombre': 'Alex', 'edad': '18', 'grupo': 'A'}
{'nombre': 'Lucas', 'edad': '15', 'grupo': 'C'}
{'nombre': 'Dylan', 'edad': '43', 'grupo': 'B'}

Nota: Si usas una versión de Python inferior a la 3.8 tendrás que usar print(dict(fila))

Como puedes ver, las entradas de la primera fila se utilizan como claves del diccionario. Y las entradas de las otras filas son utilizadas como valores.


La clase de Python csv.DictWriter()

El objeto generado por la clase csv.DictWriter() puede utilizarse para escribir un diccionario en un archivo CSV. 

La sintaxis mínima de la misma es:

csv.DictWriter(file, fieldnames)

en donde,

file - es el objeto file en el que queremos escribir.

fieldnames - es una lista que contiene los nombres de los campos con el orden en el que se escribirán los datos.

Vamos a crear un archivo nuevo para guardar los datos - ejemplo2.csv -

import csv
with open('ejemplo2.csv','w') as file:
    datos = csv.DictWriter(file, ['fruta','color'])

    # Escribimos la primera fila que contendrá las llaves del diccionario o cabecera.
    datos.writeheader()
    # Escribimos el diccionario
    datos.writerow({'fruta':'pera','color':'verde'})  
    datos.writerow({'fruta':'limon','color':'amarillo'})
    datos.writerow({'fruta':'manzana','color':'verde'})
fruta,color
pera,verde
limon,amarillo
manzana,verde

Podríamos pasar varios diccionarios usando writerows([{..},...,{...}]) y pasando una lista con los datos (los diccionarios)



Persistencia con archivos Binarios.


¿Que es un bytearray?


Antes de comenzar a hablar de archivos binarios, vamos a ver una de las clases especializadas que usa Python para almacenar datos amorfos. Los datos amorfos son datos que no tienen una forma específica, solo son una serie de bytes. Esto no significa que estos bytes no puedan tener su propio significado o que no puedan representar un objeto útil, por ejemplo, puntos de un gráfico de mapa de bits.

Lo más importante es que en el lugar en el que tenemos contacto con los datos,  no podemos o queremos saber nada al respecto. Los datos amorfos no pueden guardarse utilizando alguno de los sistemas que ya hemos visto, no son cadenas ni listas. Para ello utilizamos una clase especial llamada bytearray

Por ejemplo si queremos usar un contenedor para leer una imagen de mapa de bits, tenemos que crearlo explícitamente.

datos = bytearray(10)

Esta invocación crea un objeto bytearray capaz de almacenar 10 bytes. El constructor rellena toda la estructura con ceros. Los bytearrays se parecen en muchos aspectos a las listas.  Por ejemplo, son mutables, se puede usar sobre ellos la función len, y puedes acceder a cualquiera de sus elementos usando la indexación convencional.

Existe una limitación importante: no debes establecer ningún elemento del arreglo de bytes con un valor que no sea un entero y tampoco está permitido usar un valor fuera del rango 0 - 255. Recuerda que un byte son 8 bits y que con ello se pueden crear 2 elevado a 8 números binarios distintos es decir 256 números.

Se puede tratar cualquier elemento del arreglo de bytes como un entero. Echa un vistazo a este ejemplo.

datos = bytearray(10)

for i in range(len(datos)):
    datos[i] = 10 - i

for b in datos:
    print(hex(b))
Salida:

0xa
0x9
0x8
0x7
0x6
0x5
0x4
0x3
0x2
0x1

Hemos usado dos métodos para iterar el arreglo de bytes y la función hex() para ver los elementos impresos como valores hexadecimales.

Dicho lo cual, vamos a ver como guardar un arreglo de bytes en un archivo binario, ya que no queremos guardar su representación legible sino escribir en el archivo uno a uno el contenido de la memoria física byte a byte.

Observa el siguiente código.

from os import strerror

data = bytearray(10)

for i in range(len(data)):
    data[i] = 10 + i

try:
    bf = open('file.bin', 'wb')
    bf.write(data)
    bf.close()
except IOError as e:
    print("Se produjo un error de E/S:", strerror(e.errno))

Analicémoslo:

  • Primero, inicializamos bytearray con valores a partir de 10; si deseas que el contenido del archivo sea claramente legible, reemplaza el 10con algo como ord('a'), esto producirá bytes que contienen valores correspondientes a la parte alfabética del código ASCII (no pienses que harás que el archivo sea un archivo de texto; sigue siendo binario, ya que se creó con un indicador: wb).
  • Después, creamos el archivo usando la función open(), la única diferencia en comparación con las variantes anteriores es que el modo de apertura contiene el indicador b.
  • El método write() toma su argumento (bytearray) y lo envía (como un todo) al archivo.
  • El stream se cierra de forma rutinaria.

El método write() devuelve la cantidad de bytes escritos correctamente.

Si los valores difieren de la longitud de los argumentos del método, puede significar que hay algunos errores de escritura. En este caso, no hemos utilizado el resultado; esto puede no ser apropiado en todos los casos.

Intenta ejecutar el código de forma local en tu ordenador y analiza el contenido del archivo recién creado.

Lo vamos a usar en el siguiente paso.

Cómo leer bytes de un flujo (stream)

La lectura de un archivo binario requiere el uso de un método especializado llamado readinto() ya que el método no crea un nuevo objeto del arreglo de bytes, sino que llena uno creado previamente con los valores tomados del archivo binario.

Nota:

  • El método devuelve el número de bytes leídos con éxito.
  • El método intenta llenar todo el espacio disponible dentro de su argumento; si existen más datos en el archivo que espacio en el argumento, la operación de lectura se detendrá antes del final del archivo; el resultado del método puede indicar que el arreglo de bytes solo se ha llenado de manera fragmentaria (el resultado también lo mostrará y la parte del arreglo que no está siendo utilizada por los contenidos recién leídos permanece intacta).

Observa el código a continuación:

from os import strerror

data = bytearray(10)

try:
    binary_file = open('file.bin', 'rb')
    binary_file.readinto(data)
    binary_file.close()

    for b in data:
        print(hex(b), end=' ')
except IOError as e:
    print("Se produjo un error de E/S:", strerror(e.errno))

Salida:

0xa 0xb 0xc 0xd 0xe 0xf 0x10 0x11 0x12 0x13 

Analicémoslo:


Primero, abrimos el archivo (el que se creó usando el código anterior) con el modo descrito como rb. Luego, leemos su contenido en el arreglo de bytes llamado data, con un tamaño de diez bytes. Finalmente, imprimimos el contenido del arreglo de bytes: ¿Son los mismos que esperabas?


Ejecuta el código y verifica si funciona.


Se ofrece una forma alternativa de leer el contenido de un archivo binario mediante el método denominado read().


Invocado sin argumentos, intenta leer todo el contenido del archivo en la memoria, haciéndolo parte de un objeto recién creado de la clase bytes.
Esta clase tiene algunas similitudes con bytearray, con la excepción de una diferencia significativa: es immutable.


Afortunadamente, no hay obstáculos para crear un arreglo de bytes tomando su valor inicial directamente del objeto de bytes, como aquí:

from os import strerror

try:
    binary_file = open('file.bin', 'rb')
    data = bytearray(binary_file.read())
    binary_file.close()

    for b in data:
        print(hex(b), end=' ')

except IOError as e:
    print("Se produjo un error de E/S:", strerror(e.errno))

Pero ¡Ten cuidado! No utilices este tipo de lectura sino estás seguro de que el contenido del archivo se ajusta a la memoria disponible en tu sistema.

Si el método read() se invoca con un argumento, se le indica el número máximo de bytes a leer.

El método intenta leer la cantidad deseada de bytes del archivo, y la longitud del archivo devuelto puede usarse para determinar la cantidad de bytes realmente leídos.

Por ejemplo. 

from os import strerror

try:
    binary_file = open('file.bin', 'rb')
    data = bytearray(binary_file.read(5))
    binary_file.close()

    for b in data:
        print(hex(b), end=' ')

except IOError as e:
    print("Se produjo un error de E/S:", strerror(e.errno))

Salida:


0xa 0xb 0xc 0xd 0xe 

Se han leído los primeros 5 bytes del archivo mientras que los restantes están a la espera de ser leídos.

Para repasar un poco lo que hemos visto creemos un programa sencillo para copiar un archivo:

from os import strerror

srcname = input("Ingresa el nombre del archivo fuente: ")
try:
    src = open(srcname, 'rb')
except IOError as e:
    print("No se puede abrir archivo fuente: ", strerror(e.errno))
    exit(e.errno)	

dstname = input("Ingresa el nombre del archivo destino: ")
try:
    dst = open(dstname, 'wb')
except Exception as e:
    print("No se puede crear el archivo de destino: ", strerror(e.errno))
    src.close()
    exit(e.errno)	

buffer = bytearray(65536)
total  = 0
try:
    readin = src.readinto(buffer)
    while readin > 0:
        written = dst.write(buffer[:readin])
        total += written
        readin = src.readinto(buffer)
except IOError as e:
    print("No se puede crear el archivo de destino: ", strerror(e.errno))
    exit(e.errno)	
    
print(total,'byte(s) escritos con éxito')
src.close()
dst.close()

Analicémoslo:

Las líneas 3 a la 8: solicitan al usuario el nombre del archivo a copiar e intentan abrirlo para leerlo; se termina la ejecución del programa si falla la apertura; nota: emplea la función exit() para detener la ejecución del programa y pasar el código de finalización al sistema operativo; cualquier código de finalización que no sea 0 significa que el programa ha encontrado algunos problemas; se debe utilizar el valor errno para especificar la naturaleza del problema.

Las líneas 10 a la 16: repiten casi la misma acción, pero esta vez para el archivo de salida.

La línea 18: prepara una parte de memoria para transferir datos del archivo fuente al destino; tal área de transferencia a menudo se llama un búfer, de ahí el nombre de la variable; el tamaño del búfer es arbitrario; en este caso, decidimos usar 64 kilobytes; técnicamente, un búfer más grande es más rápido al copiar elementos, ya que un búfer más grande significa menos operaciones de E/S; en realidad, siempre hay un límite, cuyo cruce no genera más ventajas; pruébalo tú mismo si quieres.

Línea 19: cuenta los bytes copiados: este es el contador y su valor inicial.

Línea 21: intenta llenar el búfer por primera vez.

Línea 22: mientras se obtenga un número de bytes distinto a cero, repite las mismas acciones.

Línea 23: escribe el contenido del búfer en el archivo de salida (nota: hemos usado un segmento para limitar la cantidad de bytes que se escriben, ya que write() siempre prefiere escribir todo el búfer).

Línea 24: actualiza el contador.

Línea 25: lee el siguiente fragmento de archivo.

Las líneas 30 a la 32: limpieza final, el trabajo está hecho.



La b
iblioteca Pickle.

Vamos a ver de forma rápida como guardar datos en código binario con la biblioteca Pickle. Hasta hace poco los archivos que habíamos guardado anteriormente eran archivos de texto que podíamos ver con cualquier editor de texto, sin embargo con este módulo los datos se guardarán de forma binaria, (al igual que por ejemplo una imagen).

Podemos utilizar esta biblioteca para guardar cualquier objeto de Python (cadenas, listas, diccionarios etc) 

Esta biblioteca Pickle tiene dos métodos:

dump = para volcar datos en el archivo.

load = para cargar datos de un archivo.


Por ejemplo, para guardar una lista que contenga una serie de números haríamos lo siguiente:

import pickle

lista = [1,2,3,8]

with open('archivo_bin','wb') as file:
    pickle.dump(lista, file)


Como en anteriores ejemplos empezamos importando la librería, en este caso pickle. Abrimos el archivo que contendrá los datos. Fíjate en la particularidad que lo abrimos como 'wb',  es decir que lo escribimos para escribir datos binarios. Usamos pickle.dump(objeto a guardar, nombre del objeto archivo) para grabar los datos al fichero.

Si por el contrario quisiéramos leer esta lista, después de abrir el archivo en modo binario de lectura con 'rb', usaríamos el método pickle.load():

import pickle

with open('archivo_bin','rb') as file:
    lista = pickle.load(file)
    print(lista)
  ... [1, 2, 3, 8]


Persistencia usando archivos json.

Otra forma de poder guardar datos, y que no sea mediante un archivo de solo cadenas, son los archivos json.  Si usamos el módulo JSON no solo podremos intercambiar datos entre distintos programas de Python, sino que también lo podremos hacer con programas escritos en otros lenguajes de programación ya que JSON (Java Script Object Notation) se desarrolló originalmente para Javascript pero actualmente lo usan muchos lenguajes de programación para intercambiar datos.

El funcionamiento es muy similar al anterior ya que utilizaremos json.dump() para guardar los datos (cadenas, listas, diccionarios etc) y json.load() para recuperarlos.

Para verlo con un ejemplo, vamos a crear un programa que pida su nombre al usuario y si este no lo ha hecho previamente (no existe el archivo json) lo cree y se guarde.

import json

# Carga el nombre de usuario si se ha guardado previamente.
# Si no es el caso, solicita el nombre del usuario y lo
# guardado

archivo = 'nombre_usuario.json'
try:
    with open(archivo) as f:
        nombre_usuario = json.load(f)
except FileNotFoundError:
    nombre_usuario = input("¿Como te llamas? > ")
    with open(archivo, 'w') as f:
        json.dump(nombre_usuario, f)
        print(f"la próxima vez te recordaré, usuario {nombre_usuario}")
else:
    print(f"Bienvenido: {nombre_usuario}")


Si el archivo nombre_usuario.json existe lo leerá. Si no existe creará uno con los datos y en cualquiera de los dos casos terminará con una frase de bienvenida.


Persistencia usando la librería "shelve".


La librería shelve en Python proporciona una forma sencilla de almacenar y recuperar objetos en una base de datos persistente. Básicamente, shelve actúa como un "estante" o "estantería" en la que puedes guardar objetos Python y luego recuperarlos más adelante. Está diseñada para ser fácil de usar y ofrece una interfaz similar a un diccionario.

La librería shelve utiliza un formato de almacenamiento basado en clave-valor, donde cada objeto almacenado se identifica mediante una clave única. Puedes pensar en ello como una especie de diccionario persistente en el que puedes guardar objetos complejos y luego recuperarlos utilizando su clave correspondiente.

Una característica interesante de shelve es que puede almacenar objetos Python arbitrarios, incluyendo listas, diccionarios, clases personalizadas, entre otros. Internamente, shelve utiliza el módulo pickle para la serialización y deserialización de los objetos.

Aquí hay un ejemplo simple que muestra cómo utilizar shelve para almacenar y recuperar objetos:

import shelve

# Abrir o crear una base de datos de estante
db = shelve.open('mi_estante.db')

# Almacenar objetos en la base de datos
db['clave1'] = 'valor1'
db['clave2'] = [1, 2, 3]
db['clave3'] = {'nombre': 'Juan', 'edad': 30}

# Recuperar objetos de la base de datos
valor1 = db['clave1']
lista = db['clave2']
diccionario = db['clave3']

print(valor1)  # Imprime: valor1
print(lista)  # Imprime: [1, 2, 3]
print(diccionario)  # Imprime: {'nombre': 'Juan', 'edad': 30}

# Cerrar la base de datos
db.close()


En este ejemplo, se crea o abre una base de datos de estante utilizando shelve.open(). Luego, se almacenan objetos en la base de datos utilizando claves y valores. Posteriormente, se recuperan los objetos utilizando las claves correspondientes. Finalmente, se cierra la base de datos utilizando close().

La librería shelve es útil cuando necesitas almacenar objetos de Python en un formato persistente y recuperarlos en un momento posterior. Puede ser útil para almacenar datos, configuraciones o cualquier otro objeto que necesites conservar más allá de la ejecución actual del programa.

Si queremos abrir el archivo creado solo como lectura hay que añadir en open el argumento flag = 'r' despues del nombre del archivo.

Para añadir o trabajar con datos de una estantería ya existente.


Hay que tener cuidado porque por defecto shelve no detecta cambios en estanterías ya creadas salvo que usemos writeback = True en los argumentos.

Por defecto, cuando se almacena un objeto en shelve, se crea una copia en la base de datos y los cambios posteriores en el objeto no se reflejan automáticamente en la copia almacenada. Si se utiliza el argumento writeback=True al abrir la base de datos, se activa el modo de escritura y los cambios realizados en los objetos almacenados se actualizan automáticamente en la base de datos sin requerir una operación de reescritura explícita.

 Para entenderlo mejor veamos un ejemplo:

>>> import shelve
>>> open shelve.open('mi_estanteria') as db:
>>> db['key1'] = {'int':10, 'float': 9.5, 'texto': 'una frase'}
# Esto funciona como se espera.
>>> print(db['key1'])
{'int': 10, 'float': 9.5, 'texto': 'una frase'}
>>> db['key1']['nuevo']='no estaba antes' 
# Pero esto no funciona. No actualiza online la nueva clave.
>>> print(db['key1'])
{'int': 10, 'float': 9.5, 'texto': 'una frase'}
db.close()
    
Si queremos que funcione on fly tenemos que añadir el parametro writeback = True de la siguiente manera.

>>> import shelve
>>> open shelve.open('mi_estanteria', writeback = True) as db:
>>> db['key1'] = {'int':10, 'float': 9.5, 'texto': 'una frase'}
>>> print(db['key1'])
{'int': 10, 'float': 9.5, 'texto': 'una frase'}

>>> db['key1']['nuevo']='no estaba antes' 
>>> print(db['key1'])
{'int': 10, 'float': 9.5, 'texto': 'una frase', 'nuevo': 'no estaba antes'}
>>> db.close()

Esto es a costa de sacrificar un poco de memoria y que el cierre del archivo se haga más lentamente.

Metodo .sync()

El método .sync() de la librería shelve en Python se utiliza para asegurar que los cambios realizados en la base de datos se escriban físicamente en el disco. Cuando se modifican objetos almacenados en shelve, los cambios se mantienen en memoria y se escriben en disco de forma diferida para mejorar el rendimiento. Sin embargo, si necesitas asegurarte de que los cambios se guarden de inmediato, puedes llamar al método .sync().

Aquí tienes un ejemplo para ilustrar el uso del método .sync():

import shelve

# Abrir la base de datos de estante
with shelve.open('mi_estante.db') as db

    # Almacenar un objeto en la base de datos
    db['clave1'] = 'valor1'

    # Modificar el objeto almacenado
    db['clave1'] = 'valor_modificado'

    # Guardar los cambios en disco
    db.sync()

En este ejemplo, se abre la base de datos de estante y se almacena un objeto con la clave 'clave1'. Luego, se modifica el objeto asignando un nuevo valor a la clave 'clave1'. Después de realizar la modificación, se llama al método .sync() para asegurarse de que los cambios se guarden físicamente en el disco.

Es importante destacar que el método .sync() puede ser útil en situaciones en las que necesitas garantizar que los cambios se guarden de forma inmediata y no quieres depender de la escritura diferida en la base de datos. Sin embargo, debes tener en cuenta que el uso frecuente de .sync() puede afectar el rendimiento, ya que implica una operación de escritura en disco adicional. Por lo tanto, se recomienda utilizar este método de manera consciente y evaluar las necesidades específicas de tu aplicación.


Y con esto finalizamos un repaso sencillo sobre la persistencia de datos en Python.