PythonでCSVとJSONを扱う|読み込み・検証・型変換・出力の基本

「PythonでCSVとJSONを扱う|読み込み・検証・型変換・出力の基本」の内容を表す技術イラスト

設計データ、部品表、計測値、設定ファイルをPythonで処理するとき、よく使われる形式がCSVとJSONです。

CSVは行と列からなる表形式に向き、JSONは辞書やリストによる階層構造を表せます。ただし、ファイルを読み込めただけでは安全に計算できません。

実務では、次の流れを分けて考えます。

読み込み
↓
構造の確認
↓
欠損・型・範囲の検証
↓
用途に合うデータへ変換
↓
計算
↓
CSVまたはJSONへ出力

この記事ではPython標準ライブラリだけを使い、CSVの測定・設計データを検証し、計算結果をJSONへ出力します。

目次

今日の到達点

  • CSVのrowとfieldを説明できる
  • csv.readerとcsv.DictReaderを使い分けられる
  • CSVから読み込んだ値を検証して数値へ変換できる
  • JSON文字列とPythonオブジェクトを区別できる
  • json.load()・loads()・dump()・dumps()を使い分けられる
  • 欠損値、文字コード、非有限値を考慮できる
  • 読み込み、検証、変換、出力を分離できる

CSVとJSONの役割

CSVは、同じ列構造を持つ複数レコードに向いています。

part_no,pressure_mpa,area_mm2
CYL-001,14.0,1000.0
CYL-002,7.0,500.0

この例では、1行が1件のレコードです。各列はfieldと呼ばれます。

JSONは、keyとvalueの組み合わせや配列、入れ子構造を表せます。

{
  "part_no": "CYL-001",
  "conditions": {
    "pressure_mpa": 14.0,
    "area_mm2": 1000.0
  },
  "enabled": true
}

CSVとJSONの大まかな選択基準です。

データの特徴適した形式
同じ列が並ぶ一覧CSV
表計算ソフトとの受け渡しCSV
部品表・測定値一覧CSV
階層構造や入れ子JSON
設定・APIデータJSON
数値・真偽値・nullを区別JSON

どちらが上位という関係ではありません。入力はCSV、計算結果はJSON、集計結果はCSVというように、役割ごとに使い分けられます。

csvモジュールを使う理由

CSVは単純に見えますが、値の中にカンマ、引用符、改行が含まれる場合があります。

part_no,description
A001,"plate, machined"

これをline.split(",")で分割すると、description内のカンマまで区切りと誤認します。

Pythonのcsv標準ライブラリは、このような引用規則を考慮して読み書きします。CSVを手作業で分割せず、csv.readerやcsv.DictReaderを使います。

csv.readerで行を読む

csv.readerは、各行を文字列のリストとして返します。

import csv

with open(
    "parts.csv",
    mode="r",
    encoding="utf-8",
    newline="",
) as file:
    reader = csv.reader(file)

    for row in reader:
        print(row)

入力が次の場合を考えます。

part_no,pressure_mpa,area_mm2
CYL-001,14.0,1000.0

出力です。

['part_no', 'pressure_mpa', 'area_mm2']
['CYL-001', '14.0', '1000.0']

すべてのfieldが文字列として返る点が重要です。14.0も自動的にはfloatになりません。

CSVファイルをcsvモジュールへ渡す場合、Python公式ドキュメントではnewline=""で開くよう示されています。これにより、CSVモジュールが改行を正しく処理できます。

DictReaderで列名から値を取り出す

ヘッダー付きCSVではcsv.DictReaderが便利です。各行が辞書として返ります。

import csv

with open(
    "parts.csv",
    mode="r",
    encoding="utf-8",
    newline="",
) as file:
    reader = csv.DictReader(file)

    for row in reader:
        print(row["part_no"])
        print(row["pressure_mpa"])

位置番号のrow[1]ではなく、意味を表すrow["pressure_mpa"]で参照できます。

ただし、辞書になっても値は文字列です。

pressure_text = row["pressure_mpa"]
pressure_mpa = float(pressure_text)

型変換が失敗する可能性があるため、検証と例外処理が必要です。

欠損値を区別する

次のCSVでは、2行目の圧力が空欄です。

part_no,pressure_mpa,area_mm2
CYL-001,14.0,1000.0
CYL-002,,800.0

