Scripts e automação de arquivos

Converta uma lista JSON para CSV e verifique campos ausentes

Converta os dados em uma tabela diferenciando chaves ausentes, valores null e strings vazias, e crie um relatório separado de problemas.

Ver o sumário

A tradução foi feita com IA. Confira o código, as unidades e os valores junto com o original. A revisão por falantes nativos de cada idioma ainda não foi concluída. English

Para quem éIniciantes em Python que querem visualizar um arquivo JSON como tabela e encontrar onde há dados ausentes

Preparação
  • Tenha Python 3.12 ou posterior e um terminal prontos. Nenhum pacote adicional é necessário.
  • Extraia o ZIP e abra um terminal na pasta que contém example.py e sample.json.
  • Comece apenas com os dados sintéticos de items incluídos. Mantenha os arquivos originais separados.

01Por que diferenciar campos ausentes de valores vazios?

Em JSON, uma chave ausente, um valor null e uma string vazia são estados diferentes. Como todos eles podem parecer células em branco no CSV, este exemplo registra o estado original em um arquivo field_issues.csv separado. Ele não preenche valores substitutos arbitrários.

As colunas esperadas são item, quantity e unit. Os valores 0, false e um único espaço não são tratados como ausentes. O código primeiro verifica se a chave existe e depois verifica o estado do valor.

02Examine os dados de prática

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

Há 4 registros. O segundo não tem a chave unit e o terceiro não tem a chave quantity. No quarto, quantity é null e unit é uma string vazia. Os nomes de item e as units são valores reais de entrada, portanto permanecem iguais nas versões traduzidas.

03Crie os resultados em uma nova pasta

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

Os caminhos relativos no comando são resolvidos a partir da pasta atual do terminal. Antes de executar, navegue até a pasta que contém example.py. A pasta de saída ainda não deve existir, e sua pasta pai já deve existir. Se outputs já existir, escolha um novo nome.

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

Uma nova pasta é criada depois que a entrada é validada. O código não esvazia nem sobrescreve uma pasta de saída existente e não modifica o JSON de entrada.

04Compare com os resultados esperados

ArquivoO que verificar
converted.csv4 linhas de dados após o cabeçalho
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

As contagens de problemas se referem a células com problemas, não ao número de linhas. A quarta linha tem dois problemas, elevando o total de problemas relatados para 4. Verifique primeiro o CSV em um editor de texto. Dados externos arbitrários podem ser interpretados automaticamente por softwares de planilha, e este código não sanitiza formula strings.

05Código completo e fluxo de processamento

Somente objects dentro de um JSON array são permitidos. Nested objects ou arrays, columns desconhecidas, duplicate JSON keys, NaN e Infinity são rejeitados. O código lê decimal numbers como Decimal para evitar conversão desnecessária para binary floating point. Arquivos de entrada maiores que 5 MiB não são processados.

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())

06Execute os testes incluídos

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

Os testes usam temporary folders para verificar conversão normal, distinção entre valores ausentes e vazios, entrada inválida e proteção da saída existente. Verifique se o resultado final dos 22 tests é OK. Passar nos testes não significa que todos os formatos de dados do mundo real sejam suportados.

07Encontre a causa pela mensagem de erro

MensagemO que verificar
E_INPUTVerifique JSON syntax, column names, value types e input size.
E_OUTPUT_EXISTSEspecifique um novo output folder name.
E_READVerifique input path e read permissions.
E_WRITEVerifique write permissions e o estado do disk. Não use uma output incompleta.

08Escopo e limitações

Este exemplo é destinado a arquivos pequenos e mantém todo o conteúdo de entrada e saída na memória. Ele não achata nested JSON, não converte datas, não infere columns automaticamente nem preenche missing values. Ao corrigir erros, trabalhe em uma cópia de prática do arquivo original.

Se ocorrer um disk error durante a gravação da saída, apenas alguns arquivos podem permanecer. Verifique tanto o success message quanto o conteúdo dos três arquivos. Este exemplo não verifica o comportamento de conversão automática de softwares de planilha ao abrir arquivos CSV.

Registro de execução e verificação

2026-09-19 · Windows 11 · CPython 3.12.14 · Biblioteca padrão

  • Foi confirmado que 22 tests passaram em uma revisão independente
  • Foi confirmado que o código do artigo corresponde ao código disponível para download
  • Foram confirmadas 4 sample rows e 4 reported issues
  • Foi confirmado o bloqueio de uma output folder existente e a preservação do input original
Limites da verificação
  • O autor original realizou uma verificação separada no Linux com CPython 3.13.5.
  • Nem todos os dados do mundo real ou interpretações automáticas de softwares de planilha foram verificados.

Princípios de redação e verificação de todo o site

Arquivos de exemplo para executar

Inclui código, dados de entrada e instruções de execução. Extraia o ZIP e leia primeiro o README.txt.

Baixar ZIP de exemplo

O código de exemplo, os nomes de arquivos e as chaves de entrada são mantidos como no original. Consulte também os comandos e os procedimentos de conferência do texto traduzido.

Material de prática original · Guarde os arquivos originais separadamente antes de executar.

Fontes de referência

As explicações e os exemplos são de elaboração própria. Os comportamentos e conceitos relacionados podem ser consultados nas fontes oficiais abaixo.