收上来一堆 Excel,怎么保证没改错

收上来一堆报名表,合并、改格式、查重复、按学院拆回去,这些活手工做慢是一回事,更麻烦的是不可复现——同一堆文件,两个人合出来不一样,同一个人在不同时间合出来也不一样,而且谁都没留下上次改了什么的记录,出了错也倒不回去查。

把整套动作固化成脚本,让每一步都留下凭据:哪行来自哪份文件、改过哪些格、哪些行被标了重复,最后落到一张行数对账单上。

本文把这几个环节挨个拆开:每步留下什么凭据、少掉的每一行怎么在账上对平。

下面的示例用 8 个学院交上来的报名表走一遍,一共 424 行,格式上的坑是设计进去的。

完整脚本文末分享。

1. 先把几十份文件堆成一张

第一件事是把散在各处的表收进来,同时给每行盖个章,写明它来自哪份文件。

d = pd.read_csv(f, dtype=str, encoding="utf-8-sig")
frames.append(d.assign(来源文件=f.name))
merged = pd.concat(frames, ignore_index=True)

脚本顺手把每份的行数、多出哪些列、缺哪些列打印出来:

读入行数合计: 424
         来源文件  行数 多出的列 缺的列
  报名表_医学院.csv  58    -   -
报名表_外国语学院.csv  49   备注   -
 报名表_数学学院.csv  55    -   -
  报名表_文学院.csv  60    -   -
  报名表_法学院.csv  55    -   -
 报名表_物理学院.csv  53    -   -
  报名表_经管学院.csv  46    -   -
报名表_计算机学院.csv  48    -   -

8 份文件、424 行合到一张;外国语学院那份 49 行、多一列「备注」,另外 7 份既不多也不缺列。真实收表时各学院用的模板往往不是同一版,这份清单比合并结果本身更该先扫一眼——某份少了关键列,后面清洗会整列落空,而对账单上的行数照样是齐的。

原始文件如下:

两个细节值得说。

  1. 读的时候全按字符串读,不让 pandas 替你猜类型——学号里的前导零、写成文本的日期,一被猜成数字就再也补不回来了。
  2. 拼的时候用 concat,它按列名对齐,不按位置:这 8 份表的列顺序被刻意打乱过,concat 会把多出来的「备注」补成空值,其余文件那一列自动留空,不会把手机号错并进日期列。

手工复制粘贴最容易栽在这里。清单加上 来源文件 这一列,就是后面所有凭据的起点:改动记录要能追到哪份文件,对账单要能按文件核行数,都靠它。

2. 统一格式,但把改动记下来

表合起来了,格式还是各写各的:日期有 2026/3/1、3月1日、2026-03-01 三种,手机号有带横线的、带空格的、光秃秃的,姓名前后甚至中间夹着空格,有两份表的学院名整列带空格。

直接拿去查重,同一条记录会被当成好几条。

日期里最麻烦的是 3月1日 这种没写年份的,直接丢给 to_datetime 会补出 1900 年,脚本先把年份补成 2026,再统一成 %Y-%m-%d,补不出来的也不硬猜。

if str(new) != str(old):
    out.at[i, col] = new
    log.append({"来源文件": row["来源文件"], "学号": row["学号"],
                "列": col, "原值": old, "新值": new})

关键是这段:每改一格就记一条,原值、新值、哪一列、哪份文件、哪个学号,全留下来。

这次跑完打印的是:

清洗改动格数: 696
报名日期    272
手机号     263
学院       99
姓名       62
无法解析的日期: 8 行;识别为空行: 8 行(都只标记,不删)

696 格改动按列分得清清楚楚,单独存成 改动记录.xlsx。

改错了或者对方不认,翻这一张表就能倒回去。

输出最后一行是另一条更重要的规矩:不静默丢弃。

8 行日期写成「待定」,解析不了就写进 _clean_errors 列;8 个全空行标一个 空行。

都只标记,一行都不 drop。清洗阶段只改值、不动行数,这是后面能对得平的前提。

3. 再查重复

格式统一之后,查重就轻松了。

「张 三」和「张三」、带横线的和不带横线的手机号,在上一章已经变成同一个值,精确重复自己浮出来。

exact = valid[valid.duplicated(subset=key, keep=False)]
for i in grp.index:
    out.at[i, "重复组"] = f"D{g:03d}"
    out.at[i, "匹配置信度"] = "1.00 精确"
精确重复: 16 行 / 8 组
模糊兜底: 16 行 / 8 组
合计标记: 32 行;删除 0 行

