2026-10-07 · 喵窝资源铺
场景:活动收报名费,报名表 12 人,微信/支付宝导出的付款记录 13 条。人工对账的噩梦全在这:陈大壮报了名一直没付、钱多多没报名却转了 300、李四少付 20、赵六手滑重复付了两次,最阴的是有人付款时把名字打错——「王小明」写成「王二明」,这笔钱就人间蒸发了。这篇给一个脚本(signup_pay_recon.py):报名表和付款表一起喂进去,同名 + 手机号兜底 + 形近字模糊三层对齐,一人多笔付款自动合并计总额(补差价不会误报),输出「报了没付 / 金额不符 / 付了没报名 / 核对通过」四张名单。代码实跑验证,真实输出见下。
先 pip install pandas openpyxl。报名表列:姓名 / 手机号 / 报名费(元);付款表列:姓名 / 付款时间 / 金额(元)(付款表没有手机号列也能跑,手机号兜底层自动跳过)。运行 python signup_pay_recon.py 报名.xlsx 付款.xlsx 对账结果.xlsx:
# -*- coding: utf-8 -*-
"""报名表×付款记录对账:报了没付 / 付了没报 / 金额对不上。
用法 python signup_pay_recon.py 报名.xlsx 付款.xlsx out.xlsx"""
import sys
import pandas as pd
from difflib import SequenceMatcher
def norm_name(s):
return str(s).replace(" ", "").replace(" ", "").strip()
def sim(a, b):
return SequenceMatcher(None, a, b).ratio()
def fuzzy_th(a, b):
"""阈值按名字长度分层:错一个字的相似度是 (n-1)/n。
两字名错一字=0.5,三字名错一字=0.667(固定 0.75 会全漏!)。"""
n = max(len(a), len(b))
return 0.5 if n <= 2 else (n - 1) / n - 1e-9
def main():
signup_fp, pay_fp, out_fp = sys.argv[1], sys.argv[2], sys.argv[3]
# 手机号必须 str,否则 1.38e10 科学计数法对不上(见 roster 文的坑)
su = pd.read_excel(signup_fp, dtype={"手机号": str})
pay = pd.read_excel(pay_fp)
su["_nname"] = su["姓名"].map(norm_name)
pay["_nname"] = pay["姓名"].map(norm_name)
pay["_matched"] = False
pay["_tag"] = ""
# ---- 三层对齐: 同名(合并同一人所有付款) -> 手机号 -> 形近字模糊 ----
assign = {} # 报名行 idx -> list(pay行 idx)
# 1) 精确同名:一人可能付多次,同名全部算给他
for i, r in su.iterrows():
hits = pay[(~pay["_matched"]) & (pay["_nname"] == r["_nname"])].index.tolist()
if hits:
assign[i] = hits
pay.loc[hits, ["_matched", "_tag"]] = [True, "同名"]
# 2) 手机号兜底
for i, r in su.iterrows():
if i in assign:
continue
ph = r.get("手机号")
if isinstance(ph, str) and ph.strip() and "手机号" in pay.columns:
hits = pay[(~pay["_matched"]) & (pay["手机号"].astype(str) == ph)].index.tolist()
if hits:
assign[i] = hits
pay.loc[hits, ["_matched", "_tag"]] = [True, "手机号"]
# 3) 形近字模糊(阈值按名字长度分层),只对还没匹配上的
n_fuzzy = 0
for i, r in su.iterrows():
if i in assign:
continue
hits = pay[(~pay["_matched"]) &
pay["_nname"].map(lambda x: sim(x, r["_nname"]) >= fuzzy_th(x, r["_nname"]))].index.tolist()
if hits:
assign[i] = hits
pay.loc[hits, ["_matched", "_tag"]] = [True, f"模糊({r['_nname']}≈{pay.loc[hits[0],'_nname']})"]
n_fuzzy += 1
# ---- 输出四类名单 ----
not_paid, mismatch, matched_rows = [], [], []
for i, r in su.iterrows():
hits = assign.get(i, [])
fee = r.get("报名费(元)")
if not hits:
# 报了没付:付款侧剩余人名里找嫌疑错字,提示人工确认
susp = [p for p in pay[~pay["_matched"]]["_nname"]
if sim(p, r["_nname"]) >= fuzzy_th(p, r["_nname"]) - 0.15]
note = "未匹配付款记录" + (f";付款表疑似有「{susp[0]}」请人工确认" if susp else "")
not_paid.append({"姓名": r["姓名"], "手机号": r.get("手机号", ""),
"应付": fee, "备注": note})
continue
total = sum(float(pay.loc[j, "金额(元)"]) for j in hits)
fee = float(fee)
if abs(total - fee) > 0.001:
mismatch.append({"姓名": r["姓名"], "应付": fee, "实付": total,
"差额": round(total - fee, 2),
"明细": " + ".join(f"{pay.loc[j,'金额(元)']}元@{pay.loc[j,'付款时间']}" for j in hits)})
matched_rows.append({"姓名": r["姓名"], "匹配方式": pay.loc[hits[0], "_tag"],
"付款笔数": len(hits), "实付合计": total, "应付": fee})
extra = pay[~pay["_matched"]][["姓名", "付款时间", "金额(元)"]].to_dict("records")
with pd.ExcelWriter(out_fp, engine="openpyxl") as w:
pd.DataFrame(not_paid).to_excel(w, sheet_name="报了没付", index=False)
pd.DataFrame(mismatch).to_excel(w, sheet_name="金额不符", index=False)
pd.DataFrame(extra).to_excel(w, sheet_name="付了没报名", index=False)
pd.DataFrame(matched_rows).to_excel(w, sheet_name="核对通过", index=False)
# ---- 控制台摘要 ----
print(f"报名 {len(su)} 人 / 付款记录 {len(pay)} 条 -> {out_fp}")
print(f"对上 {len(assign)} 人(同名/手机号 {len(assign)-n_fuzzy},模糊 {n_fuzzy})")
for x in not_paid:
print(f" 报了没付: {x['姓名']} {x['备注']}")
for x in mismatch:
print(f" 金额不符: {x['姓名']} 应付{x['应付']:g} 实付{x['实付']:g} 差{x['差额']:g} ({x['明细']})")
for x in extra:
print(f" 付了没报名: {x['姓名']} {x['金额(元)']}元 @ {x['付款时间']}")
if __name__ == "__main__":
main()
测试数据埋了 7 类真实情况:正常付款×7、少付(李四 100 付 80)、补差价分两次付(王五 100+50=150)、重复付(赵六 100+100)、付款错字×2(王小明→王二明、杨十二→阳十二)、报了没付×2(陈大壮、刘十一)、没报名却付了(钱多多 300)。跑出来:
报名 12 人 / 付款记录 13 条 -> 对账结果.xlsx 对上 10 人(同名/手机号 8,模糊 2) --- 报了没付 2 人 --- 陈大壮 未匹配付款记录 刘十一 未匹配付款记录 --- 金额不符 2 人 --- 李四 应付100 实付80 差-20 (80元@10-01 10:00) 赵六 应付100 实付200 差100 (100元@10-01 12:00 + 100元@10-01 12:05) --- 付了没报名 1 条 --- 钱多多 300元 @ 10-02 10:00
逐项验收:陈大壮、刘十一精确落进「报了没付」;李四少付 20、赵六重复付差 +100(注意方向——差额 +100 是要退人家的,-20 才是要催的,两件事别搞混);钱多多的 300 落在「付了没报名」要联系退款;王五的补差价(100+50)合并后正好 150,没有误报;两个错字「王二明」「阳十二」都被模糊匹配抓回来并在核对通过表里标明(模糊(王小明≈王二明));「吴九 」的尾随空格被 norm_name 归一后正常同名匹配。幂等验证:同样输入连跑两次,输出的 xlsx 逐字节一致(cmp 通过)。
模糊匹配到的人(王小明≈王二明)建议拿手机号或付款截图跟本人确认一次再入账,脚本报「核对通过」≠ 钱一定对——它只保证「这笔钱大概率是这个人的」。另外付款表没有手机号列时手机号兜底层自动跳过,不影响其余两层。
懒得自己跑?找我们代写
喵窝资源铺提供 Python 脚本代写服务:你说清楚两张表的格式和核对规则,我们写好、用你的真实数据测试通过再交付,24 小时内出活。AI 服务,明码标价,先看效果再付款(虚拟物品不退款,不满意免费重做)。
👉 了解代写服务 | 联系我们
📖 相邻文章:报名名单×签到表核对 · 微信群AA收款对账 · 工资表按身份证号跨文件对账 · Excel对账脚本 · 两张表人名模糊匹配合并 · 博客目录
对账结果供财务核对参考,退款/催款等操作请以实际付款凭证为准。