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

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

キーの欠落・null・空文字列を区別しながら表に変換し、確認用のレポートを別途作成します。

目次を表示

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

対象読者JSONファイルを表で確認し、データが欠けている箇所を見つけたいPython初心者

準備するもの
  • Python 3.12以上とターミナルを用意してください。追加パッケージは不要です。
  • ZIPを展開し、example.pyとsample.jsonがあるフォルダーでターミナルを開いてください。
  • 最初は同梱の架空の品目データだけを使用してください。元のファイルは別に保管してください。

01欠落と空の値を区別する理由

JSONでは、キーが存在しない状態、値がnullの状態、空文字列の状態はそれぞれ異なります。CSVではいずれも空欄に見えることがあるため、この例では別のfield_issues.csvに元の状態を記録します。任意の値で補完することはありません。

想定する列はitem、quantity、unitです。0、false、スペース一つは欠落として扱いません。まずキーの存在を調べ、その後で値の状態を確認します。

02練習用データを確認する

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

レコードは4件です。二件目にはunitキーがなく、三件目にはquantityキーがありません。四件目のquantityはnullで、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件になります。まずテキストエディターでCSVを確認してください。任意の外部データは表計算ソフトで自動解釈されることがあり、このコードは数式文字列のサニタイズを行いません。

05コード全体と処理の流れ

JSON配列内のオブジェクトのみを許可します。入れ子のオブジェクト・配列、未知の列、重複するJSONキー、NaN・Infinityは拒否します。小数は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

テストでは一時フォルダーを使用し、正常な変換、欠落の区別、不正な入力、既存の出力の保護を確認します。22個のテストの最終結果がOKであることを確認してください。テストに合格しても、実際のあらゆるデータ形式に対応しているという意味ではありません。

07エラーメッセージから原因を探す

メッセージ確認する内容
E_INPUTJSONの形式、列名、値の型、入力サイズを確認してください。
E_OUTPUT_EXISTS新しい出力フォルダー名を指定してください。
E_READ入力パスと読み取り権限を確認してください。
E_WRITE書き込み権限とディスクの状態を確認してください。不完全な出力は使用しないでください。

08用途と制限

入力全体と出力内容をメモリに保持する、小さなファイル向けの例です。入れ子のJSONの平坦化、日付変換、列の自動推定、欠損値の補完は行いません。エラーを修正するときは、元のファイルをコピーした練習用ファイルで試してください。

出力中にディスクエラーが発生すると、一部のファイルだけが残ることがあります。成功メッセージと三つのファイルの内容を併せて確認してください。この例では、CSVを開く表計算ソフトの自動変換動作までは検証していません。

実行・検証の記録

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

  • 独立したレビューで22個のテストに合格
  • 本文のコードとダウンロード用コードの一致を確認
  • サンプル4行と確認項目4件を確認
  • 既存の出力フォルダーの拒否と入力原本の保持を確認
検証範囲の限界
  • 原著者はLinux・CPython 3.13.5で別途検証しました。
  • 実際のすべてのデータや表計算ソフトの自動解釈を検証したわけではありません。

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

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

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

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

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

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

参考資料

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