Indice / Repaso: archivos y errores / 1.1
El archivo como fuente de datos
Antes de que read_csv lo esconda todo, conviene ver qué
hay abajo: un archivo es una secuencia de bytes, y para convertirla en texto hace falta
saber en qué codificación está.
Para qué sirve
pandas resuelve en una línea lo que esta unidad hace a mano, y eso es justamente el
motivo de hacerlo a mano una vez: cuando read_csv falle - y va a
fallar - el mensaje habla de bytes, de codificaciones y de rutas. Sin haber
visto qué son, ese mensaje no dice nada.
Tres cosas de esta lección reaparecen en todo el curso: el
with, el encoding y
cómo se resuelve una ruta relativa.
ELI5Explicado facil
Un archivo en el disco es una tira larguísima de números. Que esos números signifiquen «José» y no cualquier otra cosa depende de qué tabla uses para traducirlos.
La codificación es esa tabla. Si abrís el archivo con la tabla equivocada, no se rompe nada: simplemente lee otra palabra.
El ejemplo mínimo
from pathlib import Path
ruta = Path("contactos_origen.csv")
print("existe:", ruta.exists())
print("nombre:", ruta.name)
print("extension:", ruta.suffix)
print("tamano:", ruta.stat().st_size, "bytes")
# with cierra el archivo pase lo que pase: al terminar, o si algo falla en el
# medio. Sin el, un error deja el archivo abierto.
with open(ruta, encoding="utf-8") as f:
primeras = [next(f) for _ in range(3)]
print()
for linea in primeras:
print(repr(linea))
print()
print("Fijate en el \\n al final de cada linea: open devuelve el salto")
print("incluido. Es la fuente de la mitad de los errores de comparacion")
print("cuando se lee un archivo a mano.")
existe: True
nombre: contactos_origen.csv
extension: .csv
tamano: 2616 bytes
'nombre,correo,telefono,ciudad\n'
'Guadalupe Vega,guadalupe.vega@example.com,+54 11 5555-0895,San Juan\n'
'José Juárez,jose.juarez@example.com,+54 11 5555-0218,San Juan\n'
Fijate en el \n al final de cada linea: open devuelve el salto
incluido. Es la fuente de la mitad de los errores de comparacion
cuando se lee un archivo a mano.
with, siempre
with open(...) as f: cierra el archivo pase lo que pase: al terminar el
bloque, o si algo falla en el medio. Sin él, una excepción deja el archivo abierto, y
en Windows eso además impide borrarlo o volver a escribirlo.
Y fijate en el \n del final de cada línea: open devuelve
el salto incluido. Es la causa de la mitad de las comparaciones que fallan cuando se
lee un archivo a mano.
encoding
import locale
import os
import sys
from pathlib import Path
ruta = Path("contactos_origen.csv")
with open(ruta, encoding="utf-8") as f:
contenido = f.read()
crudo = ruta.read_bytes()
print("bytes en disco :", len(crudo))
print("caracteres leidos:", len(contenido))
print()
# La diferencia tiene DOS causas, y conviene separarlas.
acentuados = [c for c in contenido if ord(c) > 127]
finales = crudo.count(b"\r\n")
print("1) cada acento ocupa 2 bytes en UTF-8 ->", len(acentuados), "acentos")
print("2) el archivo termina las lineas con CR LF y open descarta el CR ->",
finales, "lineas")
print(" ", len(crudo), "-", len(acentuados), "-", finales, "=",
len(crudo) - len(acentuados) - finales)
print()
print("caracteres no ASCII distintos:", sorted(set(acentuados)))
print()
try:
with open(ruta, encoding="ascii") as f:
f.read()
except UnicodeDecodeError as error:
print("leido como ascii ->", type(error).__name__)
print(str(error))
# latin-1 NO falla nunca: cualquier byte es un caracter valido para el.
# Por eso es el error peligroso, el que no avisa.
mal = crudo.decode("latin-1")
print()
print("leido como latin-1 -> no hay excepcion, pero:")
print(" ", mal.splitlines()[2][:34])
print()
print("Sin encoding=, open usa el del sistema. En una instalacion tipica de")
print("Windows eso es cp1252, no UTF-8, y el mismo programa anda en una")
print("maquina y falla en otra.")
print()
print("codificacion por defecto aca:", locale.getpreferredencoding(False))
print("modo UTF-8 activado :", bool(sys.flags.utf8_mode))
print("PYTHONUTF8 :", os.environ.get("PYTHONUTF8", "(sin definir)"))
print()
print("Esta corrida tiene PYTHONUTF8=1, que fuerza UTF-8 en todos lados: por")
print("eso dice utf-8. Sin esa variable, en Windows diria cp1252.")
print("Escribir encoding= es lo que hace que no dependa de nada de esto.")
bytes en disco : 2616
caracteres leidos: 2533
1) cada acento ocupa 2 bytes en UTF-8 -> 42 acentos
2) el archivo termina las lineas con CR LF y open descarta el CR -> 41 lineas
2616 - 42 - 41 = 2533
caracteres no ASCII distintos: ['á', 'é', 'í', 'ñ', 'ó', 'ú']
leido como ascii -> UnicodeDecodeError
'ascii' codec can't decode byte 0xc3 in position 103: ordinal not in range(128)
leido como latin-1 -> no hay excepcion, pero:
José Juárez,jose.juarez@example.
Sin encoding=, open usa el del sistema. En una instalacion tipica de
Windows eso es cp1252, no UTF-8, y el mismo programa anda en una
maquina y falla en otra.
codificacion por defecto aca: utf-8
modo UTF-8 activado : True
PYTHONUTF8 : 1
Esta corrida tiene PYTHONUTF8=1, que fuerza UTF-8 en todos lados: por
eso dice utf-8. Sin esa variable, en Windows diria cp1252.
Escribir encoding= es lo que hace que no dependa de nada de esto.
encoding=, el programa depende de la máquina
open usa la codificación del sistema, que en una instalación típica de
Windows es cp1252 y en Linux o Mac es UTF-8. El mismo programa, con el
mismo archivo, anda en una máquina y falla en otra.
Escribir encoding="utf-8" es una palabra y elimina la clase entera de
problema. En este curso va siempre, incluso cuando «funciona igual».
Fijate en el peor caso del ejemplo: leído como ascii el archivo
falla, y eso es bueno - el error dice exactamente qué byte y en
qué posición. Leído como latin-1 no falla: devuelve
José Juárez y sigue de largo. latin-1 le asigna un carácter
a cada uno de los 256 bytes posibles, así que nunca puede quejarse. Es la codificación
que conviene probar cuando UTF-8 falla, pero siempre mirando después si los acentos
salieron bien.
El archivo pesa 2616 bytes y Python lee 2533 caracteres. Los 83 de diferencia son
42 acentos (2 bytes cada uno en UTF-8, 1 carácter) más 41 finales de línea: estos
archivos usan CR LF como corte, y el modo texto de open
convierte los dos bytes en un solo \n.
Esa traducción automática se llama universal newlines y es cómoda al leer.
Al escribir CSV se vuelve un problema, y es el motivo del
newline="" que aparece en la lección 1.3.
Cómo recorrerlo
from pathlib import Path
ruta = Path("estudiantes_notas.csv")
# 1. read(): todo el archivo en una cadena. Comodo y caro.
with open(ruta, encoding="utf-8") as f:
todo = f.read()
print("read() ->", len(todo), "caracteres en memoria")
# 2. readlines(): una lista con todas las lineas. Igual de caro.
with open(ruta, encoding="utf-8") as f:
lineas = f.readlines()
print("readlines() ->", len(lineas), "lineas en memoria")
# 3. Recorrer el archivo: una linea por vez, sin cargarlo entero.
total = 0
with open(ruta, encoding="utf-8") as f:
for linea in f:
total += 1
print("for linea in f ->", total, "lineas, UNA en memoria por vez")
print()
print("Con 41 lineas da igual. Con un log de 4 GB, las dos primeras no")
print("entran en memoria y la tercera anda igual de bien.")
print()
# Path tiene atajos para los casos chicos.
print("Path.read_text() hace lo mismo que la primera, en una linea:")
print(len(ruta.read_text(encoding="utf-8")), "caracteres")
read() -> 1308 caracteres en memoria
readlines() -> 41 lineas en memoria
for linea in f -> 41 lineas, UNA en memoria por vez
Con 41 lineas da igual. Con un log de 4 GB, las dos primeras no
entran en memoria y la tercera anda igual de bien.
Path.read_text() hace lo mismo que la primera, en una linea:
1308 caracteres
| Forma | Qué carga en memoria | Cuándo |
|---|---|---|
f.read() | El archivo entero, como una cadena | Archivos chicos, o cuando necesitás el texto completo |
f.readlines() | Una lista con todas las líneas | Casi nunca: cuesta lo mismo que read |
for linea in f: | Una línea por vez | Siempre que el archivo pueda ser grande |
ruta.read_text(encoding=) | El archivo entero | El atajo de read, en una línea |
Rutas
import os
from pathlib import Path
print("directorio de trabajo:", Path.cwd().name)
relativa = Path("contactos_origen.csv")
print()
print("ruta relativa:", relativa)
print("a que apunta :", relativa.resolve().name)
print("existe :", relativa.exists())
print()
print("Esa ruta NO se busca al lado del archivo .py: se busca en el")
print("directorio de trabajo, que es desde donde se ejecuto el programa.")
print("Es el error mas comun al correr algo desde el depurador (leccion 0.1).")
print()
# La forma robusta: una ruta relativa AL ARCHIVO de codigo.
aqui = Path(__file__).resolve().parent
print("carpeta de este .py:", aqui.name)
print("asi se arma una ruta que no depende del directorio de trabajo:")
print(" Path(__file__).resolve().parent / 'datos' / 'archivo.csv'")
print()
# Y las rutas se arman con /, no pegando cadenas: funciona en los tres
# sistemas operativos y no hay que acordarse de las barras.
carpeta = Path("datos")
print("carpeta / 'sub' / 'archivo.csv' ->",
(carpeta / "sub" / "archivo.csv").as_posix())
print("os.sep en esta maquina:", repr(os.sep))
directorio de trabajo: datos
ruta relativa: contactos_origen.csv
a que apunta : contactos_origen.csv
existe : True
Esa ruta NO se busca al lado del archivo .py: se busca en el
directorio de trabajo, que es desde donde se ejecuto el programa.
Es el error mas comun al correr algo desde el depurador (leccion 0.1).
carpeta de este .py: ejemplos
asi se arma una ruta que no depende del directorio de trabajo:
Path(__file__).resolve().parent / 'datos' / 'archivo.csv'
carpeta / 'sub' / 'archivo.csv' -> datos/sub/archivo.csv
os.sep en esta maquina: '\\'
Una ruta relativa no se busca al lado del archivo .py:
se busca en el directorio de trabajo, que es desde donde se ejecutó el programa. Es
exactamente el FileNotFoundError de la lección 0.1, y el motivo por el que
todos los ejemplos de este curso se corren desde la carpeta de los datos.
/, no pegando cadenas
Path("datos") / "sub" / "archivo.csv" produce la ruta correcta en los
tres sistemas operativos, sin que haya que acordarse de si la barra va para un lado o
para el otro. Y Path(__file__).resolve().parent da la carpeta del propio
programa, que es la base robusta cuando el archivo tiene que estar al lado del
código.
Dónde se rompe
Causa: la ruta relativa se resuelve contra el directorio de trabajo, no
contra la carpeta del .py.
Arreglo: print(Path.cwd()) para ver desde dónde está
corriendo, y si el archivo tiene que estar al lado del código,
Path(__file__).resolve().parent / "datos.csv".
Causa: el archivo no está en UTF-8.
Arreglo: probá encoding="latin-1" o "cp1252".
latin-1 nunca falla al leer - puede leer cualquier byte -
pero puede dar texto sin sentido, así que confirmá que los acentos se vean bien.
Causa: se leyó un archivo UTF-8 con una codificación de un byte. No hay excepción porque todos los bytes son válidos en esa tabla.
Arreglo: encoding="utf-8". Es el caso peligroso: el que no
avisa.
Causa: el archivo está abierto en Excel, o la ruta es una carpeta.
Arreglo: cerrarlo, o revisar que ruta.is_file() sea
verdadero.
Causa: cada línea trae su \n al final, así que
linea == "Ana" es falso para la línea "Ana\n".
Arreglo: linea.rstrip("\n"), o mejor, usar el módulo
csv (lección 1.3), que ya lo resuelve.
Recetario
| Expresión | Qué hace | Cuándo |
|---|---|---|
with open(r, encoding="utf-8") as f: | Abre y cierra solo | Siempre |
open(r, "w", encoding="utf-8") | Escribe, borrando lo que había | Con cuidado |
open(r, "a", encoding="utf-8") | Agrega al final | Logs |
for linea in f: | Una línea por vez | Archivos que pueden ser grandes |
Path("a") / "b" / "c.csv" | Arma la ruta | Siempre, en vez de pegar cadenas |
Path(__file__).resolve().parent | La carpeta del programa | Rutas que no dependen del cwd |
Path.cwd() | Directorio de trabajo | Cuando no encuentra un archivo |
ruta.exists() / .is_file() | bool | Antes de abrir |
ruta.stat().st_size | Tamaño en bytes | Antes de leerlo entero |
ruta.suffix / .stem / .name | Partes del nombre | Sin cortar cadenas a mano |
ruta.read_text(encoding="utf-8") | Todo el contenido | Archivos chicos |
carpeta.glob("*.csv") | Los archivos que coinciden | Procesar una carpeta |