CSVとJSONの読み書き
日本語、項目内カンマ、項目内改行を含むCSVを読みます。個数を数値へ変換してJSONへ保存し、CSVへ戻したときの値と型まで照合します。
説明
CSVでは項目内のカンマや改行を引用符で囲む場合があり、一行を単純にsplitするだけでは正しく解析できません。JSONへ変換するときも、文字列と数値などの型を意図に合わせて決めます。
日本語、項目内カンマ、項目内改行を含むCSVを読みます。個数を数値へ変換してJSONへ保存し、CSVへ戻したときの値と型まで照合します。
1物理的な行とCSVのレコード
input.csvには見出しと二人分のデータがあります。一人目の備考はカンマ、二人目の備考は改行を含みます。
from pathlib import Path; text = Path("input.csv").read_bytes().decode("utf-8")保存バイトをUTF-8として取得; print(repr(text))引用符と改行を表示; print(len(text.splitlines()))物理的な行数splitlinesでは4行になりますが、論理的なCSVレコードは見出しを含め3件です。引用符内の改行を、別レコードの始まりとして扱ってはいけません。
2単純なカンマ分割が崩れる箇所
一人目の行をカンマでsplitしてみます。
print(text.splitlines()[1].split(",")引用符を解釈しない分割)本来は名前・備考・個数の3項目ですが、備考のblue, penまで分かれて4項目になります。引用符の内外を解釈するCSVパーサーを使います。
3CSVの規則で解析する
newline=""を指定して開き、csv.readerに改行の処理を任せます。
import csvCSVの解析機能csvを読み込みました。次のwith内でレコードを全件読み取り、ブロックを抜けるとファイルを閉じます。
4引用されたカンマと改行を保持する
readerから取得した各レコードと件数を表示します。
with open("input.csv", encoding="utf-8", newline="")文字コードと改行処理 as stream:
records = list(csv.reader(stream))CSVレコードへ解析
print(records, len(records))値と論理レコード数三件として読め、blue, penとline1\nline2がそれぞれ一項目に戻りました。通常のcsv.readerは個数3と0も文字列として返します。
5数値の列だけを変換する
見出しを除いた二件から辞書を作り、qtyだけintへ変換します。
rows = [{"name":name, "note":note, "qty":int(qty)}列の役割に応じて型を決める for name,note,qty in records[1:]見出しを除く]; print(rows)変換後の値名前と備考はstr、qtyは整数3と0です。CSVは数値型の列定義を自動で保証しないため、この変換と、用途に必要な値の検査を読み取り側で行います。
6JSONへ保存して読み返す
日本語を直接表記するensure_ascii=Falseを使い、JSONをUTF-8で保存します。
import json; document = json.dumps(rows, ensure_ascii=False, indent=2)JSON文字列へ変換; written = Path("output.json").write_text(document, encoding="utf-8")UTF-8で保存; restored = json.loads(Path("output.json").read_text(encoding="utf-8"))保存内容を再解析; print(document)保存する表記; print(restored == rows, type(restored[0]["qty"]).__name__)値と型の往復JSON内の備考の改行は\nとしてエスケープされ、読み戻すと改行文字へ戻ります。今回の値は一致し、qtyもintです。JSONの文字列としての見た目と、解析後の値を分けて確認します。
7同じ列順でCSVへ戻す
DictWriterへ列順を指定し、見出しと二件を書き出します。
with open("roundtrip.csv", "w", encoding="utf-8", newline="")CSV出力を開く as stream:
writer = csv.DictWriter(stream, fieldnames=["name", "note", "qty"]列順を明示)
written = writer.writeheader()見出しを書き出す
writer.writerows(restored)二件をCSV化writerは必要な項目を引用し、項目内カンマと改行を保持します。JSONでは整数だったqtyも、CSV上では文字の表記として保存されます。
8CSVの値を元の解析結果と照合する
再出力したCSVを同じ条件で読み、最初のreaderの結果と比較します。
with open("roundtrip.csv", encoding="utf-8", newline="") as stream:
reread = list(csv.reader(stream)再出力したCSVを解析)
print(reread == records, type(reread[1][2]).__name__)値の一致と列の型値は一致し、CSVから読んだqtyは再びstrです。CSVとJSONでは保持する型の仕組みが違うため、行数やファイルサイズだけで変換の成功を判断しません。
9JSONで表せない数値を明示的に拒否する
PythonのfloatにはNaNがありますが、標準のJSON数値としては扱えません。allow_nan=Falseで検出します。
try:
json.dumps({"qty": float("nan")}JSON数値にできない値, allow_nan=False非標準値を拒否)
except ValueError as error:保存時の失敗を捕捉
print(f"{type(error).__name__}: {error}")拒否の理由ValueErrorになります。Pythonの既定動作で非標準のNaN表記を出すことと、受け取る側のJSON実装が解析できることは別なので、交換形式の条件を明示します。
まとめ
引用符内のカンマや改行はCSVパーサーへ任せます。
型を決めて変換し、再出力後は解析した値で照合します。
操作履歴と解説を振り返れます。
Python 3
from pathlib import Path; text = Path("input.csv").read_bytes().decode("utf-8"); print(repr(text)); print(len(text.splitlines()))'name,note,qty\r\nあや,"blue, pen",3\r\n健,"line1\nline2",0\r\n' 4
print(text.splitlines()[1].split(","))['あや', '"blue', ' pen"', '3']
import csvwith open("input.csv", encoding="utf-8", newline="") as stream:
records = list(csv.reader(stream))
print(records, len(records))[['name', 'note', 'qty'], ['あや', 'blue, pen', '3'], ['健', 'line1\nline2', '0']] 3
rows = [{"name":name, "note":note, "qty":int(qty)} for name,note,qty in records[1:]]; print(rows)[{'name': 'あや', 'note': 'blue, pen', 'qty': 3}, {'name': '健', 'note': 'line1\nline2', 'qty': 0}]import json; document = json.dumps(rows, ensure_ascii=False, indent=2); written = Path("output.json").write_text(document, encoding="utf-8"); restored = json.loads(Path("output.json").read_text(encoding="utf-8")); print(document); print(restored == rows, type(restored[0]["qty"]).__name__)[
{
"name": "あや",
"note": "blue, pen",
"qty": 3
},
{
"name": "健",
"note": "line1\nline2",
"qty": 0
}
]
True intwith open("roundtrip.csv", "w", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=["name", "note", "qty"])
written = writer.writeheader()
writer.writerows(restored)with open("roundtrip.csv", encoding="utf-8", newline="") as stream:
reread = list(csv.reader(stream))
print(reread == records, type(reread[1][2]).__name__)True str
try:
json.dumps({"qty": float("nan")}, allow_nan=False)
except ValueError as error:
print(f"{type(error).__name__}: {error}")ValueError: Out of range float values are not JSON compliant: nan
演習完了