春江暮客

春江暮客的个人学习分享网站

Validate CSV Structure with Python: Exact Headers, Column Order, and Field Counts

2026-10-17 Technology
Validate CSV Structure with Python: Exact Headers, Column Order, and Field Counts

An upstream CSV export swaps two columns, but an importer still reads the old positions. An unquoted comma in a note can also shift the remaining values. Being able to open the file does not prove that its structure matches the downstream contract.

This beginner-to-intermediate tutorial builds a Python standard-library command that checks an exact header, exact order, and consistent field counts. It reads one record at a time and exits nonzero on the first problem. A successful JSON report appears only after the entire file passes, making the command useful before a batch import.

1. Method: treat the header as an interface contract

Our example requires sample_id,label,note, in that order:

  1. Compare column names exactly, including their order. Added, removed, duplicate, or padded column names are rejected.
  2. Every data record must have the same number of fields as the header. Blank lines also fail.
  3. Require at least one data record. Empty fields with the correct field count are allowed; data types and business meaning require separate checks.
  4. Default to comma-separated fields and double quotes, accepting UTF-8 with or without a BOM. Semicolon-separated input needs an explicit option.

Use csv.reader to obtain field lists. Python’s official CSV documentation explains that DictReader fills missing fields with a default value and stores excess fields in an additional entry. Receiving a dictionary therefore does not prove the field count is correct. This example compares list lengths explicitly.

This check enforces only the stated contract. Follow it with streaming CSV processing and label validation for rules about label values and empty IDs. It is not a comprehensive CSV standards validator.

2. Setup and test files

Use Python 3.9 or newer, with no third-party dependencies. These examples were tested on Linux with Python 3.11.4; shell commands work in Bash or zsh.

  1. Create a demo directory and run all subsequent commands there. Choose a new directory name when repeating the exercise:
python3 --version
mkdir csv-schema-demo
cd csv-schema-demo
  1. Generate fixtures. valid.csv has a UTF-8 BOM and CRLF line endings, including a quoted comma and a field spanning two lines:
python3 - <<'PY_DATA'
import csv
from pathlib import Path

with open("valid.csv", "w", encoding="utf-8-sig", newline="") as target:
    writer = csv.writer(target)
    writer.writerow(["sample_id", "label", "note"])
    writer.writerow(["001", "1", "quoted, comma"])
    writer.writerow(["002", "0", "two\nlines"])

fixtures = {
    "reordered.csv": "label,sample_id,note\n1,001,ok\n",
    "duplicate.csv": "sample_id,label,label\n001,1,ok\n",
    "short.csv": "sample_id,label,note\n001,1\n",
    "extra.csv": "sample_id,label,note\n001,1,ok,extra\n",
    "broken.csv": 'sample_id,label,note\n001,1,"unfinished\n',
    "blank.csv": "sample_id,label,note\n\n",
    "header-only.csv": "sample_id,label,note\n",
    "empty.csv": "",
    "semicolon.csv": "sample_id;label;note\n001;1;ok\n",
}
for name, text in fixtures.items():
    Path(name).write_text(text, encoding="utf-8")
PY_DATA

This creates files only in the demo directory and reads no production data. For UnicodeDecodeError, start with CSV encoding identification and UTF-8 conversion. Do not discard original bytes with errors="ignore".

3. Complete implementation: validate headers and record widths

Save this code as csv_schema.py in the current directory:

"""Validate an exact CSV header and row widths. Python 3.9+, standard library."""
import argparse
import csv
import json
from pathlib import Path
import sys


def validate(path, columns, delimiter):
    with path.open(encoding="utf-8-sig", newline="") as source:
        reader = csv.reader(source, delimiter=delimiter, strict=True)
        try:
            header = next(reader, None)
            if header != columns:
                raise ValueError(f"header mismatch: expected {columns!r}, got {header!r}")
            records = 0
            for records, row in enumerate(reader, start=1):
                if len(row) != len(columns):
                    raise ValueError(
                        f"record {records}, ending line {reader.line_num}: "
                        f"expected {len(columns)} fields, got {len(row)}"
                    )
            if records == 0:
                raise ValueError("no data records")
        except csv.Error as exc:
            raise ValueError(f"CSV parse error near line {reader.line_num}: {exc}") from exc
    return {"file": str(path), "columns": header, "records": records, "status": "ok"}


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("file", type=Path)
    parser.add_argument("--columns", nargs="+", required=True,
                        help="exact column names, in the required order")
    parser.add_argument("--delimiter", choices=[",", ";"], default=",")
    args = parser.parse_args()
    if any(not name or name != name.strip() for name in args.columns):
        parser.error("column names must be nonempty with no surrounding whitespace")
    if len(set(args.columns)) != len(args.columns):
        parser.error("expected column names must be unique")
    try:
        report = validate(args.file, args.columns, args.delimiter)
    except (OSError, UnicodeError, ValueError) as exc:
        print(f"ERROR: {args.file}: {exc}", file=sys.stderr)
        return 1
    print(json.dumps(report, ensure_ascii=False))
    return 0


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

Opening with newline="" leaves CSV newline handling to the parser. strict=True raises an exception for malformed input that the parser recognizes. See the official documentation for csv.reader and Dialect.strict. Strict mode does not enforce your header contract, data types, or every possible quoting mistake, so explicit header and width comparisons are still needed.

The checker leaves the input unchanged and never collects all records into a list. Memory mainly holds the current record and header; an oversized field can still hit the parser’s field-size limit. Expected column names must also be nonempty, unique, and free of surrounding whitespace, so an invalid contract cannot become accepted configuration.

