JSON e CSV
Lettura e scrittura di tabelle quotate con il modulo csv, perché serve `newline=""`, dati strutturati con json, e quali tipi cambiano in transito.
Come vuoi imparare?
Il problema che stiamo risolvendo
Dal capitolo 23 abbiamo diviso i file con .split(","), e ha funzionato. Ora un file dove un campo contiene una virgola dentro:
name,note,price
pen,blue ink,15.0
bag,"large, sturdy",850.0with open("items.csv", "r", encoding="utf-8") as fh:
for line in fh:
print(line.strip().split(","))['name', 'note', 'price']
['pen', 'blue ink', '15.0']
['bag', '"large', ' sturdy"', '850.0']L'ultima riga è uscita con quattro campi invece di tre, e le virgolette sono diventate parte dei dati.
Le virgolette non sono un errore — sono la regola del CSV, ed esistono proprio per questa situazione. L'errore è nel nostro codice di lettura. E scrivere quella regola da soli significa gestire virgolette dentro virgolette e a capo dentro i campi; un piccolo lavoro che diventa costantemente più grande.
Il lavoro è già stato fatto.
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
for row in csv.reader(fh):
print(row)['name', 'note', 'price']
['pen', 'blue ink', '15.0']
['bag', 'large, sturdy', '850.0']Questo capitolo copre due formati — CSV per le tabelle, JSON per la struttura — e un modulo per ognuno è incluso in Python.
Alla fine di questo capitolo sarai in grado di
- Leggere con
csv.readerecsv.DictReader, e scrivere concsv.writer - Spiegare perché si scrive
newline="" - Dire cosa separa
json.dumps/loadsdajson.dump/load - Dire quali tipi Python tornano dal JSON e quali no
- Scegliere tra CSV e JSON
- Leggere un
JSONDecodeErrore trovare il problema
Prerequisiti: Eccezioni.
Lettura di CSV
csv.reader dà ogni riga come lista. Ma poi le posizioni vanno ricordate — cos'era row[2]?
csv.DictReader tratta la prima riga come intestazioni e fa di ogni riga un dizionario:
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
for row in csv.DictReader(fh):
print(row["name"], "-", row["price"], type(row["price"]))pen - 15.0 <class 'str'>
bag - 850.0 <class 'str'>Due cose.
L'accesso è per nome, quindi riordinare le colonne non rompe il codice. DictReader è quasi sempre la scelta migliore.
Ma price è ancora testo. Il modulo csv separa i campi, non li converte — nulla in un file CSV dice quale colonna è un numero. float() e int() le chiami tu, esattamente come nel capitolo 23.
Scrittura di CSV
import csv
rows = [["pen", "blue ink", 15.0], ["bag", "large, sturdy", 850.0]]
with open("out.csv", "w", encoding="utf-8", newline="") as fh:
writer = csv.writer(fh)
writer.writerow(["name", "note", "price"])
writer.writerows(rows)name,note,price
pen,blue ink,15.0
bag,"large, sturdy",850.0Nota che large, sturdy è stato quotato da solo — e blue ink no, perché non serviva. Ciò che non abbiamo saputo leggere, non abbiamo dovuto neanche scrivere.
Per una lista di dizionari, DictWriter:
import csv
rows = [
{"name": "pen", "price": 15.0},
{"name": "bag", "price": 850.0},
]
with open("out.csv", "w", encoding="utf-8", newline="") as fh:
writer = csv.DictWriter(fh, fieldnames=["name", "price"])
writer.writeheader()
writer.writerows(rows)name,price
pen,15.0
bag,850.0fieldnames fissa anche l'ordine delle colonne, e senza writeheader() non c'è la riga di intestazione.
Perchénewline=""? Il modulocsvdecide da solo cosa chiude una riga (\r\n). Se il file viene aperto normalmente, su Windows Python traduce i finali di riga una seconda volta, lasciando una riga vuota dopo ogni riga. Su Linux e macOS questa seconda traduzione non avviene, quindi lì non vedi differenza.newline=""disattiva quella traduzione, così il codice si comporta uguale ovunque. Scrivilo anche per la lettura — così gli a capo dentro campi quotati vengono letti correttamente.
JSON — struttura che una tabella non può contenere
Il CSV è una tabella: righe e colonne, tutto piatto. Ma se un ordine contiene una lista di righe, e ogni riga contiene una lista di tag? Una tabella non può contenerlo.
import json
order = {"name": "pen", "price": 15.0, "tags": ["ink", "blue"], "stocked": True, "note": None}
print(json.dumps(order)){"name": "pen", "price": 15.0, "tags": ["ink", "blue"], "stocked": true, "note": null}json.dumps trasforma un oggetto Python in testo. La forma assomiglia a quella di Python, con due differenze: True è diventato true e None è diventato null. Il JSON è un linguaggio separato condiviso tra molti linguaggi di programmazione, quindi ha la sua ortografia.
Per qualcosa che una persona leggerà:
import json
order = {"name": "pen", "tags": ["ink", "blue"]}
print(json.dumps(order, indent=2)){
"name": "pen",
"tags": [
"ink",
"blue"
]
}E riportarlo indietro:
import json
text = '{"name": "pen", "price": 15.0, "stocked": true, "note": null}'
order = json.loads(text)
print(order)
print(type(order["price"]), type(order["stocked"]), order["note"] is None){'name': 'pen', 'price': 15.0, 'stocked': True, 'note': None}
<class 'float'> <class 'bool'> TrueQuesta è la differenza più grande rispetto al CSV. price è tornato come numero e stocked è tornato come True, senza chiamare alcuna conversione. Il JSON porta i tipi; il CSV no.
I quattro nomi sono facili da tenere a mente: dumps/loads lavorano con testo, dump/load lavorano con file. La s finale sta per string.
import json
from pathlib import Path
order = {"name": "pen", "price": 15.0}
Path("order.json").write_text(json.dumps(order, indent=2) + "\n", encoding="utf-8")
with open("order.json", "r", encoding="utf-8") as fh:
back = json.load(fh)
print(back){'name': 'pen', 'price': 15.0}Cosa il JSON non restituisce
Non ogni oggetto Python può andare nel JSON, e non tutto ciò che entra torna uguale.
import json
data = {"pair": (1, 2), "numbers": {3, 4}}
print(json.dumps({"pair": data["pair"]}))
print(json.dumps(data)){"pair": [1, 2]}
TypeError: Object of type set is not JSON serializable(Prima della riga dell'errore Python stampa anche alcune righe di traceback; in questo capitolo di solito viene mostrata solo l'ultima.)
Una tupla diventa silenziosamente una lista. Il JSON non ha tuple, quindi la scrittura non solleva errori — ma ciò che torna è una lista. La promessa del capitolo 14 che non cambierebbe è persa per strada.
Un insieme non ci entra per niente, ed è il comportamento migliore — fermarsi batte essere silenziosamente sbagliati. Manda sorted(...) come lista se ti serve.
Un altro, che morde più forte:
import json
data = {1: "pen", 2: "bag"}
text = json.dumps(data)
print(text)
print(json.loads(text)){"1": "pen", "2": "bag"}
{'1': 'pen', '2': 'bag'}Le chiavi del JSON sono sempre stringhe. Dagli chiavi numeriche e diventano silenziosamente stringhe, e tornano come stringhe. Quindi data[1] funzionava prima, e data["1"] serve dopo.
La lista da ricordare è corta: dizionari, liste, stringhe, numeri, True/False e None sono sicuri. Le tuple cambiano. Insiemi, date e oggetti tuoi non ci entrano.
Il JSON è rigoroso
import json
print(json.loads("{'name': 'pen'}"))json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)In Python apici singoli e doppi sono uguali. Nel JSON non lo sono — solo virgolette doppie, e niente virgola dopo l'ultimo elemento.
Il messaggio dà una riga e una colonna, così il punto si trova anche in un file grande. E JSONDecodeError è davvero un ValueError — quindi except ValueError lo cattura anche, come dice la regola delle famiglie del capitolo precedente.
Quale, quando
Prendi il CSV quando i dati sono una tabella piatta e una persona lo aprirà in un foglio di calcolo. Con molte righe il file resta più piccolo.
Prendi il JSON quando c'è struttura — liste dentro, dizionari dentro — o quando i tipi devono tornare intatti. File di configurazione e API web sono quasi sempre JSON.
Un esempio completo
items.csv:
name,note,price,quantity
pen,blue ink,15.0,3
bag,"large, sturdy",850.0,1
ink,,120.0,2
clip,broken,,4main.py:
"""Read a CSV of items, total it, and write the result as JSON."""
import csv
import json
from pathlib import Path
TAX_RATE = 0.15
def read_items(path):
"""Returns the usable rows and the complaints. csv handles the quoting."""
items = []
problems = []
with open(path, "r", encoding="utf-8", newline="") as fh:
for number, row in enumerate(csv.DictReader(fh), start=2):
try:
items.append({
"name": row["name"],
"note": row["note"],
"amount": round(
float(row["price"]) * int(row["quantity"]) * (1 + TAX_RATE), 2),
})
except ValueError as err:
problems.append(f"line {number}: {err}")
return items, problems
def main():
items, problems = read_items(Path("items.csv"))
summary = {
"items": items,
"total": round(sum(item["amount"] for item in items), 2),
"skipped": problems,
}
Path("summary.json").write_text(
json.dumps(summary, indent=2) + "\n", encoding="utf-8")
print(Path("summary.json").read_text(encoding="utf-8"), end="")
if __name__ == "__main__":
main(){
"items": [
{
"name": "pen",
"note": "blue ink",
"amount": 51.75
},
{
"name": "bag",
"note": "large, sturdy",
"amount": 977.5
},
{
"name": "ink",
"note": "",
"amount": 276.0
}
],
"total": 1305.25,
"skipped": [
"line 5: could not convert string to float: ''"
]
}Quattro cose che val la pena guardare.
Il CSV entra e il JSON esce, e questo deriva da ciò che è ogni formato. L'input è una tabella piatta, che un foglio di calcolo può anche aprire. L'output non è piatto — una lista di cose, ognuna un dizionario, con una seconda lista di reclami accanto. Questo non si poteva scrivere come CSV.
large, sturdy sopravvive a tutto il viaggio. csv l'ha letto correttamente e json l'ha scritto correttamente. Il problema che apre il capitolo non si vede da nessuna parte.
Il note vuoto e il price vuoto sono entrambi testo vuoto, e finiscono diversamente. Un campo CSV vuoto è "", non None. note resta come testo e non causa nulla; float("") solleva un ValueError, quindi la riga clip viene saltata con un reclamo. Il CSV non ha "mancante", solo "vuoto" — e cosa significa lo decidi tu.
Lo start=2 non è arbitrario. DictReader consuma la prima riga come intestazioni, quindi la prima riga di dati che restituisce è la seconda linea del file. Un messaggio che qualcuno controllerà contro il file deve contare come conta il file.
Quando si rompe
Una riga vuota dopo ogni riga del mio CSV (su Windows) Manca newline="". Mettilo quando scrivi.
KeyError: 'price' — ma la colonna è proprio lì L'intestazione non corrisponde, nell'ortografia o nello spazio bianco. Stampa reader.fieldnames per vedere cosa Python ha letto davvero.
Un TypeError facendo aritmetica con i numeri csv dà tutto come testo. float() o int() va chiamato.
La prima riga viene trattata come dati Stai usando csv.reader, che non sa nulla di intestazioni. Usa DictReader, o togli tu la prima riga.
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes Apici singoli, o una virgola finale. Il JSON non è Python.
TypeError: Object of type set is not JSON serializable È stato passato un insieme, una data o un tuo oggetto. Convertilo in lista o testo prima.
Le mie tuple sono tornate come liste Come deve essere — il JSON non ha tuple. Chiama tuple(...) tu dopo se ti serve una.
data[1] funzionava, e dopo un giro solleva KeyError Le chiavi del JSON sono sempre stringhe. Ora è data["1"].
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
Un file a tre colonne, diviso a mano. Quanti campi escono?
# items.csv
# name,note,price
# pen,blue ink,15.0
# bag,"large, sturdy",850.0
with open("items.csv", "r", encoding="utf-8") as fh:
for line in fh:
print(len(line.strip().split(",")))- A3 3 4
- B3 3 3
- C3 3 5
- DUn `ValueError`
DictReader ha letto la riga. Cosa succede alla seconda riga?
# items.csv
# name,price
# pen,15
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
row = next(csv.DictReader(fh))
print(row)
print(row["price"] + 1)- AIl dizionario viene stampato, poi `16`
- BIl dizionario viene stampato, poi `151`
- CIl dizionario viene stampato, poi un `TypeError` — `price` è ancora testo
- DUn `KeyError`
json.dumps riceve un dizionario Python. Cosa viene stampato?
import json
print(json.dumps({"name": "pen", "stocked": True, "note": None}))- A{'name': 'pen', 'stocked': True, 'note': None}
- B{"name": "pen", "stocked": True, "note": None}
- C{"name": "pen", "stocked": true}
- D{"name": "pen", "stocked": true, "note": null}
Per rispondere serve un account
Accedi per verificare le tue risposte
Le domande sono qui sopra, e ragionarci a mente è la parte che conta. Accedi per vedere le risposte, le spiegazioni e i suggerimenti a tre livelli.
Tocca a te
Crea un books.csv con le colonne title,author,pages,year e almeno sei righe. Metti una virgola in almeno un titolo (così deve essere quotato), lascia pages vuoto in una riga, e metti lettere dove sta pages in un'altra.
Poi scrivi library.py che:
- Legge il file con
DictReader, raccogliendo un reclamo con numero di riga per ogni riga sbagliata - Costruisce un riepilogo per autore — quanti libri ciascuno, e quante pagine in totale
- Scrive tutto in
library.jsonconindent=2 - Rilegge quel file e conferma che il totale è invariato
Poi cinque esperimenti:
- Usa
line.split(",")invece diDictReader. Cosa succede al titolo con la virgola? - Rimuovi
newline=""quando scrivi, poi apri il file CSV e guarda. Su Windows vedrai una riga vuota dopo ogni riga; su Linux e macOS non vedrai differenza — ma tieninewline=""anche lì, così il codice si comporta uguale ovunque. - Metti un insieme nel riepilogo (l'insieme degli autori, per dire) e prova a scrivere il JSON. Leggi il messaggio, poi sistemalo.
- Apri
library.jsona mano, cambia una virgoletta doppia in un apice singolo, e prova a leggerlo. Il messaggio dà una riga e una colonna? - Usa l'anno come chiave di dizionario —
{2020: [...]}— poi scrivilo e rileggilo. Con cosa devi cercarlo ora?
Quegli ultimi due fanno il punto di questo capitolo: scrivere su un file è tradurre, e ogni traduzione cambia qualcosa. Sapere cosa cambia ti fa scegliere il formato; non saperlo significa che il formato sceglie per te.
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Accedi per fare il quiz