Column order, header names, encodings and BOMs all differ between exports. The merge order that works, and the four failures that make rows disappear silently.
Merging CSV files looks like a one-liner until the first real folder. The failures are rarely loud — you get a file with fewer rows than you expected and no error, and you find out a month later.
errors="ignore", quietly drops the
bytes it cannot decode. Column names come back as garbage and the merge produces empty columns.EF BB BF. Read it as plain UTF-8
and the first column is named \ufeffid, not id. Your merge then has two
different columns called "id" and half the rows fill the wrong one.Revenue / revenue /
the Chinese for revenue / Amount (USD). Concatenating first and mapping later drops
rows, because a groupby on one of those names ignores the other three entirely.Read → normalize each file → then merge. Never merge first.
import pandas as pd, os
RENAME = {"revenue ": "revenue", "Amount (USD)": "revenue"}
frames = []
for fn in sorted(os.listdir(folder)):
if not fn.lower().endswith(".csv") or fn.startswith("~$"):
continue
path = os.path.join(folder, fn)
# 1. encoding: try utf-8-sig (handles BOM), fall back to gbk
for enc in ("utf-8-sig", "gbk"):
try:
df = pd.read_csv(path, dtype=str, encoding=enc)
break
except UnicodeDecodeError:
continue
else:
print("SKIP (no encoding worked):", fn)
continue
# 2. header names: strip, lower, then apply the known aliases
df.columns = [c.strip().lower() for c in df.columns]
df = df.rename(columns=RENAME)
# 3. provenance: you will want to know which file a bad row came from
df["source_file"] = fn
frames.append(df)
merged = pd.concat(frames, ignore_index=True, sort=False)
merged.to_csv("merged.csv", index=False, encoding="utf-8-sig")
Three details in there earn their keep. dtype=str stops pandas turning
007 into 7 and 20240101 into a date. sort=False keeps
column order stable across files instead of sorting them alphabetically. source_file is the
column you will thank yourself for when a total looks wrong.
print("files merged :", len(frames))
print("rows merged :", len(merged))
print("per file :", merged.source_file.value_counts().to_dict())
print("empty cols :", merged.columns[merged.isna().all()].tolist())
If files merged is smaller than the number of files in the folder, one file failed to
parse and was skipped — that is the failure you were about to miss. A column that is entirely
empty means a header mismatch, not missing data.
Past roughly a million rows Excel will not open the result and pandas will need a few GB of RAM. Two options that keep the row count honest:
chunksize=100000 and write to one output CSV in
append mode, skipping the header after the first chunk. Memory stays flat.Excel Report Toolkit is the read → normalize → merge → render pipeline in one Python file: encoding fallback, header aliasing, provenance column, and a summary sheet on the other end, with a demo folder of deliberately messy files. 19 USDT, runs offline.
Turn a folder of spreadsheets into a finished Excel summary + Word report. One command.
Get it — 19 USDT