Saltar al contenido

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

EjemploAbrir un archivo, y por que siempre con with
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

Ejemploencoding no es opcional
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.
Sin 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.

Los bytes no son los caracteres, por dos motivos a la vez

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

EjemploTres formas de recorrer un archivo, y cual conviene
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
Tres formas, y cuándo
FormaQué carga en memoriaCuándo
f.read()El archivo entero, como una cadenaArchivos chicos, o cuando necesitás el texto completo
f.readlines()Una lista con todas las líneasCasi nunca: cuesta lo mismo que read
for linea in f:Una línea por vezSiempre que el archivo pueda ser grande
ruta.read_text(encoding=)El archivo enteroEl atajo de read, en una línea

Rutas

EjemploRutas: la relativa depende de donde corras el programa
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.

Las rutas se arman con /, 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

FileNotFoundError: [Errno 2] No such file or directory: 'datos.csv'

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".

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf3

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.

(sin error) los acentos se ven como é o ñ

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.

PermissionError: [Errno 13] Permission denied

Causa: el archivo está abierto en Excel, o la ruta es una carpeta.

Arreglo: cerrarlo, o revisar que ruta.is_file() sea verdadero.

(sin error) las comparaciones con el contenido nunca dan

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

Archivos y rutas
ExpresiónQué haceCuándo
with open(r, encoding="utf-8") as f:Abre y cierra soloSiempre
open(r, "w", encoding="utf-8")Escribe, borrando lo que habíaCon cuidado
open(r, "a", encoding="utf-8")Agrega al finalLogs
for linea in f:Una línea por vezArchivos que pueden ser grandes
Path("a") / "b" / "c.csv"Arma la rutaSiempre, en vez de pegar cadenas
Path(__file__).resolve().parentLa carpeta del programaRutas que no dependen del cwd
Path.cwd()Directorio de trabajoCuando no encuentra un archivo
ruta.exists() / .is_file()boolAntes de abrir
ruta.stat().st_sizeTamaño en bytesAntes de leerlo entero
ruta.suffix / .stem / .namePartes del nombreSin cortar cadenas a mano
ruta.read_text(encoding="utf-8")Todo el contenidoArchivos chicos
carpeta.glob("*.csv")Los archivos que coincidenProcesar una carpeta

Ponete a prueba

1. ¿Para qué sirve escribir with open(ruta, encoding="utf-8") as f: en vez de f = open(ruta, encoding="utf-8")?Nivel 1 · Recordar
2. El archivo tiene una primera línea nombre,correo,telefono,ciudad. ¿Qué imprime print(repr(next(f)))?Nivel 1 · Recordar
3. Un CSV en UTF-8 con acentos se lee con open(ruta, encoding="latin-1"). ¿Qué pasa?Nivel 2 · Predecir la salida
4. El archivo pesa 2616 bytes y len(f.read()) devuelve 2533. ¿Por qué la diferencia?Nivel 2 · Predecir la salida
5. Tenés que procesar un log de 4 GB contando cuántas líneas contienen ERROR. Las cuatro opciones son código válido: ¿cuál conviene?Nivel 3 · Elegir entre dos casi correctas
6. El script C:\proyecto\src\informe.py hace open("datos.csv") y datos.csv está en C:\proyecto\src\. Se ejecuta con python src\informe.py parado en C:\proyecto. ¿Qué pasa?Nivel 3 · Elegir entre dos casi correctas
7. Este código busca a Ana en el archivo, ella está, y sin embargo imprime 0. ¿Por qué?
ruta = Path("contactos_origen.csv")
encontradas = 0
with open(ruta, encoding="utf-8") as f:
    for linea in f:
        campos = linea.split(",")
        if campos[-1] == "San Juan":
            encontradas += 1
print(encontradas)
Nivel 4 · Encontrar el defecto