微信群 AA 收款记录批量对账:谁没付、谁少付、谁发错了重转,一个脚本揪出来

2026-10-06 · 喵窝资源铺

场景:组织完聚餐/团建,要在群里收 AA 款。等收完一看聊天记录——「张伟 转账¥25.50」「已付25」「发错了重转」「红包¥8.88」混成一团,人对着屏幕数谁没交,数三遍数出三个结果。这篇给一个纯标准库的 Python 脚本(aa_collect.py):微信聊天记录整段复制进去存成 txt,自动按人对账,--expect 25.5 传入每人应付金额,直接输出「正常 / 少付 / 多付」名单。文中代码实跑验证,真实输出贴在下面。

它做的几件事

输入形态脚本的处理
「张伟 转账给你 ¥25.50」姓名 + 金额,正常付款
「已付 25」「付了 25.5」「30元」金额写法全兼容(¥/元/块/裸数字)
「发错了重转 ¥25.50」覆盖之前那笔,只认重转的(不是累加!)
「补了0.5 转账¥0.50」同人多笔累加,补转也算数
红包 ¥8.88不计入 AA,单独列出提醒
系统行(收款到账/退款/收款码/订单号)整行剔除,绝不当成某人付款
「该收每人25.5元」有金额但没付款动词,不算付款
同一个人转两笔金额累加,最后对账看总数
输出付款人数 + 合计 + 按 --expect 算差额点名

实跑:14 行混合消息 → 8 人对账全对

仿微信群记录 14 行:8 人付款 + 1 个红包 + 1 笔系统收款 + 1 笔退款 + 若干干扰:

付款 8 人,合计 ¥233.50
红包(不计入AA): 陈晨 ¥8.88

应付 ¥25.50/人 对账:
  正常 5 人: 张伟、李娜、赵敏、孙磊、吴刚
  少付 王强: 差 ¥0.50
  多付 刘洋: 多 ¥4.50(多退少补或记入下次)
  多付 郑雪: 多 ¥25.50(多退少补或记入下次)

逐行核对:王强「已付 25」少 5 毛是对的;刘洋备注「多退少补」转了 30 也对;郑雪替两个人转了 51 对——该进名单的一个不漏,一个不错。系统行、红包、退款全部剔除。

坑一:名字里带数字的人,名字被当金额咬掉一块

第一版先跑金额正则再取姓名。群昵称「2B青年 转账 ¥25.5」——金额正则从行首扫,先把名字里的 2 当成了金额 ¥2.00,真正的 25.5 反而丢了。

实测抓到后修复:先取姓名(必须含至少一个汉字或字母),金额只在姓名之后的剩余文本里找:

nm = re.match(r"([0-9]*[一-龥A-Za-z][一-龥A-Za-z0-9·'’_]*|...", line)
rest = line[nm.end():]        # 金额只在姓名之后找
m = MONEY.search(rest)

修复后「2B青年 转账 ¥25.5」正确输出 2B青年: ¥25.50。这个顺序问题在快递单号提取那篇也出现过(EMS 前缀被通用数字正则咬掉)——「先抓结构、再在结构内抓数字」是文本提取的通用铁律。

坑二:「该收每人25.5元」这种非付款行被当成转账

通知类消息「该收每人25.5元」里也有金额,第一版把它算成了一笔 ¥25.50 的付款,总额虚高。

正解:金额之外还要有付款动词(转账/已付/付了/已转账),没有动词的行不算:

PAY = re.compile(r"转账|已付|付了|已转账|已发")
if kind == "pay" and not PAY.search(rest):
    return None   # 有金额但不是付款语气 → 不算

坑三:同一人「发错了重转」,累加成双倍

张伟先转了 ¥10 发错,重转 ¥25.50。第一版同人累加算成 ¥35.50——多收人 10 块钱,群里直接社死。

这个坑和「补转」是反的:补转(¥25 + ¥0.5)要累加,重发(10 作废 + 25.5)要覆盖。靠文案特征区分:

RESEND = re.compile(r"发错|重转|重发|转错|作废|撤回")
elif RESEND.search(raw):
    paid[name] = amount          # 发错重发:新值覆盖旧值
else:
    paid[name] = paid.get(name, 0.0) + amount   # 补转:累加

实测:重发场景输出 ¥25.50(不是 35.50),补转场景「¥25 + ¥0.5」输出 ¥25.50 全对。

坑四:系统行被当成某人付款

「收款到账 ¥25.50」「转账退款 ¥25.50 原路退回」「收款码收款 ¥25.50」这些系统消息都带金额,没有姓名也照算,账目直接乱掉。

正解:系统特征词出现整行剔除,优先级最高:

SYS = re.compile(r"收款到账|收款码|微信支付|订单号|退款|已存入|零钱明细")

实测 4 行系统消息 + 1 行真人付款,只出 1 人 ¥25.50。

坑五:红包混进 AA 总额

「陈晨 抢了你的红包 ¥8.88」被当成了陈晨付的 AA 款——但他明明只是抢了个红包。

正解:红包行单独归类,不计入付款,输出里单独提醒让你自己判断(有时候群主会发红包抵一部分,这是你的决定不是脚本的)。

坑六:备注里的数字差点被当金额

「张伟 转账给你 ¥25.50 备注:3号桌302」——如果金额正则匹配范围不限制,「302」有机会被卷进来(尤其是 ¥ 符号缺失时)。

修复方式和坑一同源:金额搜索限定在姓名之后的文本、优先匹配带 ¥/元/块特征的数字。实测备注带桌号、房号都正确取到 ¥25.50。

边界测试一览(实跑输出)

输入片段结果说明
2B青年 转账 ¥25.52B青年 ¥25.50坑一修复后名字数字不干扰
O'Neil 转账给你 ¥25.50O'Neil ¥25.50英文名/撇号兼容
该收每人25.5元(排除)非付款行
张伟 发错了重转 ¥25.50覆盖为 ¥25.50坑三:不累加
周婷 补了0.5 转账¥0.50累加 ¥25.50补转算数
收款到账 ¥25.50 / 退款 / 收款码(排除)坑四:系统行
陈晨 抢了你的红包 ¥8.88红包单独列出坑五
转账给你 ¥25.50 备注:3号桌302¥25.50坑六:备注数字不干扰

幂等性:提取结果回灌再跑,结果一致

first pass: 付款 8 人 合计 ¥233.50
second pass(回灌): 付款 8 人 合计 ¥233.50  identical: True

把脚本认出的 9 行付款消息原样回灌再跑一遍,人数、合计、差额名单完全一致——对账结果可复现,不是碰运气。

使用

python aa_collect.py 聊天记录.txt --expect 25.5
# 不传 --expect 则只汇总每人付款金额,自己心算也行

纯标准库,Python 3.8+ 直接跑。金额口径注意:脚本认最后一次到账的实付金额,收了红包抵扣、线下现金这种聊天记录里看不出来的,还是要人脑补一刀——脚本点名,你来判断。

完整脚本

#!/usr/bin/env python3
"""aa_collect.py — 从微信群聊天记录里批量核对 AA 收款
用法: python aa_collect.py 聊天记录.txt [--expect 25.5]
输出: 终端对账报告(未付/多付/少付名单 + 合计)
规则:
  - 只认「人名 + 金额」的付款行;系统行(到账/退款/收款码/订单号)剔除
  - 红包不计入 AA 付款,单独列出提醒
  - 金额口径:支持 ¥25.50 / 25.5 / 25 / 30元 多种写法
  - --expect 传入每人应付金额,自动算差额;不给则只汇总
"""
import re
import sys
from collections import OrderedDict