空欄をfloat("")へ渡すとValueErrorになります。

def parse_required_float(
    text: str,
    field_name: str,
) -> float:
    if text.strip() == "":
        raise ValueError(f"{field_name} is missing")

    return float(text)

欠損値を無条件に0.0へ置き換えると、「未入力」と「実測値0」を区別できなくなります。空欄を許可する項目なのか、必須項目なのかをデータ仕様として決めます。

数値の型と範囲を検証する

工学計算では、数値へ変換できるだけでは不十分です。NaNや無限大、負値などを除外する必要があります。

import math


def parse_positive_float(
    text: str,
    field_name: str,
) -> float:
    if text.strip() == "":
        raise ValueError(f"{field_name} is missing")

    value = float(text)

    if not math.isfinite(value):
        raise ValueError(f"{field_name} must be finite")

    if value <= 0:
        raise ValueError(
            f"{field_name} must be greater than 0"
        )

    return value

検証項目を分解すると、原因を追いやすくなります。

  • 空欄ではないか
  • 数値形式か
  • NaNや無限大ではないか
  • 許容範囲内か
  • 想定単位と一致するか

単位はpressure_mpa、area_mm2のように列名へ含めると、変換規則を明確にできます。

CSVを書き出す

csv.writerは行のリストを出力します。

import csv

rows = [
    ["part_no", "force_n"],
    ["CYL-001", 14000.0],
    ["CYL-002", 3500.0],
]

with open(
    "results.csv",
    mode="w",
    encoding="utf-8",
    newline="",
) as file:
    writer = csv.writer(file)
    writer.writerows(rows)

列名と辞書を使う場合はcsv.DictWriterを使用できます。

import csv

records = [
    {"part_no": "CYL-001", "force_n": 14000.0},
    {"part_no": "CYL-002", "force_n": 3500.0},
]

fieldnames = ["part_no", "force_n"]

with open(
    "results.csv",
    mode="w",
    encoding="utf-8",
    newline="",
) as file:
    writer = csv.DictWriter(
        file,
        fieldnames=fieldnames,
    )
    writer.writeheader()
    writer.writerows(records)

出力内容です。

part_no,force_n
CYL-001,14000.0
CYL-002,3500.0

JSON文字列とPythonオブジェクトは別物

次の値はPythonの辞書です。

record = {
    "part_no": "CYL-001",
    "pressure_mpa": 14.0,
    "enabled": True,
    "note": None,
}

一方、次はJSON形式の文字列です。

json_text = """
{
  "part_no": "CYL-001",
  "pressure_mpa": 14.0,
  "enabled": true,
  "note": null
}
"""

見た目は似ていますが、型が異なります。

print(type(record))
print(type(json_text))

出力です。

<class 'dict'>
<class 'str'>

JSONでは真偽値がtrue・false、値なしがnullです。PythonではTrue・False・Noneです。

loadsとdumpsは文字列を変換する

json.loads()はJSON文字列をPythonオブジェクトへ変換します。

import json

json_text = """
{
  "part_no": "CYL-001",
  "pressure_mpa": 14.0
}
"""

record = json.loads(json_text)

print(record["part_no"])
print(type(record))

出力です。

CYL-001
<class 'dict'>

json.dumps()はPythonオブジェクトをJSON文字列へ変換します。

import json

record = {
    "part_no": "CYL-001",
    "force_n": 14000.0,
}

json_text = json.dumps(
    record,
    ensure_ascii=False,
    indent=2,
    allow_nan=False,
)

print(json_text)
print(type(json_text))

出力です。

{
  "part_no": "CYL-001",
  "force_n": 14000.0
}

dumpsの末尾のsは、文字列を返す操作だと覚えると区別しやすくなります。

loadとdumpはファイルを扱う

JSONファイルを読むにはjson.load()を使います。

import json

with open(
    "cylinder.json",
    mode="r",
    encoding="utf-8",
) as file:
    record = json.load(file)

print(record["part_no"])

ファイルへ書くにはjson.dump()を使います。

import json

record = {
    "part_no": "CYL-001",
    "force_n": 14000.0,
}

with open(
    "result.json",
    mode="w",
    encoding="utf-8",
) as file:
    json.dump(
        record,
        file,
        ensure_ascii=False,
        indent=2,
        allow_nan=False,
    )
    file.write("\n")

