Ir al contenido
wannacode

Datos en archivos

En resumen: CSV es un archivo de texto en el que cada línea es un registro y los valores van separados por comas; en Python el módulo csv divide las líneas y lee las columnas por nombre.

Problema

Texto, pero necesitas números

El almacén exporta sus productos a un archivo. Cada fila es un producto: nombre, precio y existencias, separados por comas. La primera fila dice qué hay en cada columna:

products.csv
Nombre,Precio,Existencias
Pan,1.20,30
Leche,2.10,18
Café en grano,18.90,12

Ya sabes leer el archivo. Pero para el programa, la fila es solo texto:

line = "Pan,1.20,30"
print(len(line))
print(line[4])
Salida
11
1

No sabe que 1.20 es un precio y que 30 es una cantidad. Para calcular cuánto vale la mercancía, tienes que convertir cada fila en algo con campos: un objeto Product.

La idea

CSV: de fila a objeto

Un archivo así se llama CSV (comma-separated values, «valores separados por comas»). Lo lee Excel, lo leen las bases de datos, lo lee cualquier programa. Una fila se convierte en objeto en tres pasos: split, convertir cada parte al tipo que necesitas y el constructor.

Lo más cómodo es que sea un método en la propia clase que no necesita un objeto, sino que lo crea: @staticmethod.
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
@staticmethod
def from_csv(line):
f = line.split(",")
if len(f) != 3:
raise ValueError(f"esperaba 3 columnas, pero hay {len(f)}")
return Product(f[0].strip(), float(f[1]), int(f[2]))
def to_csv(self):
return f"{self.name},{self.price},{self.stock}"
p = Product.from_csv("Café en grano,18.90,12")
print(f"{p.name}: {p.price:.2f} € x {p.stock} = {p.price * p.stock:.2f} €")
Salida
Café en grano: 18.90 € x 12 = 226.80 €

from_csv es el único lugar que sabe cómo es la fila. Si el formato cambia, cambias solo eso. La comprobación del número de columnas lanza ValueError con un mensaje claro, en lugar de que el programa se detenga más tarde con un índice fuera de la lista. float e int lanzan ValueError por sí solos si el texto no es un número, y quitan por sí solos los espacios alrededor del número.

Qué hace from_csv con la fila, paso a paso, con su cuerpo desarrollado línea por línea:

line = "Café en grano,18.90,12"
f = line.split(",")
name = f[0]
price = float(f[1])
stock = int(f[2])
p = Product(name, price, stock)
print(f"{p.name}: {p.price} € x {p.stock}")
print(f"{p.price * p.stock:.2f} €")
Paso a paso, en palabras
  1. La fila del archivo es una sola cadena. Para el programa, el 18.90 que hay en ella son solo cuatro caracteres.
  2. split corta la fila en cada coma: una lista de tres cadenas.
  3. El nombre sigue siendo una cadena.
  4. float convierte la cadena "18.90" en el número 18.9. Ya se puede calcular con él.
  5. int saca de "12" el número entero 12.
  6. Product(...) crea un objeto nuevo, __init__ pone los tres valores en sus campos, y p apunta a él.
  7. p.price se imprime como 18.9: el cero del final solo estaba en el texto.
  8. El precio por las existencias: ahora es un cálculo con números.

Míralo

El archivo completo

Cargas todos los productos en una lista y calculas el valor de la mercancía:
try:
with open("products.csv", "w", encoding="utf-8") as file:
file.write("Nombre,Precio,Existencias\nPan,1.20,30\nLeche,2.10,18\nCafé en grano,18.90,12\n")
with open("products.csv", encoding="utf-8") as file:
lines = file.read().splitlines()
products = []
header = True
for line in lines:
if header:
header = False
continue
products.append(Product.from_csv(line))
value = 0
for p in products:
value += p.price * p.stock
print(f"{len(products)} productos, existencias por valor de {value:.2f} €")
except OSError as e:
print(f"No puedo leer products.csv: {e}")
Salida
3 productos, existencias por valor de 300.60 €
  • products empieza como una lista vacía, y append añade cada producto nuevo al final. len(products) es cuántos hay.
  • header es True solo en la primera vuelta. Entonces la fila se salta con continue.

Hay más sobre las listas en el módulo «Colecciones».

La idea

Filas incorrectas

Los archivos los escriben personas y otros programas. Tarde o temprano llega una fila a la que le falta una columna, con una palabra en lugar de un número, o una fila vacía:

products.csv
Nombre,Precio,Existencias
Pan,1.20,30
Leche,2.10
Café,siete,4
Té,3.50,25

