两个部门的工资表对不上账?按身份证号跨文件对账(含 3 个实跑大坑)

对账的经典变体:不是两份流水对账(那份见 30行Python自动对账),而是两份花名册——技术部一份工资表、市场部一份工资表,老板要的是「同一个人两边发的钱对不对得上」。按姓名对?遇到重名或改名就穿帮。本篇按身份证号对账,本次完整实跑过:2 个文件 9 行数据(含故意埋的脏数据和「同证号不同名」陷阱),4 个证号全对出、1 人金额差异被标记、脏行跳过打印原因,重跑幂等(两次输出 md5 相同)。

核心脚本

#!/usr/bin/env python3
"""跨文件按身份证号对账:多个部门的工资表,同一人在不同文件里金额是否一致?"""
import sys, os
from collections import defaultdict
from openpyxl import load_workbook, Workbook

def norm(s):
    return str(s).strip() if s is not None else ""

def main(paths, out):
    recs = defaultdict(list)   # 证号 -> [(来源, 姓名, 金额)]
    skipped = []
    for p in paths:
        wb = load_workbook(p, data_only=True)
        for ws in wb.worksheets:
            header = None; col = None
            for row in ws.iter_rows(values_only=True):
                cells = [norm(c) for c in row]
                if not any(cells):
                    continue
                if header is None:
                    header = [norm(h) for h in row]   # 坑①:表头先 strip
                    col = {h: i for i, h in enumerate(header)}
                    if not {"姓名","身份证号","应发工资"} <= set(header):
                        header = None
                    continue
                try:
                    amount = float(cells[col["应发工资"]])   # 坑②:脏行兜底
                except (KeyError, ValueError):
                    skipped.append(f"{os.path.basename(p)}::{ws.title} {cells}")
                    continue
                recs[cells[col["身份证号"]]].append(
                    (f"{os.path.basename(p)}::{ws.title}", cells[col["姓名"]], amount))
    outwb = Workbook(); ws1 = outwb.active; ws1.title = "对账结果"
    ws1.append(["身份证号","姓名(各处)","各处金额","是否一致","来源"])
    n_diff = 0
    for idno in sorted(recs):
        rs = recs[idno]
        names = "/".join(sorted({r[1] for r in rs}))    # 坑③:口径按证号不按姓名
        amts = sorted({r[2] for r in rs})
        ok = "一致" if len(amts) == 1 else "❌差异"
        if len(amts) > 1: n_diff += 1
        ws1.append([idno, names, "/".join(str(a) for a in amts), ok,
                    "; ".join(f"{r[0]}:{r[2]}" for r in rs)])
    for s in skipped:
        print(f"[跳过行] {s}", file=sys.stderr)
    outwb.save(out)
    print(f"共对账 {len(recs)} 个证号,金额差异 {n_diff} 人,跳过脏行 {len(skipped)} 行")

if __name__ == "__main__":
    main(sys.argv[1:-1], sys.argv[-1])

实跑结果

构造两个文件:技术部 3 人(张三 8000 / 李四 9500 / 王五 7000.5),市场部 5 行——张三同证号填 8500(真差异)、赵六正常、还有一个「钱七」用了张三的身份证号但姓名不同填 5000(陷阱行)、孙八金额填 "abc"(脏行):

共对账 4 个证号,金额差异 1 人,跳过脏行 1 行
[跳过行] 市场部-9月工资.xlsx::9月 ['孙八', '999', 'abc']

-- 对账结果.xlsx
110101199001011234, 张三/钱七, 5000.0/8000.0/8500.0, ❌差异, 技术部-9月工资.xlsx::9月:8000.0; 市场部-9月工资.xlsx::9月:8500.0; 市场部-9月工资.xlsx::9月:5000.0
110101199203024321, 李四, 9500.0, 一致, ...
120102198811115678, 王五, 7000.5, 一致, ...
130103199507078765, 赵六, 6800.0, 一致, ...

「张三/钱七」这行就是最值钱的输出:同一个证号底下挂着两个名字、三种金额,不管这是填错、冒用还是重名录入,人肉对账基本看不出来。重跑一次输出文件 md5 完全相同,幂等。

3 个实跑大坑

坑 1:按姓名对账天然有洞。第一直觉是拿「姓名」当对账键——但姓名不是唯一标识:同名不同人是误伤,同人不同名(改名、英文名、录错字)是漏报。能拿身份证号就不要拿姓名当键;姓名只用来「展示」,把同一证号下出现过的所有名字拼在一起,异常自己会浮出水面(实跑里「张三/钱七」就是这么暴露的)。
坑 2:脏行不兜底,一行 "abc" 崩掉整批。「应发工资」列出现 "abc"、"待定"、"—" 太常见,float() 直接 ValueError 且崩在循环深处。必须 try/except 单行跳过 + 把跳过的行列到 stderr——不然用户根本不知道少了谁。
坑 3:汇总口径要按证号聚合,不能按姓名聚合。如果先用姓名做 defaultdict 收集再想换键,张三和钱七的钱就永久分不开了。聚合键从第一行收集时就要定成身份证号;判定「是否一致」用金额去重后的个数(len(set(amts))),而不是逐对比较——后者写起来绕还容易漏。

和流水对账的区别

流水对账是「两边各一条记录,按单号对齐找单边/金额差」;花名册对账是「同一个人在多份表里各有一条,比的是金额集合是否一致」。前者输出「缺了哪单」,后者输出「哪个人哪几处金额不一样」。选错模板对出来的结果没法看。

没时间自己改?这是 喵窝资源铺 的常用脚本场景之一:9.9 元起,把你的 Excel 发来,AI 写脚本+人工核验结果后交付,虚拟物品不退款但免费重做到结果正确。更多实跑验证过的脚本见 文章合集。