主な使い分けです。

関数入力出力
json.loads()JSON文字列Pythonオブジェクト
json.dumps()PythonオブジェクトJSON文字列
json.load()読み取り可能なファイルPythonオブジェクト
json.dump()PythonオブジェクトとファイルファイルへJSON出力

JSONへ変換できる型

代表的な対応関係です。

PythonJSON
dictobject
list・tuplearray
strstring
int・floatnumber
True・Falsetrue・false
Nonenull

すべてのPythonオブジェクトをそのままJSONへ変換できるわけではありません。

from pathlib import Path
import json

data = {"path": Path("result.json")}
json.dumps(data)

Pathは既定の変換対象ではないためTypeErrorになります。文字列へ変換するなど、データモデルをJSONで表現可能な型へ整えます。

data = {"path": str(Path("result.json"))}

ensure_asciiと日本語

json.dump()とjson.dumps()は、既定では非ASCII文字をエスケープします。

import json

data = {"status": "正常"}
print(json.dumps(data))

人が読むJSONとして日本語をそのまま出力したい場合はensure_ascii=Falseを指定します。

print(json.dumps(
    data,
    ensure_ascii=False,
    indent=2,
))

出力です。

{
  "status": "正常"
}

ファイル自体はUTF-8で開きます。

NaNとInfinityを出力しない

Pythonのjsonモジュールは既定ではNaNやInfinityを出力できますが、これらは厳密なJSON仕様の数値ではありません。外部システムとの交換では問題になる可能性があります。

import json

data = {"value": float("nan")}

json.dumps(
    data,
    allow_nan=False,
)

allow_nan=Falseを指定するとValueErrorが発生し、非標準の数値を出力前に検出できます。工学データでは、計算途中で生じた非有限値を黙って保存しない方が安全です。

JSONの異常系を処理する

不正なJSON文字列を読み込むとjson.JSONDecodeErrorが発生します。

import json

json_text = '{"pressure_mpa": 14.0,}'

try:
    record = json.loads(json_text)
except json.JSONDecodeError as error:
    print(
        f"JSON error at line {error.lineno}, "
        f"column {error.colno}: {error.msg}"
    )

JSONでは末尾の余分なカンマや、キーを単一引用符で囲む書き方は無効です。

外部から受け取るJSONには、サイズ制限も必要です。Python公式ドキュメントでも、信頼できないJSONの解析はCPUやメモリを大量消費する可能性があるため、入力サイズを制限するよう注意されています。

実務例:CSVを検証してJSONへ変換する

次のCSVを入力とします。

part_no,pressure_mpa,area_mm2
CYL-001,14.0,1000.0
CYL-002,,800.0
CYL-003,high,1200.0
CYL-004,7.0,500.0

欠損や不正値を除外し、検証済みデータだけで理論推力を計算します。

import csv
import json
import math


def parse_positive_float(
    text: str,
    field_name: str,
) -> float:
    if text.strip() == "":
        raise ValueError(f"{field_name} is missing")

    value = float(text)

    if not math.isfinite(value) or value <= 0:
        raise ValueError(
            f"{field_name} must be a finite positive number"
        )

    return value


valid_records = []
errors = []

with open(
    "cylinders.csv",
    mode="r",
    encoding="utf-8",
    newline="",
) as file:
    reader = csv.DictReader(file)

    for row_no, row in enumerate(reader, start=2):
        try:
            pressure_mpa = parse_positive_float(
                row["pressure_mpa"],
                "pressure_mpa",
            )
            area_mm2 = parse_positive_float(
                row["area_mm2"],
                "area_mm2",
            )
        except (KeyError, TypeError, ValueError) as error:
            errors.append({
                "row_no": row_no,
                "reason": str(error),
            })
            continue

        valid_records.append({
            "part_no": row["part_no"].strip(),
            "pressure_mpa": pressure_mpa,
            "area_mm2": area_mm2,
            "force_n": pressure_mpa * area_mm2,
        })

with open(
    "results.json",
    mode="w",
    encoding="utf-8",
) as file:
    json.dump(
        valid_records,
        file,
        ensure_ascii=False,
        indent=2,
        allow_nan=False,
    )
    file.write("\n")