判定键选的是姓名加手机号,没用学号:学号有人填错、有人空着,手机号在报名表里基本是硬标识。

剩下的是手机号一样、姓名被写错一个字的那种,用标准库的 difflib 算个相似度当置信度,又标出 16 行、8 组。

两字姓名换掉一个字,相似度常常只有 0.50——这种低分组最不该由脚本替你决定。

所以输出里「删除 0 行」不是漏了,是刻意只加 重复组 和 匹配置信度 两列,合计标记 32 行。

哪个组要合并、哪条该留,是人看一眼再定的事。

回头看这个顺序:先清洗再查重,模糊匹配从主角降级成兜底,连专门的模糊匹配库都不用装。

要是反过来,就得靠模糊算法去硬扛一堆本该先统一掉的格式差异,还很容易误判。

4. 按学院拆回去

标记留好了,接着把干净的行按学院拆成各自的文件发出去。

ready = df[~df["空行"] & (df["_clean_errors"] == "")].copy()
excluded = len(df) - len(ready)
for value, grp in ready.groupby(cfg["拆分字段"]):
显式排除: 16 行;应拆出: 408 行
                文件  行数
  报名表_医学院_已整理.xlsx  56
报名表_外国语学院_已整理.xlsx  47
 报名表_数学学院_已整理.xlsx  53
  报名表_文学院_已整理.xlsx  58
  报名表_法学院_已整理.xlsx  53
 报名表_物理学院_已整理.xlsx  51
  报名表_经管学院_已整理.xlsx  44
报名表_计算机学院_已整理.xlsx  46

排除的 16 行就是前面记下的 8 个空行加 8 行日期解析失败,excluded 这个数得单独留着。

别图省事写一句 dropna() 把不明不白的行都丢掉——少了几行没人说得清,第 5 章的账就永远对不平。

清单里 8 份文件各在 44 到 58 行之间,合计正好 408。拆出去的每行都还带着 来源文件、重复组和置信度,收到文件的人自己就能核。

脚本到这里就停了:文件备齐、清单打出来,发不发、发给谁,留给人按最后那一下。

自动发送看着省事,可一旦分发错了收不回来,跟「敢交出去」正好相反。

5. 出一张对账单

最后交的不是按学院求和的汇总表,是一张行数账单,专门回答「这一趟有没有把数据弄丢」。脚本打印出来的是这两张:

项目 行数
各文件读入行数合计 424
减:显式排除(空行 / 日期无法解析) -16
应拆出行数 408
拆分文件行数合计 408
差异 0
改动统计:
          项目      数值
      清洗改动格数     696
查重标记行数(只标不删)      32
   其中精确 / 模糊 16 / 16
       拆分文件数       8
{"项目": "拆分文件行数合计", "行数": actual},
{"项目": "差异", "行数": actual - ready_n},

这两张表一起写进 对账单.xlsx 的两个 sheet。

右边那几个数只作备忘,别拿去加减——696 是改过的单元格,32 是被标记的记录,都不改变表的行数,真正参与对账的只有行数那一列。

左边那个 0 不是凑出来的。整条流水线只有拆分那一步会减少行数,而减少的 16 行全都有名有姓;清洗只改值、查重只标记,都不动行数。

前面四章留下的来源列、改动记录、重复标记、拆分清单,到这里被一张表串完。

对账单只对本次输出目录里的文件负责,所以重跑之前得先清干净,否则上个月的拆分文件混进来,数就再也对不平了。

6. 串成一个入口

五步各是一个函数,run_pipeline 按序跑完,输出目录里留下 11 个 .xlsx:合并总表、改动记录、8 份拆分文件,加一份双 sheet 的对账单。入口第一件事就是清目录:

if out_dir.exists():
    shutil.rmtree(out_dir)
out_dir.mkdir(parents=True)

脚本只依赖 pandas,装了 openpyxl 就走 pandas 自带的写出,没装就用标准库直接写一份最小可用的 .xlsx,两种情况生成的都是真 Excel 文件。

跑完先看对账单的「差异」是不是 0,再往外发——这一列是整趟处理唯一要紧的数字。

整理后的文件如下:

文中所用的完整代码和输入输出的文件压缩后共享在:收上来一堆 Excel,怎么保证没改错.zip:

https://url11.ctfile.com/f/45455611-17569912153929-77e1f8?p=6872

posted @ 2026-11-13 13:26  wang_yb  阅读(44)  评论(0)    收藏  举报