MONEY = re.compile(r"([0-9]+(?:\.[0-9]{1,2})?)\s*(?:元|块)?")
# 系统行特征:出现即整行剔除,不参与付款判断
SYS = re.compile(r"收款到账|收款码|微信支付|订单号|退款|已存入|零钱明细|转账给\s*\S+\s*¥?0\.01")
HONGBAO = re.compile(r"红包")
# 「发错重发」特征:出现则同人金额取新值覆盖,而不是累加
RESEND = re.compile(r"发错|重转|重发|转错|作废|撤回")
# 付款动词:这行像「某人付了钱」
PAY = re.compile(r"转账|已付|付了|已转账|已发")

def parse_line(line):
    """返回 (姓名, 金额, 类型) 或 None。类型: pay / hongbao"""
    line = line.strip()
    if not line or SYS.search(line):
        return None
    # 姓名:必须含至少一个汉字/字母(纯数字开头后面也要落到汉字),防止把「2B青年」的 2 当金额
    nm = re.match(r"([0-9]*[\u4e00-\u9fa5A-Za-z][\u4e00-\u9fa5A-Za-z0-9·'’_]*|[A-Za-z][A-Za-z0-9'’ _]{0,14}[A-Za-z])", line)
    if not nm:
        return None
    name = nm.group(1).strip()
    rest = line[nm.end():]          # 金额只在姓名之后的剩余文本里找,备注里的数字、名字里的数字都咬不到
    m = MONEY.search(rest)
    if not m:
        return None
    amount = float(m.group(1))
    if amount <= 0:
        return None
    kind = "hongbao" if HONGBAO.search(line) else "pay"
    if kind == "pay" and not PAY.search(line):
        return None   # 有金额但不是付款语气(比如「该收 25.5」)→ 不算
    return (name, amount, kind)

def main():
    args = sys.argv[1:]
    expect = None
    if "--expect" in args:
        i = args.index("--expect")
        expect = float(args[i + 1])
        del args[i:i + 2]
    if not args:
        sys.exit("用法: python aa_collect.py 聊天记录.txt [--expect 25.5]")
    paid = OrderedDict()      # name -> amount(同一人多次转账取最后一次,聊天里常见「发错了重发」)
    hongbao = {}
    for raw in open(args[0], encoding="utf-8"):
        got = parse_line(raw)
        if not got:
            continue
        name, amount, kind = got
        if kind == "hongbao":
            hongbao[name] = amount
        elif RESEND.search(raw):
            paid[name] = amount          # 发错重发:新值覆盖旧值,10块作废只认重转的25.5
        else:
            paid[name] = paid.get(name, 0.0) + amount   # 补转/分两笔:累加
    total = sum(paid.values())
    print(f"付款 {len(paid)} 人,合计 ¥{total:.2f}")
    if hongbao:
        print(f"红包(不计入AA): " + ", ".join(f"{n} ¥{a:.2f}" for n, a in hongbao.items()))
    if expect is None:
        for n, a in paid.items():
            print(f"  {n}: ¥{a:.2f}")
        return
    ok, less, more = [], [], []
    for n, a in paid.items():
        d = round(a - expect, 2)
        if abs(d) < 0.01:
            ok.append(n)
        elif d < 0:
            less.append((n, -d))
        else:
            more.append((n, d))
    print(f"\n应付 ¥{expect:.2f}/人 对账:")
    print(f"  正常 {len(ok)} 人: {'、'.join(ok) if ok else '—'}")
    for n, d in less:
        print(f"  少付 {n}: 差 ¥{d:.2f}")
    for n, d in more:
        print(f"  多付 {n}: 多 ¥{d:.2f}(多退少补或记入下次)")

if __name__ == "__main__":
    main()

喵窝资源铺提供代跑脚本服务:你把聊天记录发过来,跑完把对账名单发回你,¥5 起,虚拟物品不退款(跑不通不收费)。脚本本身免费自取。

📖 相邻文章:微信群接龙/报名消息一键变Excel · 聊天记录批量提取快递单号 · Excel 批量提取手机号 · 博客目录

对账结果仅供参考,涉及金额分歧请以微信/支付宝官方账单为准。