スクリプト・ファイル自動化

JSON リストを CSV に変換し、欠損フィールドを確認する

欠落したキー、null 値、空文字列を区別しながらデータを表形式に変換し、別の問題レポートを作成します。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English

対象読者JSON ファイルを表形式で確認し、どこにデータ欠損があるかを見つけたい Python 初学者

準備するもの
  • Python 3.12 以降とターミナルを用意します。追加パッケージは不要です。
  • ZIP を展開し、example.py と sample.json があるフォルダでターミナルを開きます。
  • まず付属の合成 item データだけを使用します。元ファイルは別に保管してください。

01欠落フィールドと空の値を区別する理由

JSON では、キーの欠落、null 値、空文字列はそれぞれ異なる状態です。CSV ではすべて空白セルのように見える可能性があるため、この例では元の状態を別の field_issues.csv ファイルに記録します。任意の置換値を自動で埋めることはありません。

想定する列は item, quantity, unit です。値 0、false、半角スペース1文字は欠損として扱いません。コードはまずキーが存在するか確認し、その後で値の状態を確認します。

02練習データを確認する

json
[
  {"item": "가상부품-A", "quantity": 3, "unit": "개"},
  {"item": "가상부품-B", "quantity": 5},
  {"item": "가상부품-C", "unit": "개"},
  {"item": "가상부품-D", "quantity": null, "unit": ""}
]

レコードは4件あります。2件目は unit キーがなく、3件目は quantity キーがありません。4件目では quantity が null、unit が空文字列です。item 名と unit は実際の入力値なので、翻訳版でもそのまま保持します。

03新しいフォルダに結果を作成する

bash
python --version
python example.py sample.json --output-dir outputs

コマンド内の相対パスは、ターミナルの現在のフォルダを基準に解決されます。実行前に example.py があるフォルダへ移動してください。出力フォルダはまだ存在していてはいけません。また、その親フォルダは事前に存在している必要があります。outputs がすでに存在する場合は、新しい名前を選んでください。

bash
python example.py sample.json --output-dir outputs_second

入力の検証が完了した後に新しいフォルダが作成されます。コードは既存の出力フォルダを空にしたり上書きしたりせず、入力 JSON も変更しません。

04想定結果と比較する

ファイル確認すること
converted.csvヘッダーの後にデータ行が4件
field_issues.csvmissing_key: 2, null: 1, empty_string: 1
summary.jsoninput_rows=4, output_rows=4, reported_issue_cells=4
text
OK: rows=4; missing_key_cells=2; null_cells=1; empty_string_cells=1; issue_cells=4

問題件数は、問題のある行数ではなく、問題のあるセル数を指します。4行目には問題が2つあるため、報告される問題の合計は4件になります。まず CSV をテキストエディタで確認してください。任意の外部データは表計算ソフトによって自動解釈される可能性があり、このコードは formula strings を無害化しません。

05完全なコードと処理フロー

JSON array 内の objects だけを許可します。nested objects または arrays、未知の columns、duplicate JSON keys、NaN、Infinity は拒否されます。不要な binary floating point 変換を避けるため、decimal numbers は Decimal として読み取ります。5 MiB を超える入力ファイルは処理しません。

example.py
#!/usr/bin/env python3
"""Convert a small local JSON array to CSV and a field-issue report.

Target: Python 3.12+, standard library only. No network access.
All output goes to a NEW directory; existing directories/files are refused.
"""

from __future__ import annotations

import argparse
import csv
import io
import json
import sys
from decimal import Decimal
from pathlib import Path

DEFAULT_FIELDS = ("item", "quantity", "unit")
MAX_INPUT_BYTES = 5 * 1024 * 1024
ISSUE_FIELDS = ("row_number", "field", "issue")


class InputError(ValueError):
    """The input does not satisfy this example's data contract."""