print(f"Valid: {len(valid_records)}")
print(f"Errors: {len(errors)}")

実行結果です。

Valid: 2
Errors: 2

results.jsonの内容です。

[
  {
    "part_no": "CYL-001",
    "pressure_mpa": 14.0,
    "area_mm2": 1000.0,
    "force_n": 14000.0
  },
  {
    "part_no": "CYL-004",
    "pressure_mpa": 7.0,
    "area_mm2": 500.0,
    "force_n": 3500.0
  }
]

除外理由は次のように保持できます。

[
  {
    "row_no": 3,
    "reason": "pressure_mpa is missing"
  },
  {
    "row_no": 4,
    "reason": "could not convert string to float: 'high'"
  }
]

主要な変換・検証ロジックはPython 3.12.14で実行確認済みです。

この例で重要なのは、形式を変換しただけではない点です。

  1. CSVを辞書として読み込む
  2. 必須項目を確認する
  3. 文字列を数値へ変換する
  4. 有限の正数であることを確認する
  5. 検証済みデータだけ計算する
  6. 数値型を保持したJSONへ出力する
  7. 除外した行番号と理由を別に残す

よくある失敗

CSVの数値を文字列のまま使う

CSVから読んだ"14"は文字列です。加算や比較の前に、仕様に沿ってintまたはfloatへ変換します。

空欄をすべて0へ変換する

未入力、計測不能、該当なし、実測値0は別の状態です。欠損値の意味をデータ仕様として定義します。

CSVをsplitで処理する

引用符で囲まれたカンマや改行を正しく扱えません。csvモジュールを使用します。

JSON文字列を辞書だと思う

json.dumps()の戻り値はstrです。辞書として参照するにはjson.loads()でPythonオブジェクトへ戻します。

dumpを同じファイルへ繰り返す

JSONは複数オブジェクトを区切るフレーム形式ではありません。同じファイルへjson.dump()を連続実行すると、通常は1個の有効なJSON文書になりません。複数件はリストへまとめるか、JSON Linesなど目的に合う形式を別途設計します。

ファイル形式だけで妥当性を判断する

JSONとして構文が正しくても、圧力が負、単位が不明、必須キーがないといった意味上の問題は残ります。構文検証と業務ルール検証を分けます。

練習問題

CSVの数値を変換する

次の行から流量をfloatへ変換してください。

row = {
    "line_name": "Line A",
    "flow_l_min": "60.0",
}

解答例です。

flow_l_min = float(row["flow_l_min"])
flow_l_s = flow_l_min / 60

print(f"Flow: {flow_l_s:.2f} L/s")

出力です。

Flow: 1.00 L/s

JSON文字列を辞書へ変換する

import json

json_text = '{"pressure_mpa": 14.0}'
record = json.loads(json_text)

print(record["pressure_mpa"])
print(type(record))

出力です。

14.0
<class 'dict'>

Pythonデータを日本語入りJSONへ変換する

import json

result = {
    "part_no": "CYL-001",
    "status": "正常",
    "force_n": 14000.0,
}

json_text = json.dumps(
    result,
    ensure_ascii=False,
    indent=2,
    allow_nan=False,
)

print(json_text)

確認観点は、日本語をそのまま表示し、非有限値を許可しない設定にしていることです。

まとめ

  • CSVは同じ列を持つ複数レコードに向く
  • JSONはkey/value、配列、入れ子構造を表せる
  • CSVのfieldは基本的に文字列として読み込まれる
  • CSVはsplit(",")ではなくcsvモジュールで処理する
  • CSVファイルはnewline=""を指定して開く
  • DictReaderとDictWriterで列名を基準に処理できる
  • 欠損、型、有限性、範囲、単位を検証してから計算する
  • loads・dumpsはJSON文字列を扱う
  • load・dumpはファイルオブジェクトを扱う
  • JSON文字列とPythonの辞書・リストは別の型である
  • ensure_ascii=Falseで日本語をそのまま出力できる
  • allow_nan=Falseで非標準の非有限値を検出できる
  • 変換できなかった行は、行番号と理由を残す

CSVやJSONはデータを保存する器です。再利用可能なデータへ変えるには、項目名、型、単位、必須条件、許容範囲を定義し、読み込みと検証を分離する必要があります。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次