Scripts e automação de arquivos

Converter uma lista JSON em CSV e verificar campos ausentes

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

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. 한국어

Para quem éIniciantes em Python que querem visualizar um arquivo JSON como tabela e localizar os dados que estão faltando

Preparação
  • Tenha Python 3.12 ou superior e um terminal disponíveis. Não são necessários pacotes adicionais.
  • Extraia o ZIP e abra um terminal na pasta que contém example.py e sample.json.
  • No início, use apenas os dados de itens fictícios incluídos. Guarde os arquivos originais separadamente.

01Por que distinguir campos ausentes de valores vazios

Em JSON, uma chave ausente, um valor null e uma string vazia são estados diferentes. Como todos podem aparecer como células em branco no CSV, este exemplo registra o estado original em um arquivo field_issues.csv separado. Ele não preenche os dados com valores 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. Primeiro, o código verifica se a chave existe; depois, verifica o estado do valor.

02Examinar os dados de prática

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

São 4 registros. No segundo, falta a chave unit; no terceiro, falta a chave quantity. No quarto, quantity é null e unit é uma string vazia. Os nomes dos itens e as unidades são valores reais de entrada, por isso permanecem iguais também nas versões traduzidas.

03Criar os resultados em uma nova pasta

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

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

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

A nova pasta é criada depois da validação da entrada. O código não esvazia nem sobrescreve uma pasta de saída existente e não modifica o JSON de entrada.

04Comparar 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 às células com problemas, não ao número de linhas. A quarta linha tem dois problemas, totalizando 4 ocorrências no relatório. Primeiro, confira o CSV em um editor de texto. Dados externos arbitrários podem ser interpretados automaticamente por um programa de planilhas, e este código não sanitiza strings de fórmulas.

05Código completo e fluxo de processamento

São permitidos apenas objetos dentro de um array JSON. Objetos ou arrays aninhados, colunas desconhecidas, chaves JSON duplicadas e valores NaN e Infinity são rejeitados. Os números decimais são lidos como Decimal para evitar conversões desnecessárias para ponto flutuante binário. 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())

06Executar os testes incluídos

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

Os testes usam pastas temporárias para verificar a conversão normal, a distinção entre estados de ausência, entradas inválidas e a proteção de saídas existentes. Confira se o resultado final dos 22 testes é OK. A aprovação nos testes não significa que todos os formatos de dados reais sejam compatíveis.

07Identificar a causa pela mensagem de erro

MensagemO que verificar
E_INPUTVerifique o formato JSON, os nomes das colunas, os tipos dos valores e o tamanho da entrada.
E_OUTPUT_EXISTSInforme um novo nome para a pasta de saída.
E_READVerifique o caminho de entrada e as permissões de leitura.
E_WRITEVerifique as permissões de gravação e as condições do disco. Não use saídas incompletas.

08Escopo e limitações

Este exemplo é destinado a arquivos pequenos e mantém toda a entrada e o conteúdo da saída na memória. Ele não transforma JSON aninhado em uma estrutura plana, não converte datas, não infere colunas automaticamente nem preenche valores ausentes. Ao corrigir erros, use uma cópia do arquivo original para praticar.

Se ocorrer um erro de disco durante a gravação da saída, apenas alguns arquivos poderão permanecer. Confira tanto a mensagem de sucesso quanto o conteúdo dos três arquivos. Este exemplo não verifica o comportamento de conversão automática dos programas de planilhas ao abrir arquivos CSV.

Registro de execução e verificação

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

  • 22 testes aprovados em uma revisão independente
  • Confirmada a correspondência entre o código do artigo e o código para download
  • Confirmadas as 4 linhas de amostra e as 4 ocorrências relatadas
  • Confirmadas a rejeição de uma pasta de saída existente e a preservação da entrada original
Limites da verificação
  • O autor original realizou uma verificação separada no Linux com CPython 3.13.5.
  • Não foram verificados todos os dados reais nem a interpretação automática dos programas de planilhas.

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.