def unique_object(pairs: list[tuple[str, object]]) -> dict[str, object]:
    result: dict[str, object] = {}
    for key, value in pairs:
        if key in result:
            raise InputError("Duplicate key in a JSON object.")
        result[key] = value
    return result


def reject_constant(token: str) -> object:
    raise InputError(f"Non-standard JSON number: {token}.")


def csv_bytes(fields: tuple[str, ...], rows: list[dict]) -> bytes:
    buffer = io.StringIO(newline="")
    writer = csv.DictWriter(buffer, fieldnames=fields, lineterminator="\r\n")
    writer.writeheader()
    writer.writerows(rows)
    return buffer.getvalue().encode("utf-8")


def prepare_outputs(source: Path, fields: tuple[str, ...]) -> tuple[dict, dict]:
    if not fields or len(fields) != len(set(fields)):
        raise InputError("--fields must contain at least one unique field name.")
    if any(not field.strip() for field in fields):
        raise InputError("Field names must not be empty or whitespace-only.")

    # Read-only, bounded input. Do not modify the source, even on failure.
    with source.open("rb") as stream:
        raw = stream.read(MAX_INPUT_BYTES + 1)
    if len(raw) > MAX_INPUT_BYTES:
        raise InputError("Input exceeds the example's 5 MiB limit.")
    records = json.loads(
        raw.decode("utf-8-sig"),
        object_pairs_hook=unique_object,
        parse_float=Decimal,
        parse_constant=reject_constant,
    )
    if not isinstance(records, list):
        raise InputError("Top-level JSON value must be an array.")

    rows: list[dict[str, str]] = []
    issues: list[dict[str, object]] = []
    counts = {"missing_key": 0, "null": 0, "empty_string": 0}
    missing_rows: set[int] = set()
    allowed = set(fields)
    for row_number, record in enumerate(records, start=1):
        if not isinstance(record, dict):
            raise InputError(f"Record {row_number} must be an object.")
        if set(record) - allowed:
            raise InputError(
                f"Record {row_number} has unlisted keys; include them in --fields."
            )
        row: dict[str, str] = {}
        for field in fields:
            issue = None
            if field not in record:
                issue = "missing_key"
                missing_rows.add(row_number)
                value = None
            else:
                value = record[field]
                if value is None:
                    issue = "null"
                elif value == "":
                    issue = "empty_string"
            if issue is not None:
                counts[issue] += 1
                issues.append({"row_number": row_number, "field": field, "issue": issue})
                row[field] = ""
            elif isinstance(value, str):
                row[field] = value
            elif isinstance(value, bool):
                row[field] = "true" if value else "false"
            elif isinstance(value, (int, Decimal)):
                row[field] = str(value)
            else:
                raise InputError(
                    f"Record {row_number} contains an unsupported nested value."
                )
        rows.append(row)

    summary = {
        "status": "complete",
        "columns": list(fields),
        "input_rows": len(records),
        "output_rows": len(rows),
        "missing_key_cells": counts["missing_key"],
        "rows_with_missing_keys": len(missing_rows),
        "null_cells": counts["null"],
        "empty_string_cells": counts["empty_string"],
        "reported_issue_cells": len(issues),
    }
    # Render and UTF-8-encode EVERYTHING before creating the output directory.
    # summary.json is written last and serves as a completion record.
    outputs = {
        "converted.csv": csv_bytes(fields, rows),
        "field_issues.csv": csv_bytes(ISSUE_FIELDS, issues),
        "summary.json": (json.dumps(summary, ensure_ascii=False, indent=2) + "\n").encode("utf-8"),
    }
    return outputs, summary


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("input", type=Path, help="Local UTF-8 JSON file")
    parser.add_argument("--output-dir", type=Path, required=True, help="NEW directory; parent must exist")
    parser.add_argument("--fields", nargs="+", default=list(DEFAULT_FIELDS), help="Expected CSV columns, in order")
    args = parser.parse_args(argv)
    if sys.version_info < (3, 12):
        print("E_VERSION: Python 3.12 or newer is required.", file=sys.stderr)
        return 2
    try:
        outputs, summary = prepare_outputs(args.input, tuple(args.fields))
    except json.JSONDecodeError as exc:
        print(f"E_INPUT: Invalid JSON at line {exc.lineno}, column {exc.colno}.", file=sys.stderr)
        return 2
    except (ValueError, ArithmeticError, RecursionError) as exc:
        print(f"E_INPUT: {exc}", file=sys.stderr)
        return 2
    except OSError as exc:
        print(f"E_READ: Cannot read input ({exc.__class__.__name__}).", file=sys.stderr)
        return 4

    try:
        args.output_dir.mkdir(exist_ok=False)
    except FileExistsError:
        print("E_OUTPUT_EXISTS: Output path already exists; choose a new directory.", file=sys.stderr)
        return 3
    except OSError as exc:
        print(f"E_WRITE: Cannot create output directory ({exc.__class__.__name__}).", file=sys.stderr)
        return 4
    try:
        for name, payload in outputs.items():
            with (args.output_dir / name).open("xb") as stream:
                stream.write(payload)
    except OSError as exc:
        print(
            f"E_WRITE: Incomplete NEW output directory ({exc.__class__.__name__}); "
            "do not use its results. Inspect it and choose a new directory.",
            file=sys.stderr,
        )
        return 4
    print(
        f"OK: rows={summary['output_rows']}; "
        f"missing_key_cells={summary['missing_key_cells']}; "
        f"null_cells={summary['null_cells']}; "
        f"empty_string_cells={summary['empty_string_cells']}; "
        f"issue_cells={summary['reported_issue_cells']}"
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

06付属テストを実行する

bash
python -B -X utf8 -m unittest -v test_example

テストでは temporary folders を使い、通常の変換、欠落値と空値の区別、不正な入力、既存出力の保護を確認します。22 tests の最終結果が OK であることを確認してください。テストに合格しても、すべての実データ形式をサポートしていることを意味するわけではありません。

07エラーメッセージから原因を特定する

メッセージ確認すること
E_INPUTJSON syntax、column names、value types、input size を確認します。
E_OUTPUT_EXISTS新しい output folder name を指定します。
E_READinput path と read permissions を確認します。
E_WRITEwrite permissions と disk の状態を確認します。不完全な output は使用しないでください。

08対象範囲と制限

この例は小規模ファイル向けで、入力と出力の内容全体をメモリに保持します。nested JSON の flatten、date conversion、column の自動推論、missing values の補完は行いません。エラーを修正する場合は、元ファイルの練習用コピーで作業してください。

出力書き込み中に disk error が発生すると、一部のファイルだけが残る可能性があります。success message と3つすべてのファイル内容の両方を確認してください。この例では、CSV ファイルを開いたときの表計算ソフトによる自動変換動作は検証していません。

実行・検証の記録

2026-09-19 · Windows 11 · CPython 3.12.14 · 標準ライブラリ

  • 独立したレビューで 22 tests が合格したことを確認しました
  • 記事内のコードがダウンロード可能なコードと一致することを確認しました
  • sample rows が4件、reported issues が4件であることを確認しました
  • 既存 output folder が拒否され、元の input が保持されることを確認しました
検証範囲の限界
  • 元の作成者は Linux と CPython 3.13.5 でも別途検証しました。
  • すべての実データや表計算ソフトによる自動解釈を検証したわけではありません。

サイト全体の執筆・検証方針

自分で実行するためのサンプルファイル

コード、入力データ、実行手順が含まれています。展開して、まずREADME.txtを読んでください。

サンプルZIPをダウンロード

サンプルコード、ファイル名、入力キーは原文のままです。翻訳本文のコマンドと確認手順も併せて参照してください。

独自に作成した練習用資料 · 元のファイルを別に保管してから実行してください。

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。