La lliçó anterior va tancar amb una autocrítica: el format titol | preu | estoc del nostre inventari és un dialecte casolà que només entén el nostre programa, i que es trenca el dia que un títol contingui el separador. Avui Papyrus es passa a CSV (Comma-Separated Values), el format tabular de text que parlen Excel, Google Sheets, els bancs, les distribuïdores de llibres i pràcticament qualsevol sistema amb què l'Anna vulgui intercanviar dades. Aprendràs el mòdul csv de la biblioteca estàndard — reader/writer i, sobretot, DictReader/DictWriter —, resoldràs els casos retorçats (comes i cometes dins d'un camp) sense escriure ni una sola línia de parsing, i completaràs dues fites del fil de Papyrus: el catàleg viatjarà a cataleg.csv i tornarà com a dict[str, Llibre], i les LiniaVenda de 05-06 acabaran on aquella solució ja va anunciar que acabarien — en un CSV de vendes.
Contingut
- Què és CSV i per què és la lingua franca de les dades tabulars
- Per què no n'hi ha prou amb
split(",") - Escriure amb
csv.writer(i el misteri denewline="") - Llegir amb
csv.reader DictReaderiDictWriter: la manera preferida- Round-trip complet: el catàleg de Papyrus a
cataleg.csvi de tornada - El registre de vendes:
LiniaVendacompleix el seu destí - Delimitadors, cometes i casos reals
- CSV a gran escala: una menció a pandas
- Errors comuns i consells
- Exercicis amb solucions
Què és CSV i per què és la lingua franca de les dades tabulars
Un CSV és un fitxer de text pla — el pots obrir amb qualsevol editor, com els de 06-01 — que representa una taula: una línia per fila, camps separats per comes, i habitualment una primera línia de capçalera amb els noms de les columnes:
La seva força no és tècnica sinó social: tothom l'entén. L'Anna pot obrir cataleg.csv a Excel o Google Sheets i veure un full de càlcul; la distribuïdora pot enviar-li les novetats en CSV; i el teu programa Python pot llegir i escriure tots dos. Quan dos sistemes que no es coneixen han d'intercanviar taules, CSV és el punt de trobada — per això se l'anomena lingua franca.
| Text pla casolà (06-01) | CSV | |
|---|---|---|
| Separador | Inventat (|) |
Convenció universal (,) |
| Capçalera amb noms | No | Sí (habitual) |
| L'obren Excel/Sheets | Com a text sense estructura | Com a taula |
| Camps amb el separador a dins | Es trenca | Resolt (cometes) |
| Suport a Python | A mà (split) |
Mòdul csv de la biblioteca estàndard |
Per què no n'hi ha prou amb split(",")
La temptació és evident: si els camps van separats per comes, linia.strip().split(",") (04-05) sembla suficient. I ho és… fins al primer títol amb coma:
Aquest llibre existeix a Papyrus, i el seu títol conté una coma. La convenció CSV ho resol posant el camp entre cometes, però llavors split(",") retorna ['"Anna', ' la de les Teulades Verdes"', '11.75', '3'] — quatre trossos on hi havia tres camps, i amb cometes enganxades. Afegeix-hi cometes dins de cometes i el parsing casolà es converteix en un fangar. La lliçó és general: no parsegis formats estàndard a mà; fes servir el mòdul que ja sap fer-ho. Per a CSV, aquest mòdul es diu csv i ve amb Python.
Escriure amb csv.writer (i el misteri de newline="")
import csv
llibres = [
("L'Odissea", 12.50, 4),
("Hamlet", 9.95, 6),
("El Quixot", 15.90, 8),
("Faust", 21.00, 0),
]
with open("cataleg.csv", "w", encoding="utf-8", newline="") as f:
escriptor = csv.writer(f)
escriptor.writerow(["titol", "preu", "estoc"]) # la capcalera
for titol, preu, estoc in llibres:
escriptor.writerow([titol, preu, estoc]) # una fila per llibrePas a pas:
csv.writer(f)embolcalla el fitxer obert (elwithi l'encoding="utf-8"de 06-01 segueixen manant) i retorna un escriptor.writerow(llista)escriu una fila: converteix cada element a text, hi posa les comes, posa entre cometes el que calgui i afegeix el salt de línia. També existeixwriterows(llista_de_files)per abocar-ne moltes de cop.newline=""és obligatori en obrir un fitxer per al mòdulcsv, tant en escriure com en llegir. Per què? El mòdulcsvgestiona ell mateix els finals de línia (l'estàndard CSV fa servir\r\n); si no desactives la traducció automàtica de salts que Python fa en mode text, a Windows totes dues capes hi afegeixen la seva i apareixen línies en blanc entre les files. No ho memoritzis com a màgia:open(..., newline="")significa "no toquis els salts, que d'això se n'ocupacsv".
Fixa't que hem escrit 12.50 com a número i csv.writer el va convertir a text per nosaltres. La conversió d'anada és automàtica; la de tornada, com veuràs, no.
Llegir amb csv.reader
import csv
with open("cataleg.csv", "r", encoding="utf-8", newline="") as f:
lector = csv.reader(f)
capcalera = next(lector) # ['titol', 'preu', 'estoc'] — la saltem
for fila in lector: # el lector es iterable, linia a linia (memoria constant)
print(fila)
# ["L'Odissea", '12.50', '4']
# ['Hamlet', '9.95', '6']
# ...Cada fila arriba com a llista de cadenes — totes cadenes, també el preu i l'estoc. És el mateix peatge de 06-01: el text no recorda tipus, i csv no endevina si "4" era un enter o un codi postal. Convertir (float(fila[1]), int(fila[2])) segueix sent feina teva. I aquí treu el cap la debilitat de reader: els camps van per posició (fila[0], fila[1]…), així que si algú reordena les columnes del CSV, el teu codi llegeix preus on esperava estocs sense queixar-se. La solució és llegir per nom de columna.
DictReader i DictWriter: la manera preferida
DictReader fa servir la capçalera del fitxer per lliurar-te cada fila com un diccionari columna → valor; DictWriter fa el simètric en escriure. Són l'opció preferida en aquest curs: el codi es llegeix sol i sobreviu a reordenacions de columnes.
import csv
with open("cataleg.csv", "r", encoding="utf-8", newline="") as f:
lector = csv.DictReader(f) # fa servir la primera linia com a capcalera
for fila in lector:
print(f'{fila["titol"]}: {float(fila["preu"]):.2f} € — estoc {fila["estoc"]}')
# L'Odissea: 12.50 € — estoc 4
# Hamlet: 9.95 € — estoc 6
# ...I l'escriptura, declarant les columnes a fieldnames:
with open("cataleg.csv", "w", encoding="utf-8", newline="") as f:
escriptor = csv.DictWriter(f, fieldnames=["titol", "preu", "estoc"])
escriptor.writeheader() # escriu la capcalera per tu
escriptor.writerow({"titol": "L'Odissea", "preu": 12.50, "estoc": 4})reader/writer |
DictReader/DictWriter |
|
|---|---|---|
| Cada fila és… | list (accés per índex) |
dict (accés per nom) |
| Capçalera | La gestiones tu (next, writerow) |
Automàtica (fieldnames, writeheader) |
| Si reordenen columnes | Bug silenciós | Segueix funcionant |
| Llegibilitat | fila[1] |
fila["preu"] |
| Quan fer-lo servir | CSV sense capçalera, scripts mínims | Per defecte |
Round-trip complet: el catàleg de Papyrus a cataleg.csv i de tornada
Anem a la fita de la lliçó: desar el catàleg canònic dict[str, Llibre] (05-06) en tancar la botiga i reconstruir-lo — objectes inclosos — en obrir-la. És el mateix round-trip de l'exercici 2 de 06-01, però en un format que Excel entén i sense separadors casolans:
import csv
from pathlib import Path
from dataclasses import dataclass
def normalitzar_titol(text):
return text.strip().casefold() # la clau canonica de 04-05/05-06
@dataclass
class Llibre: # la dataclass de 05-06
titol: str
preu: float
estoc: int = 0
IVA_LLIBRES = 0.04
DESCOMPTE_SOCI = 0.05
def __post_init__(self):
if self.preu < 0:
raise ValueError(f"Preu negatiu: {self.preu}")
if self.estoc < 0:
raise ValueError(f"Estoc negatiu: {self.estoc}")
def preu_final(self, soci=False):
descompte = Llibre.DESCOMPTE_SOCI if soci else 0
return round(self.preu * (1 - descompte) * (1 + Llibre.IVA_LLIBRES), 2)
def hi_ha_estoc(self):
return self.estoc > 0
COLUMNES_CATALEG = ["titol", "preu", "estoc"]
def desar_cataleg(cataleg, ruta="cataleg.csv"):
"""Aboca el cataleg complet a CSV. Mode 'w': es regenera sencer."""
with open(ruta, "w", encoding="utf-8", newline="") as f:
escriptor = csv.DictWriter(f, fieldnames=COLUMNES_CATALEG)
escriptor.writeheader()
for llibre in cataleg.values():
escriptor.writerow({"titol": llibre.titol,
"preu": llibre.preu,
"estoc": llibre.estoc})
def carregar_cataleg(ruta="cataleg.csv"):
"""Reconstrueix el dict[str, Llibre] des de CSV. str → float/int: el peatge de tornada."""
if not Path(ruta).exists(): # pedac de 06-01; el bo arriba al modul 7
return {}
cataleg = {}
with open(ruta, "r", encoding="utf-8", newline="") as f:
for fila in csv.DictReader(f):
llibre = Llibre(fila["titol"], float(fila["preu"]), int(fila["estoc"]))
cataleg[normalitzar_titol(llibre.titol)] = llibre
return catalegI la prova de foc, amb les xifres canòniques de sempre:
llibres = [Llibre("L'Odissea", 12.50, 4), Llibre("Hamlet", 9.95, 6),
Llibre("El Quixot", 15.90, 8), Llibre("Faust", 21.00, 0)]
cataleg = {normalitzar_titol(l.titol): l for l in llibres}
desar_cataleg(cataleg) # ... la botiga tanca, el programa acaba ...
recarregat = carregar_cataleg() # ... l'endema al mati ...
print(recarregat["hamlet"].preu_final(soci=True)) # 9.83
print(recarregat["faust"].hi_ha_estoc()) # False — Faust segueix esgotat
print(recarregat == cataleg) # True — l'__eq__ de la dataclass ho certificaAquest True final és el moment clau del mòdul: el catàleg que reneix del disc és indistingible del que va morir ahir a la nit, camp a camp, gràcies a l'__eq__ generat per @dataclass (05-06). A més, en reconstruir cada Llibre, __post_init__ torna a validar: si algú va editar el CSV a Excel i hi va posar un preu negatiu, el programa ho detecta en la càrrega, no tres vendes després.
El registre de vendes: LiniaVenda compleix el seu destí
La solució 2 de 05-06 acabava amb una profecia: «al mòdul 6 aquestes línies de venda seran exactament el que escriurem en un CSV». Complim-la. LiniaVenda és la dataclass congelada i ordenable d'aleshores, i el registre de vendes — a diferència del catàleg — és un històric: creix amb cada venda i no es reescriu mai, així que toca mode "a" (06-01):
import csv
from pathlib import Path
from dataclasses import dataclass
VENDES = "vendes.csv"
COLUMNES_VENDES = ["data", "titol", "quantia"]
@dataclass(frozen=True, order=True)
class LiniaVenda: # exactament la de 05-06
data: str
titol: str
quantia: float
def registrar_venda(linia, ruta=VENDES):
"""Afegeix una venda a l'historic. Capcalera nomes si el fitxer encara no existeix."""
nou = not Path(ruta).exists()
with open(ruta, "a", encoding="utf-8", newline="") as f:
escriptor = csv.DictWriter(f, fieldnames=COLUMNES_VENDES)
if nou:
escriptor.writeheader() # la capcalera s'escriu una sola vegada en la vida del fitxer
escriptor.writerow({"data": linia.data, "titol": linia.titol, "quantia": linia.quantia})
registrar_venda(LiniaVenda("2026-07-13", "Hamlet", 9.83)) # Lluis, amb carnet
registrar_venda(LiniaVenda("2026-07-13", "L'Odissea", 12.35)) # Marta, amb carnet
registrar_venda(LiniaVenda("2026-07-13", "El Quixot", 16.54)) # Julia, sense carnetContingut de vendes.csv després de la jornada:
I l'informe de caixa es redueix a llegir, reconstruir i agregar — sum amb una expressió generadora (02-04) sobre les línies del dia:
def total_del_dia(data, ruta=VENDES):
if not Path(ruta).exists():
return 0.0
with open(ruta, "r", encoding="utf-8", newline="") as f:
linies = [LiniaVenda(fila["data"], fila["titol"], float(fila["quantia"]))
for fila in csv.DictReader(f)]
return round(sum(l.quantia for l in linies if l.data == data), 2)
print(total_del_dia("2026-07-13")) # 38.72Delimitadors, cometes i casos reals
No tots els "CSV" fan servir comes. En països on el decimal és la coma (12,50), Excel exporta amb punt i coma; altres sistemes fan servir tabuladors (format TSV). El mòdul csv ho parametritza amb delimiter:
with open("novetats_distribuidora.csv", "r", encoding="utf-8", newline="") as f:
lector = csv.DictReader(f, delimiter=";") # el CSV "a l'europea" de la distribuidoraPel que fa a les cometes: quan un camp conté el delimitador, salts de línia o cometes, l'estàndard l'embolcalla amb cometes dobles, i les cometes internes es dupliquen (""). La bona notícia és que no ho has de gestionar tu: writer posa cometes quan cal i reader les treu en llegir. Comprova-ho amb els dos llibres problemàtics de Papyrus:
with open("dificils.csv", "w", encoding="utf-8", newline="") as f:
escriptor = csv.writer(f)
escriptor.writerow(["titol", "preu", "estoc"])
escriptor.writerow(["Anna, la de les Teulades Verdes", 11.75, 3]) # coma a dins
escriptor.writerow(['Historia del "Quixot" apocrif', 18.20, 2]) # cometes a dinsEl fitxer resultant (obre'l al teu editor, com a 06-01):
titol,preu,estoc "Anna, la de les Teulades Verdes",11.75,3 "Historia del ""Quixot"" apocrif",18.20,2
I en rellegir-lo amb csv.reader, cada títol torna exacte, amb la seva coma i les seves cometes — el fangar que split(",") no sabia travessar, resolt de sèrie. Si algun sistema exigeix posar-ho tot entre cometes, existeix quoting=csv.QUOTE_ALL com a argument de l'escriptor; per a aquest curs, el comportament per defecte (QUOTE_MINIMAL) és el correcte.
CSV a gran escala: una menció a pandas
El mòdul csv llegeix fila a fila i és perfecte per als volums de Papyrus. Quan al mòdul 11 analitzem dades de debò — milers o milions de files, filtres, agrupacions, estadístiques — farem servir pandas, la funció read_csv() del qual carrega un CSV complet en una estructura tabular (el DataFrame) amb conversió de tipus automàtica inclosa. És l'eina d'anàlisi; csv és la de lampisteria lleugera. Cada cosa al seu temps: aquí n'hi ha prou que sàpigues que existeix i que l'aprendràs a 11-03.
Errors Comuns i Consells
- Oblidar
newline=""en obrir: a Windows apareixen línies en blanc entre files (en escriure) o files fantasma (en llegir). Regla mecànica:open()per acsvsempre portaencoding="utf-8"inewline="". - Parsejar CSV amb
split(","): funciona fins al primer camp amb coma o cometes. El mòdulcsvexisteix exactament per a això; fes-lo servir sempre, també per llegir. - Oblidar les conversions en llegir:
fila["preu"]ésstr. Sumar"9.83" + "12.35"concatena ("9.8312.35") en lloc de sumar — un bug especialment traïdor perquè no llança cap error. Converteix en reconstruir l'objecte, com facarregar_cataleg(). - Escriure la capçalera a cada append: si cada
registrar_venda()cridéswriteheader(), l'històric s'ompliria de capçaleres intercalades. Capçalera només en crear el fitxer (el truc dePath.exists()). DictWriteramb claus que no són afieldnames:writerow({"titol": ..., "autor": ...})llançaValueErrorsiautorno es va declarar. Les columnes es pacten una vegada, a la constant (COLUMNES_CATALEG), i tots els que escriuen la respecten.- Obrir el CSV a Excel mentre el teu programa l'escriu: Excel bloqueja el fitxer a Windows i el teu
open()fallarà ambPermissionError. Tanca el full abans d'executar. - Consell: defineix les columnes com a constant compartida (
COLUMNES_VENDES) i fes-la servir tant afieldnamescom en construir els diccionaris. Un sol lloc a canviar si el format evoluciona.
Exercicis
Exercici 1: reposició de la distribuïdora
La distribuïdora envia reposicio.csv amb capçalera titol;unitats i delimitador ; (crea'l tu a mà amb aquestes files: Faust;5 i Hamlet;2). Escriu aplicar_reposicio(cataleg, ruta) que el llegeixi amb DictReader, sumi les unitats a l'estoc del llibre corresponent fent servir normalitzar_titol() per casar títols, i imprimeixi un avís per cada títol que no sigui al catàleg. Comprova que Faust passa de 0 a 5 exemplars — i que hi_ha_estoc() per fi diu True.
Exercici 2: informe de caixa com a CSV
Escriu informe_caixa(data, ruta_vendes, ruta_informe) que llegeixi vendes.csv, es quedi amb les línies de la data donada i escrigui un CSV nou amb columnes titol,unitats,quantia_total — una fila per títol, agregada amb un Counter o un defaultdict(float) (04-06). Prova-ho amb les tres vendes canòniques del 2026-07-13 més una segona venda de Hamlet a 10.35 (l'Omar, sense carnet): Hamlet ha de sortir amb 2 unitats i 20.18 de quantia total.
Exercici 3: el catàleg indestructible
Amplia el round-trip: afegeix al catàleg el llibre Anna, la de les Teulades Verdes (11.75, 3 exemplars) i Historia del "Quixot" apocrif (18.20, 2), executa desar_cataleg() + carregar_cataleg() i comprova amb == que el catàleg recarregat és idèntic a l'original. Obre cataleg.csv al teu editor i localitza com va quedar cada cas amb les seves cometes.
Solucions
Solució 1:
import csv
def aplicar_reposicio(cataleg, ruta="reposicio.csv"):
with open(ruta, "r", encoding="utf-8", newline="") as f:
for fila in csv.DictReader(f, delimiter=";"):
clau = normalitzar_titol(fila["titol"])
llibre = cataleg.get(clau) # el cercar_llibre de sempre (05-06)
if llibre is None:
print(f"AVIS: {fila['titol']!r} no es al cataleg")
else:
llibre.estoc += int(fila["unitats"])
aplicar_reposicio(cataleg)
print(cataleg["faust"].estoc) # 5
print(cataleg["faust"].hi_ha_estoc()) # True — la cua de reserves de Faust per fi es mouMateix DictReader, dialecte diferent: delimiter=";" absorbeix la diferència i la resta del codi ni se n'assabenta.
Solució 2:
import csv
from collections import defaultdict
def informe_caixa(data, ruta_vendes="vendes.csv", ruta_informe="informe.csv"):
unitats = defaultdict(int)
quanties = defaultdict(float)
with open(ruta_vendes, "r", encoding="utf-8", newline="") as f:
for fila in csv.DictReader(f):
if fila["data"] == data:
unitats[fila["titol"]] += 1
quanties[fila["titol"]] += float(fila["quantia"])
with open(ruta_informe, "w", encoding="utf-8", newline="") as f:
escriptor = csv.DictWriter(f, fieldnames=["titol", "unitats", "quantia_total"])
escriptor.writeheader()
for titol in unitats:
escriptor.writerow({"titol": titol,
"unitats": unitats[titol],
"quantia_total": round(quanties[titol], 2)})
registrar_venda(LiniaVenda("2026-07-13", "Hamlet", 10.35)) # Omar, sense carnet
informe_caixa("2026-07-13")
# A informe.csv: Hamlet,2,20.18 — L'Odissea,1,12.35 — El Quixot,1,16.54Un CSV que es llegeix i un altre que s'escriu a la mateixa funció: el patró entrada → agregació → sortida que veuràs mil vegades.
Solució 3:
cataleg[normalitzar_titol("Anna, la de les Teulades Verdes")] = Llibre("Anna, la de les Teulades Verdes", 11.75, 3)
cataleg[normalitzar_titol('Historia del "Quixot" apocrif')] = Llibre('Historia del "Quixot" apocrif', 18.20, 2)
desar_cataleg(cataleg)
print(carregar_cataleg() == cataleg) # True — comes i cometes inclosesAl fitxer hi veuràs "Anna, la de les Teulades Verdes" (entre cometes per la coma) i "Historia del ""Quixot"" apocrif" (cometes duplicades). Tu no vas escriure ni una cometa: csv va aplicar l'estàndard i el va desfer en llegir.
Conclusió
Papyrus ja parla l'idioma tabular universal: csv.writer/csv.reader per a les coses bàsiques i DictWriter/DictReader com a opció preferida — columnes per nom, capçalera automàtica —, sempre amb encoding="utf-8" i newline="", amb delimiter per als dialectes europeus i les cometes resoltes de sèrie. El catàleg completa el seu round-trip (desar_cataleg()/carregar_cataleg(), amb el peatge str → float/int pagat en un sol lloc) i les LiniaVenda de 05-06 van complir el seu destí anunciat a vendes.csv, un històric en mode append. Però CSV té un sostre: només sap de taules planes. Com desaries els socis de Papyrus amb la seva llista de compres a dins, o la configuració de la botiga amb valors de tipus diferents? Necessites un format que entengui estructures niades — diccionaris dins de llistes dins de diccionaris — i que a més recordi si un valor era número, text o booleà. Aquest format és JSON, l'idioma natiu de les APIs web i els fitxers de configuració, i és la propera lliçó.
Curs de Programació en Python
Mòdul 1: Introducció a Python
- Introducció a Python
- Configuració de l'Entorn de Desenvolupament
- Sintaxi de Python i Tipus de Dades Bàsics
- Variables i Constants
- Entrada i Sortida Bàsica
- Entorns Virtuals i Gestió de Paquets
Mòdul 2: Estructures de Control
Mòdul 3: Funcions i Mòduls
- Definició de Funcions
- Arguments de Funció
- Funcions Lambda
- Mòduls i Paquets
- Visió General de la Biblioteca Estàndard
Mòdul 4: Estructures de Dades
Mòdul 5: Programació Orientada a Objectes
Mòdul 6: Gestió de Fitxers
- Lectura i Escriptura de Fitxers
- Treball amb Fitxers CSV
- Gestió de Dades JSON
- Operacions amb Fitxers i Directoris
Mòdul 7: Gestió d'Errors i Excepcions
- Introducció a les Excepcions
- Gestió d'Excepcions
- Llançament d'Excepcions
- Excepcions Personalitzades
- Bones Pràctiques i Registre d'Errors amb logging
Mòdul 8: Temes Avançats
- Anotacions de Tipus (type hints)
- Decoradors
- Generadors
- Gestors de Context
- Concurrència: Fils i Processos
- Asyncio per a la Programació Asíncrona
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb unittest
- Proves amb pytest
- Desenvolupament Guiat per Proves
- Tècniques de Depuració
- Ús de pdb per a la Depuració
Mòdul 10: Desenvolupament Web amb Python
- Introducció al Desenvolupament Web
- Fonaments del Framework Flask
- Construcció d'APIs REST amb Flask
- Introducció a Django
- Construcció d'Aplicacions Web amb Django
Mòdul 11: Ciència de Dades amb Python
- Introducció a la Ciència de Dades
- NumPy per a la Computació Numèrica
- Pandas per a la Manipulació de Dades
- Matplotlib per a la Visualització de Dades
- Introducció a l'Aprenentatge Automàtic amb scikit-learn
