폴더 속 파일 목록을 CSV로 정리하기
하위 폴더까지 읽어 파일 경로·확장자·크기·수정 시각을 표로 남깁니다. 작은 예제 4개로 시작하고 원본과 기존 결과를 보존합니다.
누락 키·null·빈 문자열을 구분하면서 표로 변환하고 별도 점검 보고서를 만듭니다.
이런 분께JSON 파일을 표로 확인하고 데이터가 빠진 위치를 찾으려는 Python 입문자
JSON에서 키가 없는 상태, 값이 null인 상태, 빈 문자열인 상태는 서로 다릅니다. CSV에서는 모두 빈 칸처럼 보일 수 있으므로 이 예제는 별도의 field_issues.csv에 원래 상태를 기록합니다. 임의의 값으로 보정하지 않습니다.
기대하는 열은 item, quantity, unit입니다. 0, false, 공백 한 칸은 누락으로 처리하지 않습니다. 키의 존재를 먼저 검사한 다음 값의 상태를 확인합니다.
[
{"item": "가상부품-A", "quantity": 3, "unit": "개"},
{"item": "가상부품-B", "quantity": 5},
{"item": "가상부품-C", "unit": "개"},
{"item": "가상부품-D", "quantity": null, "unit": ""}
]레코드는 4개입니다. 두 번째에는 unit 키가 없고 세 번째에는 quantity 키가 없습니다. 네 번째의 quantity는 null이고 unit은 빈 문자열입니다. 품목 이름과 단위는 실제 입력값이므로 번역본에서도 동일하게 표시합니다.
python --version
python example.py sample.json --output-dir outputs명령의 상대 경로는 현재 터미널 폴더를 기준으로 합니다. 실행 전에 example.py가 있는 폴더로 이동하세요. 출력 폴더는 아직 없어야 하고 그 부모 폴더는 있어야 합니다. 이미 outputs가 있으면 새 이름을 지정하세요.
python example.py sample.json --output-dir outputs_second입력 검증을 마친 뒤 새 폴더를 생성합니다. 기존 출력 폴더를 비우거나 덮어쓰지 않으며 입력 JSON을 수정하지 않습니다.
| 파일 | 확인할 내용 |
|---|---|
| converted.csv | 헤더 다음에 4개 데이터 행 |
| field_issues.csv | missing_key 2개, null 1개, empty_string 1개 |
| summary.json | input_rows=4, output_rows=4, reported_issue_cells=4 |
OK: rows=4; missing_key_cells=2; null_cells=1; empty_string_cells=1; issue_cells=4점검 수치는 행 수가 아니라 문제가 있는 셀 수입니다. 네 번째 행에 두 문제가 있어 전체 점검 항목은 4개가 됩니다. CSV를 텍스트 편집기로 먼저 확인하세요. 임의 외부 데이터는 스프레드시트에서 자동 해석될 수 있으며 이 코드는 수식 문자열을 정화하지 않습니다.
JSON 배열 안의 객체만 허용합니다. 중첩 객체·배열, 알 수 없는 열, 중복 JSON 키, NaN·Infinity를 거부합니다. 소수는 Decimal로 읽어 불필요한 이진 부동소수점 변환을 피합니다. 5 MiB보다 큰 입력 파일은 처리하지 않습니다.
#!/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())
python -B -X utf8 -m unittest -v test_example테스트는 임시 폴더에서 정상 변환, 누락 구분, 잘못된 입력과 기존 출력 보호를 확인합니다. 22개 테스트의 마지막 결과가 OK인지 확인하세요. 테스트가 통과해도 모든 실제 데이터 형식을 지원한다는 뜻은 아닙니다.
| 메시지 | 확인할 내용 |
|---|---|
| E_INPUT | JSON 형식, 열 이름, 값 형식과 크기를 확인하세요. |
| E_OUTPUT_EXISTS | 새 출력 폴더 이름을 지정하세요. |
| E_READ | 입력 경로와 읽기 권한을 확인하세요. |
| E_WRITE | 쓰기 권한과 디스크 상태를 확인하세요. 불완전한 출력은 사용하지 마세요. |
메모리에 입력 전체와 출력 내용을 보관하는 작은 파일용 예제입니다. 중첩 JSON 펼치기, 날짜 변환, 자동 열 추론, 누락값 보정은 하지 않습니다. 오류를 고칠 때는 원본을 복사한 연습 파일에서 시도하세요.
출력 중 디스크 오류가 나면 일부 파일만 남을 수 있습니다. 성공 메시지와 세 파일의 내용을 함께 확인하세요. 이 예제는 CSV를 여는 스프레드시트의 자동 변환 동작까지 검증하지 않습니다.
2026-09-19 · Windows 11 · CPython 3.12.14 · 표준 라이브러리
코드·입력 데이터·실행 안내가 포함되어 있습니다. 압축을 풀고 README.txt부터 읽어 주세요.
예제 ZIP 다운로드직접 작성한 연습 자료 · 원본을 따로 보관한 뒤 실행하세요.
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.