#!/usr/bin/env python3 """ csv_to_json.py — convert CSV files to JSON. Features: - Auto-detects delimiter (comma, semicolon, tab, pipe) or accepts --delimiter - Detects header row automatically (or accepts --no-header with --columns) - Converts values to int/float/bool where unambiguous, else keeps strings - Custom output options: indent, list-of-objects vs. records+columns, key column - Robust error handling for invalid/empty/malformed CSV - Reads from file or stdin, writes to stdout or file Python 3.8+, stdlib only. Usage examples: python3 csv_to_json.py input.csv python3 csv_to_json.py input.csv --delimiter ';' --indent 2 cat input.csv | python3 csv_to_json.py --stdin --key id python3 csv_to_json.py input.csv --output out.json --format records """ import argparse import csv import io import json import sys def detect_delimiter(sample: str, candidates=(",", ";", "\t", "|")) -> str: """Pick the delimiter that yields the most columns from the first non-empty line.""" best, best_count = candidates[0], 0 for cand in candidates: for line in sample.splitlines(): if not line.strip(): continue count = len(line.split(cand)) if count > best_count: best, best_count = cand, count return best def convert_value(raw: str): """Best-effort scalar conversion: int -> float -> bool -> string.""" if raw is None: return None s = raw.strip() if s == "": return None low = s.lower() if low in ("true", "false"): return low == "true" try: return int(s) except ValueError: pass try: return float(s) except ValueError: pass return raw def parse_csv(text: str, delimiter: str): """Parse CSV text into rows, raising ValueError with a clear message on malformed input.""" try: return list(csv.reader(io.StringIO(text), delimiter=delimiter)) except csv.Error as exc: raise ValueError(f"CSV parse error: {exc}") from exc def build_output(rows, *, no_header, columns, key): """Convert parsed rows to a JSON-serializable structure.""" if not rows: raise ValueError("CSV is empty: no data rows to convert") if no_header: if not columns: raise ValueError("--no-header requires --columns (comma-separated column names)") headers = [c.strip() for c in columns.split(",")] data_rows = rows else: headers = [h.strip() for h in rows[0]] data_rows = rows[1:] if len(set(headers)) != len(headers): raise ValueError("Duplicate column names in header row") records = [] for i, row in enumerate(data_rows): if not any(cell.strip() for cell in row): continue # skip fully empty lines record = {} for idx, header in enumerate(headers): record[header] = convert_value(row[idx]) if idx < len(row) else None records.append(record) if key: keyed = {} for record in records: k = record.get(key) if k is None: raise ValueError(f"Key column '{key}' missing or empty in a record") keyed[str(k)] = record return keyed return records def main(argv=None): parser = argparse.ArgumentParser(description="Convert CSV to JSON (stdlib only).") parser.add_argument("input", nargs="?", help="CSV file path (default: read from stdin)") parser.add_argument("--delimiter", default=None, help="Explicit delimiter (auto-detect if omitted)") parser.add_argument("--no-header", action="store_true", help="Input has no header row") parser.add_argument("--columns", default=None, help="Column names for --no-header, comma-separated") parser.add_argument("--key", default=None, help="Column to use as object keys") parser.add_argument("--format", choices=["array", "records"], default="array", help="array = list of objects; records = {columns, records} (default: array)") parser.add_argument("--indent", type=int, default=2, help="JSON indent (default: 2)") parser.add_argument("--output", default=None, help="Write JSON to file instead of stdout") args = parser.parse_args(argv) try: if args.input: with open(args.input, "r", encoding="utf-8-sig", newline="") as fh: text = fh.read() else: text = sys.stdin.read() if not text.strip(): raise ValueError("No input provided (empty file/stdin)") delimiter = args.delimiter or detect_delimiter(text) rows = parse_csv(text, delimiter) records = build_output(rows, no_header=args.no_header, columns=args.columns, key=args.key) if args.format == "records": keys = list(records[0]) if isinstance(records, list) and records else [] payload = {"columns": keys, "records": records} else: payload = records out = json.dumps(payload, ensure_ascii=False, indent=args.indent) if args.output: with open(args.output, "w", encoding="utf-8") as fh: fh.write(out + "\n") else: print(out) except ValueError as exc: print(f"error: {exc}", file=sys.stderr) return 2 except OSError as exc: print(f"error: {exc}", file=sys.stderr) return 2 return 0 if __name__ == "__main__": sys.exit(main())