一覧PG-11-2 · ファイルと外部データ

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

1
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
2
print(text.splitlines()[1].split(","))
['あや', '"blue', ' pen"', '3']
3
import csv
4
with 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
5
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}]
6
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 int
7
with open("roundtrip.csv", "w", encoding="utf-8", newline="") as stream: writer = csv.DictWriter(stream, fieldnames=["name", "note", "qty"]) written = writer.writeheader() writer.writerows(restored)
8
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
9
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

演習完了