JSON と CSV
csv モジュールで引用符付きの表を読み書きし、なぜ `newline=""` が必要か理解し、json で構造化データを保持し、どの型が途中で変わるか知る。
どうやって学びますか?
解決する問題
23 章から .split(",") でファイルを分割してきましたが、うまくいっていました。さて、フィールドの中にカンマがあるファイルです:
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']最後の行が 3 つではなく 4 つのフィールドになり、引用符がデータの一部になってしまいました。
この引用符は間違いではありません — CSV のルールであり、まさにこの状況のために存在します。間違っているのは読み込みコードの方です。そして、このルールを自分で書くことは、引用符の中の引用符やフィールド内の改行を扱うことになり、小さな仕事が着実に大きなものになっていきます。
その仕事はすでに終わっています。
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']この章は 2 つの形式 — 表のための CSV、構造のための JSON — と、それぞれのモジュールが Python に標準で付いています。
この章を終える頃には
csv.readerとcsv.DictReaderで読み、csv.writerで書ける- なぜ
newline=""を書くか言える json.dumps/loadsとjson.dump/loadの違いを言える- JSON からどの Python の型が戻り、どれが戻らないか言える
- CSV と JSON の使い分けができる
JSONDecodeErrorを読んで問題を見つけられる
前提条件: 例外.
CSV を読む
csv.reader は各行をリストとして返します。しかし、位置を覚えていなければなりません — row[2] は何でしたっけ?
csv.DictReader は最初の行を見出しとして扱い、各行を辞書にします:
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'>2 点あります。
名前でアクセスできるので、列の並び替えでコードが壊れません。DictReader はほぼ常に良い選択です。
しかし price はまだ文字列です。 csv モジュールはフィールドを分けるだけで、変換はしません — CSV ファイルのどこにも、どの列が数値かは書かれていません。float() と int() を呼ぶのはあなたです。23 章とまったく同じように。
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.0large, sturdy は自動で引用符で囲まれ、blue ink は必要なかったので囲まれていません。読めなかったものは、書く必要もありませんでした。
辞書のリストには 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 で列の順序も決まり、writeheader() がなければ見出し行はありません。
なぜnewline=""なのか?csvモジュールは行の終わり (\r\n) を自分で決めます。ファイルを普通に開くと、Windows では Python が改行コードを二度変換し、各行の後に空行が残ります。Linux と macOS ではこの二度目の変換は起きないので、違いは見えません。newline=""はその変換を切ります。これでどこでも同じように動きます。読み込みにも書いてください — すると引用符で囲まれたフィールド内の改行が正しく読めます。
JSON — 表では持てない構造
CSV は表です:行と列、すべて平ら。しかし、注文に明細行のリストがあり、それぞれの明細にタグのリストがあるとしたら? 表では持てません。
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 は Python オブジェクトを文字列に変えます。形は Python に似ていますが、2 つ違います:True が true になり、None が null になります。JSON は多くのプログラミング言語で共有される別の言語なので、独自の綴りを持っています。
人が読むものには:
import json
order = {"name": "pen", "tags": ["ink", "blue"]}
print(json.dumps(order, indent=2)){
"name": "pen",
"tags": [
"ink",
"blue"
]
}そして戻すには:
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'> Trueこれが CSV との最大の違いです。 price が数値として戻り、stocked が True として戻り、変換を一切呼んでいません。JSON は型を運びます。CSV は運びません。
4 つの名前は覚えやすいです:dumps/loads は文字列、dump/load はファイルを扱います。 末尾の s は string の s です。
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}JSON が戻さないもの
すべての Python オブジェクトが JSON に入れられるわけではなく、入ったものもすべて同じ形で戻るわけではありません。
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(エラー行の前にも Python はトレースバックの行をいくつか表示しますが、この章では通常、最後の行だけを示しています。)
タプルは黙ってリストになります。 JSON にタプルがないので、書くときに文句は出ません — ですが戻ってくるのはリストです。14 章の「変わらない」という約束は、途中で失われます。
セットはまったく通りません、こちらの方が良い挙動です — 黙って間違えるより、止まる方がましです。必要なら sorted(...) をリストとして送ります。
もう 1 つ、より厄介なのがこれ:
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'}JSON のキーは常に文字列です。 数値キーを渡すと黙って文字列になり、戻ってくるのも文字列です。だから以前は data[1] が効きましたが、今度は data["1"] が必要です。
覚えるリストは短いです:辞書、リスト、文字列、数値、True/False、None は安全。タプルは変わる。セット、日付、自分で作ったオブジェクトは通りません。
JSON は厳格
import json
print(json.loads("{'name': 'pen'}"))json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)Python では一重と二重の引用符は等価です。JSON では違います — 二重引用符のみ、最後の項目のあとにカンマもありません。
メッセージに行と列が出るので、大きなファイルでも場所が特定できます。そして JSONDecodeError は本当は ValueError です — だから except ValueError でも捕まります。前の章の家系ルールどおりです。
どっち、いつ
CSV を選ぶのは、データが平らな表で、人が表計算ソフトで開くとき。行数が多くてもファイルが小さく済みます。
JSON を選ぶのは、構造があるとき — リストの中にリスト、辞書の中に辞書 — または型をそのまま戻したいとき。設定ファイルと Web API はほぼ JSON です。
完全な例
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: ''"
]
}見る価値のある 4 点です。
CSV が入り、JSON が出ます、これはそれぞれの形式の性質から来ます。入力は平らな表で、表計算ソフトでも開けます。出力は平らではありません — もののリストで、それぞれが辞書、それに不満のリストが並びます。これは CSV では書けませんでした。
large, sturdy は旅全体を生き残ります。 csv が正しく読み、json が正しく書きました。この章の冒頭の問題は、どこにもありません。
空の note と空の price はどちらも空の文字列ですが、結末が違います。 空の CSV フィールドは "" であって None ではありません。note は文字列のままなので何もしません。float("") は ValueError を投げるので、clip 行は不満とともにスキップされます。CSV に「欠損」はなく、「空」だけがある — それが何を意味するかはあなたが決めます。
start=2 は気まぐれではありません。 DictReader は最初の行を見出しとして消費するので、最初に生成されるデータ行はファイルの 2 行目です。誰かがファイルと照らし合わせるメッセージは、ファイルと同じ数え方をしなければなりません。
うまくいかないとき
自分の CSV に各行のあとに空行がある (Windows で) newline="" を渡していません。書き込み時に渡してください。
KeyError: 'price' — 列はそこにあるのに 見出しが一致しません。綴りか空白のどちらかです。reader.fieldnames を表示して、Python が実際に何を読んだか確認しましょう。
数値で算術をしようとして TypeError csv はすべてを文字列として返します。float() か int() を呼ばなければなりません。
最初の行がデータとして扱われる csv.reader を使っていますが、これは見出しを知りません。DictReader を使うか、最初の行を自分で取り除きましょう。
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes 一重引用符、または末尾のカンマ。JSON は Python ではありません。
TypeError: Object of type set is not JSON serializable セット、日付、自分で作ったオブジェクトを渡しました。先にリストや文字列に変換してください。
タプルがリストとして戻ってきた そうなるべきです — JSON にタプルはありません。あとで必要なら自分で tuple(...) を呼んでください。
data[1] が効いていたのに、往復後に KeyError になる JSON のキーは常に文字列です。今は data["1"] です。
ステップ 4 / 6 — 予想する
理解度をチェック
3 列のファイルを手で分割。いくつのフィールドが出ますか?
# 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
- D`ValueError`
DictReader が行を読みました。2 行目では何が起きますか?
# 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)- A辞書が表示され、それから `16`
- B辞書が表示され、それから `151`
- C辞書が表示され、それから `TypeError` — `price` はまだ文字列
- D`KeyError`
json.dumps に Python の辞書が渡されます。何が表示されますか?
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}
回答にはアカウントが必要です
ログインして答えを確認
問題は上にあります。頭の中で答えを考えることがいちばん大切です。ログインすると、正解・解説・3 段階のヒントを確認できます。
やってみよう
books.csv を作り、列を title,author,pages,year とし、少なくとも 6 行入れてください。タイトルのどこかにカンマを入れてください (引用符で囲まれるはずです)。pages を 1 行空にし、別の行では pages のところに文字を入れてください。
それから library.py を書きます:
DictReaderでファイルを読み、悪い行ごとに行番号つきの不満を集める- 著者ごとのまとめを作る — それぞれ何冊、総ページ数いくつ
indent=2でlibrary.jsonに全部書き出す- そのファイルを読み戻し、合計が変わっていないことを確認する
それから 5 つの実験:
DictReaderの代わりにline.split(",")を使う。カンマを含むタイトルはどうなるか?- 書き込み時に
newline=""を外し、CSV ファイルを開いて見る。Windows では各行のあとに空行が見える。Linux と macOS では違いは見えない — だがそこでもnewline=""を書き、コードをどこでも同じように動かす。 - まとめにセットを入れる (著者のセットなど) して JSON を書こうとする。メッセージを読み、それから直す。
- 手で
library.jsonを開き、二重引用符を 1 つ一重引用符に変え、読もうとする。メッセージに行と列が出るか? - 年を辞書のキーにする —
{2020: [...]}— して書き、読み戻す。今度は何で引けばいいか?
最後の 2 つがこの章のポイントです:ファイルに書くことは翻訳であり、すべての翻訳は何かを変える。 何が変わるか分かっていれば形式を選べる。分からなければ、形式があなたを選ぶことになります。