4. Usage and error locations

  1. Show help and validate the good file:
python3 csv_schema.py --help
python3 csv_schema.py valid.csv --columns sample_id label note

Expected output:

{"file": "valid.csv", "columns": ["sample_id", "label", "note"], "records": 2, "status": "ok"}

records is 2, rather than the physical line count. The second data record contains a newline in note but remains one CSV record.

  1. Check the extra-field fixture and handle this expected failure in the shell:
if python3 csv_schema.py extra.csv --columns sample_id label note; then
  printf 'Schema accepted\n'
else
  printf 'Schema rejected; check stderr above\n'
fi

Standard error includes:

ERROR: extra.csv: record 1, ending line 2: expected 3 fields, got 4

record 1 starts after the header. ending line 2 is the physical ending line consumed by the parser. The official csvreader.line_num documentation distinguishes line counts from record counts. These numbers can differ for multiline records. A parse error’s near line is diagnostic context, not an exact character location.

  1. Specify the delimiter for input known to use semicolons:
python3 csv_schema.py semicolon.csv \
  --columns sample_id label note --delimiter ';'

Do not fix records by deleting commas or splitting text with split(","); that breaks valid quoting and multiline fields.

Exit code Meaning
0 The entire file passes this structure contract; standard output contains one JSON line.
1 Structure, parsing, decoding, or file access fails; the reason goes only to standard error.
2 Invalid arguments, such as duplicate expected column names or an unsupported delimiter.

5. Validate success and failure branches

Run this self-check without creating a production failure. It asserts exit codes, error reasons, and successful reports while allowing tested subprocesses to return nonzero:

python3 - <<'PY_CHECK'
import json
import subprocess
import sys

columns = ["--columns", "sample_id", "label", "note"]
cases = [
    (["valid.csv", *columns], 0, None),
    (["reordered.csv", *columns], 1, "header mismatch"),
    (["duplicate.csv", *columns], 1, "header mismatch"),
    (["short.csv", *columns], 1, "expected 3 fields, got 2"),
    (["extra.csv", *columns], 1, "expected 3 fields, got 4"),
    (["broken.csv", *columns], 1, "CSV parse error"),
    (["blank.csv", *columns], 1, "expected 3 fields, got 0"),
    (["header-only.csv", *columns], 1, "no data records"),
    (["empty.csv", *columns], 1, "header mismatch"),
    (["missing.csv", *columns], 1, "ERROR:"),
    (["semicolon.csv", *columns], 1, "header mismatch"),
    (["semicolon.csv", *columns, "--delimiter", ";"], 0, None),
    (["valid.csv", "--columns", "sample_id", "sample_id"], 2, "must be unique"),
]
for arguments, code, message in cases:
    result = subprocess.run(
        [sys.executable, "csv_schema.py", *arguments],
        capture_output=True, text=True, check=False,
    )
    assert result.returncode == code, (arguments, result)
    if code == 0:
        report = json.loads(result.stdout)
        assert report["status"] == "ok", report
        assert report["records"] == (2 if arguments[0] == "valid.csv" else 1), report
        assert result.stderr == "", result
    else:
        assert result.stdout == "" and message in result.stderr, (arguments, result)
print("OK: 13 CSV schema checks")
PY_CHECK

Expected output:

OK: 13 CSV schema checks

The checks cover reordered headers, duplicate column names, short and extra-field records, unclosed quotes, blank lines, header-only input, empty files, missing files, delimiter mismatches, and invalid arguments. The valid fixture also exercises a BOM, CRLF, quoted commas, and embedded newlines so legitimate CSV is accepted.

6. Add a batch gate and troubleshoot

  1. Gate downstream work with &&, continuing only when the structure check succeeds:
python3 csv_schema.py valid.csv --columns sample_id label note \
  && python3 -c 'print("Downstream step started")'

The demo downstream command prints one line; replace it with your existing import command. Keep the input unchanged between validation and import. If a producer is still writing, let it finish and validate a fixed output first, otherwise the two steps may read different contents.

  1. Fix the source of each problem and validate again:
Error or symptom Concrete action
header mismatch Compare expected and actual names, order, and delimiter. Update expected columns and downstream reading together only after confirming an intentional interface change.
expected 3 fields, got 4 Inspect the record for a comma-containing field lacking quotes. Fix the exporter and regenerate the file instead of discarding its last column.
got 2 or got 0 Check for truncated exports, omitted fields, or blank lines. A valid empty value retains its delimiter, as in 001,1,.
CSV parse error Check quote closure and input completeness. Oversized fields can also hit parser limits; establish the actual data and required bound before adjusting it.
no data records This contract rejects header-only files. If empty batches are valid, change that rule and its tests explicitly instead of treating every failure as success.
Permission denied / missing file Check the actual path, file read access, and parent-directory access using the account that runs the task.

A correct structure does not prove ID uniqueness. To avoid duplicate imports after merging batches, add CSV duplicate-ID checks with Python and SQLite.

7. Summary

An exact header, column order, and record width catch upstream schema drift and some malformed records before import. Validate with small fixtures containing quotes, multiline fields, and failure cases before adding the check to a batch workflow. Keep structural rules, field-value rules, and ID uniqueness explicit, with meaningful exit codes.

标签

1024 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena copy cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo imports indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 phaser pillow pip postfix preprocessing print probability protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode usdt utf-8 uv vhhbert vite wallet webp wordcloud wordpress workflow yaml zipfile zoneinfo 迅雷解析

友情链接

其它