Un error no debe detener la carga de todo lo demás. Cada fila va en su propio try, y el mensaje da el número de fila, para que alguien pueda corregirla:

try:
with open("products.csv", "w", encoding="utf-8") as file:
file.write("Nombre,Precio,Existencias\nPan,1.20,30\nLeche,2.10\nCafé,siete,4\n\nTé,3.50,25\n")
with open("products.csv", encoding="utf-8") as file:
lines = file.read().splitlines()
products = []
row = 0
for line in lines:
row += 1
if row == 1 or not line.strip():
continue
try:
products.append(Product.from_csv(line))
except ValueError as e:
print(f"Fila {row}: {e}")
print(f"Cargados: {len(products)}")
except OSError as e:
print(f"No puedo leer products.csv: {e}")
Salida
Fila 3: esperaba 3 columnas, pero hay 2
Fila 4: could not convert string to float: 'siete'
Cargados: 2
  • Las filas vacías se saltan: line.strip() quita los espacios, y si no queda nada, not da True.
  • Un solo except ValueError atrapa los dos errores: el que lanza el propio from_csv y el de float("siete"). En Python no hay un tipo de excepción aparte para un número incorrecto. El mensaje dice cuál ocurrió.
  • El programa dice «Cargados: 2» y sigue con ellos.

Pruébalo

Columnas vacías

En la primera fila el precio está vacío, en la segunda las existencias, y la tercera fila está completamente vacía:

¿Qué imprimirá el programa?

print(len("Café,,4".split(",")))
print(len("Café,7.50,".split(",")))
print(len("".split(",")))
Respuesta

La idea

De objetos a CSV

La otra dirección es un método que arma la fila:

def to_csv(self):
return f"{self.name},{self.price},{self.stock}"

Escribes la fila de encabezado y después una fila por cada objeto:

stock = [Product("Pan", 1.2, 30), Product("Leche", 2.1, 18)]
try:
with open("stock.csv", "w", encoding="utf-8") as out:
out.write("Nombre,Precio,Existencias\n")
for p in stock:
out.write(p.to_csv() + "\n")
with open("stock.csv", encoding="utf-8") as file:
print(file.read(), end="")
except OSError as e:
print(f"No puedo escribir stock.csv: {e}")
Salida
Nombre,Precio,Existencias
Pan,1.2,30
Leche,2.1,18

from_csv puede volver a leer este archivo. Guardar y leer tienen que usar el mismo formato.

En Java, el formato de los números sigue la configuración de idioma de la computadora, y en español o alemán escribe una coma que separa las columnas. El f-string de Python no la sigue: {price:.2f} escribe un punto en cualquier computadora.

line = f"Pan,{1.2:.2f},30"
print(f"{line} -> {len(line.split(','))} columnas")
Salida
Pan,1.20,30 -> 3 columnas

La verdadera trampa es una coma en el propio valor: «Queso, curado» se convierte en dos columnas. El módulo csv de la biblioteca estándar conoce la regla: un valor así va entre comillas. csv.writer las pone al guardar, y csv.reader las quita al leer y te da cada fila como una lista:

import csv
with open("cheese.csv", "w", encoding="utf-8", newline="") as out:
writer = csv.writer(out, lineterminator="\n")
writer.writerow(["Nombre", "Precio", "Existencias"])
writer.writerow(["Queso, curado", 9.8, 5])
with open("cheese.csv", encoding="utf-8", newline="") as file:
print(file.read(), end="")
with open("cheese.csv", encoding="utf-8", newline="") as file:
for row in csv.reader(file):
print(row)
Salida
Nombre,Precio,Existencias
"Queso, curado",9.8,5
['Nombre', 'Precio', 'Existencias']
['Queso, curado', '9.8', '5']
  • newline="" tiene que estar en cada archivo que lees o escribes con csv: el módulo se ocupa él mismo de los saltos de línea.
  • lineterminator="\n" escribe las filas con \n. Sin él, csv.writer pone \r\n, como en Windows y en el estándar de CSV.

Pruébalo

Encuentra el error

El programa debería cargar los tres productos de products.csv, pero se detiene ya en la primera fila:

Error
ValueError: could not convert string to float: 'Precio'

Elige la línea con el error.

La idea

Columnas por nombre

Otro programa exporta los mismos datos, pero con las columnas en otro orden y con una más. Si escribes f[1] para el precio, con un formato nuevo leerás la columna equivocada y no te darás cuenta. Es más seguro buscar la columna por su nombre en la fila de encabezado:

