A defensive data-normalization pipeline that recursively discovers Excel and CSV exports, aligns their schemas, and vertically concatenates them into a reviewable two-sheet workbook.
The project is designed for real business exports: column aliases, reordered fields, currency stored as text, mixed date formats, empty rows, exact duplicates, nested folders, source lineage, and partially unreadable batches.
The included five-file fixture produces a deterministic audit trail:
- 5 source files discovered and processed
- 29 source rows loaded
- 1 blank row removed
- 1 exact duplicate removed
- 27 normalized rows written
Merge SummaryandMerged Dataworksheets
Salary remains separate from Revenue, and Hire Date remains separate from transaction Date; unrelated business concepts are never combined merely because they share a numeric or date type.
- Recursive, case-insensitive discovery of
.xlsx,.xls, and.csvfiles. - Deterministic source ordering and relative-path lineage in
source_file. - Configurable column aliases such as
Sales Amount→revenueandTerritory→region. - Collision-safe headers, including preservation of a user-supplied
source_filefield. - Strict money parsing with support for symbols, thousands separators, negatives, and accounting parentheses.
- Typed Excel date cells for recognized date columns while preserving unrecognized source text.
- Leading-zero CSV identifiers such as
00123without automatic numeric coercion. - Empty-row cleanup after whitespace trimming.
- Optional exact-row deduplication that excludes only the generated lineage field.
- Formula-injection protection for untrusted headers and cell text.
- Formula cells from
.xlsxinputs preserved as inert text instead of disappearing or executing. - Atomic output replacement, rotating logs, structured JSON run summaries, and explicit partial-run status.
Merge Summary is the first worksheet and shows the main run metrics plus a source-by-source audit table. Merged Data contains the normalized records with:
- an Excel table and filters;
- a frozen header row;
- typed currency and date formatting;
- bounded, readable column widths; and
- source lineage as the final column.
The output must be outside the input folder. This prevents a source workbook from being overwritten and prevents previous output from being ingested on a later run.
Run these commands from the project root.
PowerShell:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txtmacOS or Linux:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtMerge the included sample files and save both workbook and JSON audit artifacts:
python excel_merger.py \
--input ./sample_input \
--output ./merged_master.xlsx \
--json-out ./merge_run.jsonPowerShell uses the same options on one line:
python excel_merger.py --input .\sample_input --output .\merged_master.xlsx --json-out .\merge_run.jsonOptions:
--input: source folder; nested folders are included.--output: destination.xlsxfile outside the input folder.--json-out: optional machine-readable run summary.--no-dedup: keep exact duplicate rows.--strict: abort if any discovered input cannot be read.--overwrite: explicitly replace an existing destination workbook.
Without --strict, readable files still produce a workbook when another input fails. The run is marked partial, every skipped relative path is listed in the JSON summary, and the CLI exits with status 2 rather than reporting full success.
- Discover supported files recursively and reject an output path inside the source tree.
- Read CSV fields as text to preserve identifiers; read
.xlsxformulas without executing them. - Normalize and de-duplicate headers, trim text, remove blank rows, and type only known money/date fields.
- Neutralize formula-like text and add relative source lineage.
- Align columns and concatenate the normalized frames.
- Optionally remove exact duplicates across all business columns.
- Write the workbook to a temporary file and atomically replace the destination only after export succeeds.
This is vertical concatenation, not a relational join. Records are stacked after schema alignment; the tool does not match rows by a business key.
COLUMN_ALIASES is the main extension point:
COLUMN_ALIASES: dict[str, str] = {
"full name": "name",
"contact email": "email",
"sales amount": "revenue",
"territory": "region",
"transaction date": "date",
"hire date": "hire_date",
"salary": "salary",
}When two source headers map to the same canonical name, the second receives a deterministic suffix such as revenue_2; no column is silently discarded.
pip install -r requirements.txt
pip install pytest ruff
pytest -q # 64 tests
ruff check .The regression suite covers parsing, schema collisions, whitespace-only rows, source lineage, recursive discovery, identifier preservation, formula safety, output-path protection, partial and strict modes, workbook formatting, atomic overwrite behavior, JSON summaries, and the complete five-file demonstration.
- Each
.xlsxfile contributes its active worksheet; legacy.xlsfiles contribute their first worksheet. - The tool recognizes four configured money/date column names (
revenue,salary,date, andhire_date); additional typed fields should be added deliberately. - Deduplication removes only exact normalized-row matches. Business-key matching belongs in a separate join/reconciliation workflow.
MIT — see LICENSE.