表格校验与对账技能
目的
提供一套可靠、可复用的表格校验与对账流程,核心是统一命令行脚本 scripts/reconcile_toolkit.py(基于 pandas)。该脚本把两类高频、易错、且手工做起来极其繁琐的工作固化为子命令:
- 单表规则校验:按规则清单给一张表做体检,找出不满足业务约束的行
- 两表勾稽对账:把两张表按关键列对上,分出五类差异并给出差额
对账与校验是财务、仓储、采购、HR 的周期性刚需,手工用 VLOOKUP 处理容易漏行、错行,且难以复现。本技能把它固化为可重复执行的命令,每次结果一致。
适用场景
- 财务对账:银行流水 vs 账面记录、应收账款 vs 客户回款、总账 vs 明细账
- 库存盘点:系统库存 vs 实际盘点、出入库流水 vs 结存
- 采购核对:采购订单 vs 到货单 vs 发票、供应商对账单核对
- 薪酬核对:工资表 vs 银行代发清单、社保公积金基数核对
- 数据质量检查:必填缺失、主键重复、金额超范围、格式不符合规范、枚举值非法、跨列逻辑矛盾(如期末余额 < 期初余额)
- 台账体检:凭证号唯一性、编码格式规范、状态值合法性、借贷方向校验
环境准备
要求 Python 3.11+(核心依赖 pandas 3.x 的最低要求)。
核心依赖 pandas 与 openpyxl 必须安装:
python -m pip install -r requirements.txt
# 或最小安装:
python -m pip install "pandas>=3.0.5" "openpyxl>=3.1.5"
可选依赖按需安装(不装不影响核心功能,用到对应功能时脚本会在执行前报错并提示安装命令):
python -m pip install "python-calamine>=0.8" # xlsx 加速读取(推荐,见下)
python -m pip install "pyyaml>=6.0" # 读取 YAML 规则文件(JSON 规则无需)
python -m pip install "pyarrow>=20.0" # parquet / feather 格式
python -m pip install "xlrd>=2.0" # 读取旧版 .xls
python -m pip install "pytest>=8.0" # 运行单元测试
> 大表建议装 python-calamine。 xlsx 解析是整条链路上最慢的一环:
> pandas 默认用 openpyxl 逐单元格解析 XML,10 万行 6 列实测 约 7.7 秒;
> 换成 calamine 后 约 1.2 秒(快约 6 倍),两者读出的结果与 dtype 完全一致。
> 倍率随机器与数据而异,低配机器上仍应有数倍差距。
> 脚本会自动优先使用它,未安装则静默回退 openpyxl,行为不变。
> 若数据量在 10 万行以上、又经常读 xlsx,这个包基本是必装。
不确定环境是否就绪,直接跑自检,会逐项列出各库状态与缺失项的安装命令:
python scripts/reconcile_toolkit.py check
Windows 环境下若用户无系统 Python,优先使用托管运行时:
C:\Users\User\.workbuddy\binaries\python\versions\3.13.12\python.exe(或创建其 venv)。
先用内置样例跑一遍(推荐)
不想自己造数据的话,技能内带了一组样例:
python examples/make_samples.py
会在 examples/data/ 生成 8 个文件,并打印可直接复制的命令。样例里故意埋了问题——全是干净数据反而看不出工具在做什么:
| 样例文件 | 埋了什么 |
|---|---|
bank.xlsx / book.xlsx |
五类差异各出现一次:匹配 2、金额不符 1、仅左表有 1、仅右表有 1、右表键重复 1 |
发票.xlsx / 收款.xlsx |
一张发票对 3 笔收款;不加 --aggregate 判键重复,加了则合计后判匹配 |
erp.xlsx / 台账.xlsx |
键列叫「订单号」对「单号」,比对列叫「订单金额」对「实收金额」 |
ledger.xlsx |
8 处问题:凭证号缺失与重复、金额超范围、状态枚举非法、手机格式错、期末余额小于期初 |
multi_sheet.xlsx |
两个 sheet,用于试 --sheet |
样例数据由脚本现场生成(而非预置 xlsx),每一行都写在脚本里,可审阅、可复现。
使用流程
第一步:先自检与预览
首次使用或换机器后,先运行 check 做环境依赖自检;对陌生文件先用 info 确认 sheet、列名与行数,据此填对账参数。
python scripts/reconcile_toolkit.py check
python scripts/reconcile_toolkit.py info bank.xlsx
# 多 sheet 文件:先看有哪些 sheet,再看指定那一页
# (不指定 --sheet 时只读第一个 sheet,容易看成"数据不对")
python scripts/reconcile_toolkit.py info bank.xlsx --sheet 流水
# 表头不在第一行(前面有标题、导出时间等):试出正确的跳行数
python scripts/reconcile_toolkit.py info bank.xlsx --skiprows 4
列名不确定时优先用上面的 info;即使跳过这步、把列名写错了,脚本也会报错并列出现有全部列名,据此修正即可。
info 与 rule 都支持 --sheet / --skiprows / --header(--header N 直接指定表头行号,0 起;与 --skiprows 别同时用)。recon 对应的参数是左右表各一份:--left-sheet / --right-sheet、--left-skiprows / --right-skiprows、--left-header / --right-header。
第二步:两表勾稽对账(recon)
最常用的命令。用 --on 指定对账键,--compare 指定要比对的列:
# 银行流水 vs 账面记录,按流水号对账,比对金额
python scripts/reconcile_toolkit.py recon bank.xlsx book.xlsx out.xlsx \
--on 流水号 --compare 金额
# 加容差:差额在 0.01 以内视为匹配(抹平浮点误差)
python scripts/reconcile_toolkit.py recon bank.xlsx book.xlsx out.xlsx \
--on 流水号 --compare 金额 --tolerance 0.01
# 多列联合键(日期 + 单号 共同唯一)
python scripts/reconcile_toolkit.py recon a.xlsx b.xlsx out.xlsx \
--on 日期,单号 --compare 金额
# 左右表键列名不同:--on 也用 左列:右列 写法(ERP 叫「订单号」、台账叫「单号」)
python scripts/reconcile_toolkit.py recon erp.xlsx 台账.xlsx out.xlsx \
--on 订单号:单号 --compare 金额
# 左右表比对列名不同
python scripts/reconcile_toolkit.py recon bank.xlsx book.xlsx out.xlsx \
--on 流水号 --compare 银行金额:账面金额
# 键与比对列都不同名,一起映射
python scripts/reconcile_toolkit.py recon erp.xlsx 台账.xlsx out.xlsx \
--on 订单号:单号 --compare 订单金额:实收金额
# 只输出差异行,报告更精简
python scripts/reconcile_toolkit.py recon a.xlsx b.xlsx out.xlsx \
--on 单号 --compare 金额 --diff-only
# 多列比对:逐列判定,任一列不一致都会报出来,并在「差异列」指明是哪列
python scripts/reconcile_toolkit.py recon a.xlsx b.xlsx out.xlsx \
--on 订单号 --compare 金额,数量
# 键格式规范化:消除大小写 / 全角 / 空白 / 零宽字符造成的假差异
python scripts/reconcile_toolkit.py recon a.xlsx b.xlsx out.xlsx \
--on 单号 --compare 金额 --normalize-keys
# 一对多核销:一张发票 vs 多笔收款,同键多行先汇总再比对
python scripts/reconcile_toolkit.py recon 发票.xlsx 收款.xlsx out.xlsx \
--on 发票号 --compare 金额 --aggregate
# 左右表数据各在自己的 sheet 里、或表头都不在第一行
python scripts/reconcile_toolkit.py recon 台账.xlsx 流水.xlsx out.xlsx \
--on 单号 --compare 金额 --left-sheet 台账 --right-sheet 流水 \
--left-skiprows 4 --right-skiprows 2
# 差异明细另存一份 CSV——直接发给业务方,对方不必打开 xlsx
python scripts/reconcile_toolkit.py recon a.xlsx b.xlsx out.xlsx \
--on 单号 --compare 金额 --issues-out 差异明细.csv
> --left-amount / --right-amount 是单列比对时的早期写法,等价于
> --compare 左列:右列。新脚本建议统一用 --compare,两者不必同时写。
关于键列映射:--on 与 --compare 一样支持 左列:右列 写法。两张表的键列名不同是常态而非例外——银行流水叫「流水号」、账面叫「凭证号」;ERP 导出叫「订单号」、手工台账叫「单号」。有了映射就不必为了对账先去改表头。多列联合键可以只映射其中一列(--on 日期,订单号:单号),不带冒号的列仍按同名处理;报告里这类键会显示为 订单号:单号。
> 列名本身含冒号(如 金额:本币)的列暂不能用映射写法,会被当作分隔符拆开。脚本检测到这种情况会明确说明「这个名字本身就是一个列名」,而不是报一个含糊的「列不存在」。
差异分类(每个键归入且仅归入一类):
| 差异类型 | 含义 | 处理方向 |
|---|---|---|
| 匹配 | 两表都有该键,且比对值一致(或在容差内) | 无需处理 |
| 金额不符 | 两表都有该键,但比对值差额超出容差 | 逐笔核查,差额即 差额 列 |
| 仅左表有 | 只有左表有该键(如银行有、账面无) | 查未入账 |
| 仅右表有 | 只有右表有该键(如账面有、银行无) | 查虚假记账 / 未达账项 |
| 左表键重复 / 右表键重复 | 该键在某一表出现多次,无法一一对应 | 先去重,或用 --aggregate 汇总 |
| 左表键缺失 / 右表键缺失 | 键为空,该行未参与匹配 | 先补键(补单号 / 单独走账)再对 |
关于空键(左表键缺失 / 右表键缺失):键为空的记录不参与匹配。缺号、无票收入、待补录这类行在两张表里都很常见,一旦纳入匹配,两条"都没号"的记录会被当成同一个键配成一对,报告里凭空多出一条金额差异 —— 那是错误核销,比多报差异严重得多,与本工具拒绝模糊匹配是同一条红线。
因此空键行单独归入 左表键缺失 / 右表键缺失,不参与金额判定,也不计入匹配率。处置方向是先补键(补单号 / 补票号 / 单独走账),补完再对。注意只有整键全空才拦;多列联合键里部分列为空仍按原样参与匹配,否则大量真实表会直接对不上,造成更难发现的漏报。
关于键重复的优先级:重复键会被单独标记,不再做金额比对。因为一个键对应多行时,"差额"没有唯一含义,强行比对会给出误导性结果。报告中的 说明 列会指明是哪一侧重复。若多行本来就该合并(如一张发票对应多笔收款),改用 --aggregate 见下。
关于键的智能归一:Excel 常把纯数字单号存成数值,读回变成 1001.0,与另一表的文本 "1001" 对不上,产生假差异。本工具在比对前会把键列统一规范为文本并去掉浮点 .0 后缀,因此 1001.0 与 "1001" 能正确匹配。
关于键格式规范化(--normalize-keys):上面那层基础归一之外,还有四类书写差异会造成假差异——大小写(A001 vs a001)、全角半角(A001 vs A001)、内部空白(A 001 vs A001)、零宽字符(从网页复制常带入,肉眼不可见)。
这四类由 --normalize-keys 一次性开启,也可用 --ignore-case / --ignore-space / --ignore-width / --ignore-invisible 单独开启。默认关闭——工具不会擅自把写法不同的键合并,宁可报出来让人工确认。这些开关只做格式等价变换,不提供相似度模糊匹配:把两个不同的键判成同一个会造成错误核销,比多报差异危险得多。
关于多列比对:--compare 金额,数量 会逐列判定,任一列不一致即记「金额不符」,并在 差异列 列写明是哪几列。报告里的比对值列按列名生成(金额(左) / 金额(右) / 数量(左) / 数量(右)),且保持数值格式,可直接在 Excel 中求和。
关于一对多核销(--aggregate):一张发票对多笔收款、一个订单对多行明细时,1:1 匹配会把同键多行一律判为「键重复」而放弃金额判定。加 --aggregate 后,同键多行先按比对列汇总再比对:
- 数值列求和;汇总后相等即判「匹配」
- 非数值列取该键下的唯一值;若有多个不同值,会如实标出而不是悄悄取某一行
- 报告增加
左表笔数/右表笔数两列,说明列注明按几笔汇总 - 该模式下重复键已被合并,因此不再出现「键重复」分类
第三步:单表规则校验(rule)
两种用法:命令行内联规则,或规则文件。
内联规则(适合临时、少量规则):
python scripts/reconcile_toolkit.py rule ledger.xlsx report.xlsx \
--required 凭证号,金额 \
--unique 凭证号 \
--dtype 金额=数值 \
--range 金额=0,100000 \
--enum "状态=已付款|待付款" \
--format 手机=phone \
--compare "期末余额>=期初余额"
# 规则写在多 sheet 文件的某一页 / 表头不在第一行
python scripts/reconcile_toolkit.py rule ledger.xlsx report.xlsx \
--sheet 明细 --skiprows 2 --required 凭证号
# 问题明细另存一份;单条规则命中上万行时限制明细条数
# (汇总表的「不通过」列始终是全量计数,不受 --max-examples 影响)
python scripts/reconcile_toolkit.py rule ledger.xlsx report.xlsx \
--required 凭证号 --issues-out 问题清单.csv --max-examples 500
# 全部通过时也输出空的「问题明细」sheet——便于固定交付格式
python scripts/reconcile_toolkit.py rule ledger.xlsx report.xlsx \
--required 凭证号 --include-empty
规则文件(适合固定、可复用的业务规则):
# rules.yaml
columns:
凭证号:
required: true
unique: true
金额:
type: number
min: 0
max: 100000
状态:
enum: [已付款, 待付款]
手机:
format: phone
期末余额:
compare: '>= 期初余额'
python scripts/reconcile_toolkit.py rule ledger.xlsx report.xlsx --rules rules.yaml
七类规则:
| 规则 | 参数写法 | 含义 |
|---|---|---|
| 必填 | --required 列1,列2 |
该列不能为空(含空白字符串) |
| 唯一 | --unique 列1,列2 |
该列值不可重复,重复行全部列出 |
| 类型 | --dtype 金额=数值 |
类型:数值 / 整数 / 日期 / 文本 |
| 范围 | --range 金额=0,100000 |
数值区间;任一端可省略,如 金额=,1000 只校验上限 |
| 正则 | --regex "编码=^[A-Z]{2}\d{4}$" |
完全匹配(fullmatch) |
| 枚举 | --enum "状态=已付款|待付款" |
取值必须在允许列表内 |
| 跨列 | --compare "期末余额>=期初余额" |
支持 >= <= == != > <,数值优先、退化文本比较 |
八种命名格式(--format 引用,免写正则):
| 格式名 | 校验内容 |
|---|---|
phone |
中国大陆手机号 |
id |
18 位身份证号(按格式) |
email |
邮箱 |
bankcard |
银行卡号(16–19 位数字) |
taxno |
纳税人识别号(15–20 位大写字母数字) |
date |
日期 YYYY-MM-DD |
postcode |
邮政编码(6 位) |
url |
URL |
空值处理约定:正则与枚举规则跳过空值,空值问题统一由 --required 报告。这样同一条空值不会被重复计入多条规则的失败数,汇总表的「不通过」列才是准确的。
第四步:生成对账结论(summary)
对账明细行数多时,用 summary 生成一段人类可读的结论,可直接粘进报告:
python scripts/reconcile_toolkit.py summary out.xlsx conclusion.txt
读哪个 sheet 由工具自己判断:recon 写出的 xlsx 有两个 sheet,「对账汇总」在前、「对账明细」在后。summary 会自动定位明细表。若你显式指定了汇总表,它会直接报错——汇总表一行代表一种差异类型、不是一条记录,按它统计会得出错误的结论(曾经就是这样:明细 6 条被算成 4 条、匹配 2 条被算成 1 条,金额合计两行直接消失,而输出看起来并无异常)。也可以直接传 --issues-out 产出的明细文件。
在 examples/data/ 的样例数据上实际运行,输出如下:
对账结论汇总
============================================================
数据来源: out.xlsx
明细记录: 6 条
差异分布:
匹配 2 条 33.3%
金额不符 1 条 16.7%
仅左表有 1 条 16.7%
仅右表有 1 条 16.7%
右表键重复 1 条 16.7%
匹配率: 33.3%
待处理差异: 4 条
差异金额合计(绝对值): 1,450.00
最大单笔差异: 800.00
结论: 存在差异, 详见明细 sheet 的问题清单。
第五步:核对结果
每个子命令执行后都会打印统计表与结论,必须检查是否符合预期。报告为 xlsx 时含多个 sheet,可直接作为交付物。
输出说明
xlsx 报告为多 sheet 结构:
| 子命令 | sheet | 内容 |
|---|---|---|
recon |
对账汇总 | 各差异类型的记录数与占比 |
| 对账明细 | 每个键一行:键 / 差异类型 / 各比对列的 列名(左) 与 列名(右) / 差异列 / 差额 / 说明 |
|
rule |
校验汇总 | 每条规则一行:校验行数 / 通过 / 不通过 / 通过率 |
| 问题明细 | 每个问题一行:行号 / 列名 / 规则 / 问题 / 当前值 / 严重级别 |
样式:表头绿底白字加粗、冻结首行、列宽自适应;对账明细的「差异类型」列会条件着色——匹配为绿底,其余差异为红底,便于一眼定位问题行。
行号约定:问题明细中的「行号」是面向 Excel 的行号(含表头,即数据第一行为第 2 行),可直接在 Excel 中定位。
非 xlsx 目标:输出为 .csv 等格式时,多 sheet 会退化为「每个 sheet 一个同名文件」(如传 out.csv 会生成 out_对账汇总.csv 与 out_对账明细.csv),保证结果不丢。命令会逐个打印真实生成的文件路径——传入的 out.csv 本身不会被创建,按它去找文件是找不到的。
关键约定
- 参数顺序:多值选项(
--dtype、--range、--regex、--enum、--format、--compare)会连续吞掉后面的参数,因此必须写在 input 和 output 两个位置参数都出现之后(即命令末尾)。 - 含
|或>的参数需要加引号:--enum "状态=已付款|待付款"、--compare "期末余额>=期初余额"。不加引号时 shell 会把|当管道、>当重定向,导致参数被截断。这是最高频的用法错误。 - 编码:读取 CSV 时自动按 utf-8-sig → gbk → utf-8 顺序尝试;写出 CSV 默认 utf-8-sig(带 BOM,Excel 打开中文不乱码)。
- 金额格式容错:比对时自动识别千分位逗号、货币符号(
¥¥$)、全角空格、百分号,以及(500)/(500)形式的负数。 - 自动化的退出码:加
--fail-on-issue(rule)或--fail-on-diff(recon),发现问题时以退出码 2 结束,便于接入定时任务或 CI 判定;正常结束为 0。 - 输出覆盖保护:输出文件与任一输入文件同名时自动拦截并报错;确需覆盖时显式加
--force。 - 大数据量:百万行级的键匹配与规则校验在常规内存下可正常处理;超宽表(数千列)的跨列比较会较慢,建议只声明必要规则。
- 金额精度:内部用浮点数比对。涉及高精度金额(如多位小数)时,建议显式设置
--tolerance(如0.01)而非依赖精确相等。 - 金额列在报告中保持数值格式:
金额(左)/金额(右)/差额写入 Excel 时是数字而非文本,可直接用SUM()求和或做条件格式。 - 键规范化默认关闭:前后空格与浮点
.0是基础归一(始终生效);大小写、全角、内部空白、零宽字符需显式开启--normalize-keys。宁可多报差异让人工确认,也不擅自合并键。 - 键列名不同不用先改表头:
--on 订单号:单号即可,语法与--compare一致。多列联合键中不带冒号的列按同名处理。 --aggregate有额外开销:该模式需按键分组,百万行量级会比普通模式慢(分组是 Python 侧循环)。数据量很大时建议先确认是否真的需要汇总,或先转 CSV/Parquet。
性能与规模
实测数据(Windows / Python 3.13 / pandas 3.0.5,数据为 6 列业务表,
左表与右表各约 N 行,90% 匹配、5% 金额不符、5% 单边)。
表中耗时均为纯计算时间,不含 xlsx 读写开销。
| 规模 | 规则校验 | 两表对账 | 峰值内存 |
|---|---|---|---|
| 1 万行 | 0.7 s | 0.3 s | < 10 MB |
| 10 万行 | 7 s | 2 s | 64 MB |
| 100 万行 | 约 70 s | 约 19 s | 约 600 MB |
结论与建议:
- 10 万行以内可放心直用,无需额外配置。
- 10 万行以上请装
python-calamine(xlsx 读取快约 6 倍)。 - 超过 100 万行建议先把 xlsx 转成 CSV 或 Parquet 再处理:
文本格式读取比 xlsx 快约 35 倍(同数据 CSV 0.2 s vs xlsx 7.7 s)。 - 对账结果会全量载入内存后再写盘。若内存吃紧,可用
--diff-only
只输出差异行,显著减小输出体积。 - 超长文本字段(如几十万字符的备注)写入报告时自动截断为 200 字符并标注原长度,
避免报告被单行撑到无法阅读。
权限与安全
本技能完全在本机离线运行,不涉及任何数据外传。
文件访问
- 只读写用户显式指定的输入 / 输出路径,不扫描、不遍历无关目录,不递归任何目录。
- 输出文件与任一输入文件同名时默认拦截并报错,防止误覆盖原文件;确需原地覆盖必须显式加
--force。 - 不写入系统目录、不修改注册表与环境变量、不改动用户主目录下的任何配置文件。
网络
- 代码中无任何网络请求,不调用外部 API,不上传任何数据。
- 所有依赖均为本地 Python 库:安装时需要联网,运行时完全不需要。
数据处理边界
- 数据密级:适用于 ≤L2(一般内部数据)。请勿用于 L3/L4 敏感数据,如未脱敏的个人身份信息、密钥凭证、涉密财务数据等。
- 脚本不做脱敏、不做数据留存:处理结果只落在用户指定的输出文件中,不产生额外副本。
- 对账报告会把两表的键与金额写入输出文件,输出文件本身应按原数据的密级管理。
- 处理含个人信息的表格时,建议先脱敏再交给本技能。
依赖加载
- 核心依赖
pandas/openpyxl必须安装才能使用。 - 可选依赖
python-calamine/pyyaml/pyarrow/xlrd/pytest按需惰性导入,仅在使用对应功能时才加载;未安装会在执行前给出明确报错和安装命令,不会运行到一半才失败,也不会静默降级。 - 不自动安装任何依赖,安装动作始终由用户发起。
执行行为
- 不执行表格单元格中的任何内容。规则文件中的正则表达式会被编译执行,但仅用于字符串匹配(
re.fullmatch),不构成代码执行路径;请只使用可信来源的规则文件。 - 不使用
eval/exec/os.system/subprocess,不存在任意代码执行路径。 - 不修改输入文件(输出覆盖已有防呆拦截)。
与配套技能的关系
本技能可独立运行,不依赖任何其他技能。 对账键的格式差异(大小写、全角半角、空白、零宽字符)与一对多核销都已内置,不需要借助外部工具做前置处理。
同生态的 excel-toolkit 负责数据变换(清洗、格式转换、透视聚合、多表合并),与本技能的数据判定互补,属于可选增强:
| 需求 | 本技能 | 需 excel-toolkit 辅助 |
|---|---|---|
| 两表勾稽对账 | ✓ recon |
— |
| 单表规则校验 | ✓ rule |
— |
| 对账结论输出 | ✓ summary |
— |
| 多列比对与差异列定位 | ✓ recon --compare A,B |
— |
| 大小写 / 全角 / 空白 / 零宽字符规范化 | ✓ recon --normalize-keys |
— |
| 一对多核销(同键先汇总再对) | ✓ recon --aggregate |
— |
| 对账前的深度清洗(拆列、跨表合并、复杂正则替换) | — | clean / merge |
| 大表转 CSV / Parquet 提速 | — | convert |
两者的报告样式与 CLI 风格刻意保持一致,但独立打包、独立安装,装了哪一个都不影响本技能运行。跨技能调用的写法与路径约定见 references/recipes.md 第 4 节。
进阶问题排查
遇到复杂情况(多级表头、异名列对账、重复键处理策略、大文件性能、编码问题等)时,查阅 references/recipes.md 中的场景化解决方案,不要凭记忆猜测 pandas 行为。