def column(header, name):
for i in range(len(header)):
if header[i].strip().lower() == name.lower():
return i
raise ValueError(f"No existe la columna {name}")
try:
with open("export.csv", "w", encoding="utf-8") as file:
file.write("Existencias,Nombre,Código,Precio\n30,Pan,1001,1.20\n18,Leche,1002,2.10\n")
name_col = -1
price_col = -1
with open("export.csv", encoding="utf-8") as file:
for line in file.read().splitlines():
f = line.split(",")
if name_col == -1:
name_col = column(f, "nombre")
price_col = column(f, "precio")
continue
print(f"{f[name_col]}: {f[price_col]} €")
except OSError as e:
print(e)
Salida
Pan: 1.20 €
Leche: 2.10 €

La primera vuelta solo busca dónde están las columnas. Después, cada fila se lee por esos índices. Si la columna no existe, column lanza una excepción enseguida, en lugar de leer datos equivocados.

Más precisamente, para curiosos
  • Columnas por nombre con csv. csv.DictReader hace la búsqueda por nombre por ti: la primera fila da los nombres, y cada fila siguiente es un diccionario (dict) del nombre de la columna al valor. Los diccionarios están en el módulo «Colecciones».
import csv
with open("export.csv", "w", encoding="utf-8") as file:
file.write("Existencias,Nombre,Código,Precio\n30,Pan,1001,1.20\n18,Leche,1002,2.10\n")
with open("export.csv", encoding="utf-8", newline="") as file:
for row in csv.DictReader(file):
print(f"{row['Nombre']}: {row['Precio']} €")
  • Otro separador. Excel en una computadora con la configuración en español o en alemán suele guardar el CSV con ;, precisamente porque la coma es la marca decimal. Entonces lees con csv.reader(file, delimiter=";").
  • Serialización. El módulo pickle escribe un objeto entero en un archivo. El archivo es binario: una persona no puede leerlo, y no deberías cargar un archivo pickle de una fuente desconocida, porque al cargarlo puede ejecutar código. Los formatos de texto como CSV y JSON (el módulo json) son más predecibles, y por eso se usan más.

Ejercicio

Datos que cobran vida

Resuelve las tareas en tu editor. El programa crea primero él mismo el archivo de entrada.

1. Análisis de un CSV. Crea people.csv con el contenido de abajo. Imprime la edad media (división entera), quién es la persona de más edad y cuántas personas hay de cada ciudad, en el orden en que aparecen las ciudades. Guarda las ciudades en una lista, y las cantidades en otra, en las mismas posiciones.

people.csv
Nombre,Edad,Ciudad
Carlos,34,Madrid
María,28,Sevilla
Jorge,41,Madrid
Elena,25,Valencia
Salida
Edad media: 32
De más edad: Jorge, 41
Personas de Madrid: 2
Personas de Sevilla: 1
Personas de Valencia: 1

2. Guardar al héroe. Una clase Hero con nombre, nivel, vida y oro. El método save(path) guarda al héroe como líneas clave=valor, y el método estático load(path) crea un Hero nuevo a partir de un archivo así. Crea a «Arin» con nivel 6, 100 de vida y 120 de oro, súbele el nivel en 1, quítale 15 de vida, dale 220 de oro, guárdalo, imprime el archivo y vuelve a cargarlo.

Salida
name=Arin
level=7
health=85
gold=340
Cargado: Arin, nivel 7, vida 85, oro 340

3. Texto duplicado. Crea text.txt con el texto de abajo. Divídelo en oraciones por el punto (y los espacios que lo siguen), e imprime una sola vez cada oración que aparece más de una vez.

text.txt
Esto es texto. Esto también es texto. Esto es texto.
Salida
Texto duplicado: "Esto es texto."

Más tareas

  • El producto más caro. Carga products.csv de la lección, e imprime el producto con el precio más alto y cuántos productos tienen menos de 20 en existencias.
  • Filas corregidas. Carga el archivo con las filas incorrectas, y escribe las buenas en products-clean.csv, y las incorrectas, con el número de fila y el motivo, en rejected.txt.
  • Informe de ventas. Un archivo sales.csv con las columnas Fecha,Producto,Cantidad,Precio. Las filas están ordenadas por fecha. Imprime los ingresos de cada día con un f-string en columnas.

Test

Ponte a prueba

1. ¿Qué es la primera fila de la mayoría de los archivos CSV?
2. from_csv lanza ValueError cuando el número de columnas es incorrecto, y float("siete") lanza con un número incorrecto. ¿Cuántos except necesitas para atrapar los dos?
3. ¿Cuánto es len("Café,7.50,".split(","))?
4. El nombre del producto es «Queso, curado». ¿Cómo lo escribes en CSV sin que se convierta en dos columnas?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·