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

  1. Què és CSV i per què és la lingua franca de les dades tabulars
  2. Per què no n'hi ha prou amb split(",")
  3. Escriure amb csv.writer (i el misteri de newline="")
  4. Llegir amb csv.reader
  5. DictReader i DictWriter: la manera preferida
  6. Round-trip complet: el catàleg de Papyrus a cataleg.csv i de tornada
  7. El registre de vendes: LiniaVenda compleix el seu destí
  8. Delimitadors, cometes i casos reals
  9. CSV a gran escala: una menció a pandas
  10. Errors comuns i consells
  11. 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:

titol,preu,estoc
L'Odissea,12.50,4
Hamlet,9.95,6
El Quixot,15.90,8
Faust,21.00,0

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:

titol,preu,estoc
"Anna, la de les Teulades Verdes",11.75,3

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 llibre

Pas a pas:

  • csv.writer(f) embolcalla el fitxer obert (el with i 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é existeix writerows(llista_de_files) per abocar-ne moltes de cop.
  • newline="" és obligatori en obrir un fitxer per al mòdul csv, tant en escriure com en llegir. Per què? El mòdul csv gestiona 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'ocupa csv".

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 cataleg

I 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 certifica

Aquest 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 carnet

Contingut de vendes.csv després de la jornada:

data,titol,quantia
2026-07-13,Hamlet,9.83
2026-07-13,L'Odissea,12.35
2026-07-13,El Quixot,16.54

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

Delimitadors, 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 distribuidora

Pel 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 dins

El 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 a csv sempre porta encoding="utf-8" i newline="".
  • Parsejar CSV amb split(","): funciona fins al primer camp amb coma o cometes. El mòdul csv existeix exactament per a això; fes-lo servir sempre, també per llegir.
  • Oblidar les conversions en llegir: fila["preu"] és str. 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 fa carregar_cataleg().
  • Escriure la capçalera a cada append: si cada registrar_venda() cridés writeheader(), l'històric s'ompliria de capçaleres intercalades. Capçalera només en crear el fitxer (el truc de Path.exists()).
  • DictWriter amb claus que no són a fieldnames: writerow({"titol": ..., "autor": ...}) llança ValueError si autor no 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à amb PermissionError. Tanca el full abans d'executar.
  • Consell: defineix les columnes com a constant compartida (COLUMNES_VENDES) i fes-la servir tant a fieldnames com 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 mou

Mateix 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.54

Un 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 incloses

Al 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

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats