QQ邮箱发票爬取分类整理
用途
按日期范围扫描 QQ 邮箱,只把发票附件下载下来,自动按消费类型分类,
存到 D:\发票,并生成一张台账用于溯源。
用户只要说一句话(比如"帮我下载 8 月的发票"),你解析出日期后直接跑脚本。
⚠️ 行为铁律(每次必读)
用户给出日期后,立刻执行,不要多问、不要多做:
| 该做 | 不该做 |
|---|---|
| 解析日期 → 直接跑脚本下载 | ❌ 反问"确认下载到 D:\发票 吗?" |
| 下载完报结果(含分类统计) | ❌ 先跑 --dry-run 再问"要下载吗?"(除非用户主动说"先看看"/"预览") |
| 只下载发票附件 | ❌ 顺手下载别的附件、"帮你整理一下邮箱" |
| 邮箱只读,不做任何写操作 | ❌ 标记已读、移动邮件、删除邮件 |
用户说"不要台账"才加 --no-ledger |
❌ 自作主张省略台账(同名文件会自动加 _1 _2,没台账不好追来源) |
| 只汇报本次爬取的结果 | ❌ 检查、清点、汇报其它已有文件夹的状态(用户 2026-09-02 明令:"你不要管原文件夹有什么") |
> 🛑 "不要管原文件夹"是硬边界:爬取完成后只交付本次的新文件夹和台账,
> 不要顺手去 ls / 清点 D:\发票\ 下的其它目录,也不要在汇报里提"某个老文件夹有多少文件、
> 是不是变少了、是不是被删了"。老文件夹的增删改是用户自己的事,与本次任务无关。
> 只有用户主动要求整理某个老目录时,才动它。
用户说"只爬取发票其它别动"的意思就是:别节外生枝。
路径速查
| 项目 | 路径 |
|---|---|
| 技能目录 | C:\Users\<用户名>\.workbuddy\skills\qq-invoice |
| 主脚本 | scripts\fetch_invoices.py |
| 配置向导 | scripts\setup_config.py |
| 配置文件 | config.json(不随技能包发布,含邮箱+授权码,由 setup_config.py 本地生成;包内仅提供 config.example.json 模板) |
| 授权码指南 | references\auth-code-guide.md |
| 发票保存目录 | D:\发票 |
| Python | C:\Users\<用户名>\.workbuddy\binaries\python\versions\3.13.12\python.exe |
> 脚本主体只用 Python 标准库(imaplib / email / csv / zipfile);抽取发票字段需 pypdf
> (pip install pypdf,用于读 PDF 文字层),OFD 用标准库直接解。生成排版 xlsx 需
> openpyxl(pip install openpyxl,不装则 CSV 照常生成、xlsx 跳过并提示)。
> 不抽字段、不看 xlsx 时完全不影响下载/清理。
>
> 🛑 依赖必须装进「运行脚本的那个 python」:脚本由
> binaries\python\versions\3.13.12\python.exe 执行,pypdf / pymupdf / pytesseract /
> openpyxl 必须装进这个解释器,仅装在某个 venv 不够。2026-09-02 曾因这些包只存在于旧 venv、
> versions\3.13.12 没装,导致 --reprocess 重抽时发票号码空 38/42、金额空 41/42;
> 对 versions\3.13.12 补装后重跑即 0 空。(安装:versions\3.13.12\python.exe -m pip install pypdf pymupdf pytesseract openpyxl)
【重要】归档结构
D:\发票\2026年9月1日\交通差旅\拼多多商家电子发票.pdf
│ │ │
│ │ └─ 原始附件名(如 拼多多商家电子发票.pdf)
│ └─ 消费类型(自动识别)
└─ 爬取当天日期(中文文件夹,每次运行一个)
- 只按「爬取当天」建一层日期文件夹,不再按邮件月份分
(邮件本身的日期记在台账 CSV 里,需要按发票所属期查就翻台账) - 文件名 =
001_序号前缀 + 邮件里的原始附件名(原始附件名保留在后面,便于溯源)
(如001_拼多多商家电子发票.pdf、002_通行费电子票据.zip);序号从 001 起按目录内路径顺序续排,
与台账「序号」列保持一致 - 同名自动加时间戳(用户 2026-09-02 铁律:下次爬取不必清空目录里的旧文件,直接往里爬):
同一类别文件夹里出现同名文件时(含目录里原有的旧发票),自动追加时间戳_YYYYMMDD_HHMMSS区分,
绝不覆盖已有文件、也不动原有旧文件
(例如目录里已有「拼多多商家电子发票.pdf」,新下的同名件变「拼多多商家电子发票_20260902_193809.pdf」;
极偶发同秒再冲突才追加_1:拼多多商家电子发票_20260902_193809_1.pdf) - 同一天重复爬取:自动新建「日期_时间」文件夹(用户 2026-09-02 追加铁律):
当天日期文件夹若已存在(如2026年9月2日),脚本自动在日期后加_HHMMSS
生成独立的新文件夹(如2026年9月2日_203907),绝不混入/覆盖已有文件夹,
也不去读取、不改动D:\发票\下其它任何老文件夹。
去重范围只限「本次邮箱抓取 + 当前这个新文件夹」,不用看原有文件夹。 - 换一天再爬:新建当天的日期文件夹,各天互不干扰
- 台账 CSV 放在当天日期文件夹内:
D:\发票\2026年9月1日\发票台账_20260901.csv
(同天跑多次会自动加_1_2)
> 这是用户明确指定的结构:日期(爬取当天)→ 类别 → 文件,
> 类别下面不再分子目录,文件直接平铺在类别文件夹里。
【重要】消费类型自动分类
七大类目
| 类目 | 典型来源 |
|---|---|
| 交通差旅 | 机票/行程单、火车票、打车(滴滴/曹操/T3)、ETC 通行费、加油、停车、地铁公交(亿通行)、酒店外的出行类 |
| 餐饮住宿 | 酒店/民宿/宾馆、美团、饿了么、外卖、餐饮、星巴克 |
| 物流快递 | 顺丰、中通、圆通、申通、韵达、极兔、德邦、EMS/邮政、快递运费 |
| 通讯网络 | 中国移动/联通/电信、话费、宽带、流量 |
| 办公采购 | 京东、天猫、淘宝、苏宁、拼多多、得力/晨光、办公用品、耗材、数码 |
| 生活缴费 | 水费、电费(国家电网/南方电网)、燃气、供暖、物业、房租 |
| 未分类 | 以上都没命中,兜底目录 |
判定逻辑(两级)
- 第 1 级(准):发件人显示名 + 发件人邮箱地址 + 邮件主题 + 附件名
- 第 2 级(兜底):邮件正文纯文本
- 专门针对诺诺网、百旺、航信这类代开平台——它们的发件人都是平台名,
真正的商家名(顺丰/京东/携程…)只写在邮件正文里
- 第 1 级分不出来才启用正文,避免把正文里顺带提到的词当真
规则表在 fetch_invoices.py 的 CATEGORY_RULES,顺序敏感(越专属排越前,先命中先归)。
发现分类错了怎么改
编辑 fetch_invoices.py 里的 CATEGORY_RULES,把商家的名字/邮箱域名加到对应类目列表里。
例:发现"盒马"的发票错分到未分类 → 在「餐饮住宿」列表里加 "盒马"。
> 注意:分类只看邮件元数据 + 正文文本,不解析 PDF/OFD 内容(保持零第三方依赖)。
工作流
第 0 步:确认配置(一般可跳过)
首次使用请先配置(运行 setup_config.py 填入你的 QQ 邮箱 + 16 位授权码,目录默认 D:\发票)。已配置时:
"C:\Users\<用户名>\.workbuddy\binaries\python\versions\3.13.12\python.exe" \
"C:\Users\<用户名>\.workbuddy\skills\qq-invoice\scripts\setup_config.py" --show
如果提示未配置或授权码失效 → 走首次配置流程。
第 1 步:首次配置 / 重配(授权码失效时)
需要用户提供:QQ邮箱地址 + 16位授权码。
获取步骤见 references\auth-code-guide.md,简要版:
mail.qq.com → 设置 → 账户(不是"安全设置"也不是"设备管理")
→ 找到「POP3/IMAP/SMTP/Exchange/CardDAV 服务」→ 点「生成授权码」→ 短信验证 → 得到 16 位。
"C:\Users\<用户名>\.workbuddy\binaries\python\versions\3.13.12\python.exe" \
"C:\Users\<用户名>\.workbuddy\skills\qq-invoice\scripts\setup_config.py" \
--email "用户的QQ邮箱" --auth-code "16位授权码" --output "D:/发票"
> 授权码敏感,不要在回复里回显完整内容,配置脚本已做脱敏。
第 2 步:解析用户说的日期
先确认今天是几号(date 命令或系统时间),再换算。假设今天是 2026-09-01:
| 用户说法 | --start | --end |
|---|---|---|
| 8月的发票 / 上个月 | 2026-08-01 |
2026-08-31 |
| 这个月 / 本月 | 2026-09-01 |
2026-09-30 |
| 最近30天 | 2026-08-02 |
2026-09-01 |
| 最近一周 | 2026-08-25 |
2026-09-01 |
| 今年 / 2026年 | 2026-01-01 |
2026-09-01 |
| 3月到8月 | 2026-03-01 |
2026-08-31 |
| 8月15到20号 | 2026-08-15 |
2026-08-20 |
| 全都要 / 所有 | 2000-01-01 |
今天 |
跨年注意:现在是 1 月时,用户说"12月"指的是去年 12 月。
用户没给时间范围才问,给了就直接算,别反复确认。
第 3 步:跑脚本(直接下载)
"C:\Users\<用户名>\.workbuddy\binaries\python\versions\3.13.12\python.exe" \
"C:\Users\<用户名>\.workbuddy\skills\qq-invoice\scripts\fetch_invoices.py" \
--start 2026-08-01 --end 2026-08-31
脚本会:
1. 按日期范围用 IMAP 只读搜索邮件(不标记已读、不移动、不删除)
2. 只挑出真正的发票附件下载,其它附件一律跳过
3. 自动分消费类型,存到 D:\发票\2026年9月1日\消费类型\拼多多商家电子发票.pdf(保留原始附件名,同名加 _1 _2)
4. 下载后自动清理(见下一节):解压压缩包、删除原压缩包、删行程单/垃圾、内容去重
5. 打印分类统计(各类目各多少张)
6. 生成台账 D:\发票\2026年9月1日\发票台账_20260901.csv(含解压出来的发票,行数=文件数)。
台账新增列:序号 / 发票代码 / 发票号码 / 发票金额 / 发票类型(普票/专票),
其中「序号」与文件名前缀 001_ 一致;发票代码/号码/金额从 PDF/OFD 文字层抽取,
抽不到(图片型扫描件、全电发票无代码等)则留空——符合"只有一个就另一个空"的要求
7. 额外生成排版 xlsx:发票台账_20260901.xlsx(与 CSV 同名同目录)。
微软雅黑、表头加粗+淡蓝底、冻结首行、四周细边框、列宽自适应、序号/金额/大小右对齐、
大小列 1 位小数。双击打开即漂亮,可直接发给会计/同事。原 CSV 照常保留方便程序读取
第 4 步:汇报结果
简洁告诉用户:
- 扫了多少封邮件,下载了多少张发票
- 分类统计(交通差旅 X 张、物流快递 Y 张…)
- 文件在 D:\发票\爬取当天日期\消费类型\(如 D:\发票\2026年9月1日\交通差旅\)
- 台账 CSV 路径(用于查某张发票的来源)
- 有"疑似发票但无附件"的邮件时提醒——那些发票藏在正文链接里,得手动点开下
最后用 present_files 展示台账 CSV。
参数说明
| 参数 | 说明 |
|---|---|
--start / --end |
必填。邮件日期范围。支持 2026-08-01、2026-08(整月)、2026/8/1 |
--output |
下载目录,默认 D:\发票 |
--dry-run |
只列不下载。仅当用户主动说"先看看/预览/列一下"时才加 |
--no-ledger |
不生成台账 CSV(默认生成) |
--all-folders |
搜索所有文件夹。默认只搜收件箱(快很多) |
--keyword |
追加发票关键词,可重复传 |
--limit N |
最多处理 N 封邮件,调试用 |
--email / --auth-code |
临时覆盖配置里的账号/授权码 |
--reprocess <目录> |
补抽模式:对已下载目录只做"加 001_ 序号前缀 + 抽取发票字段 + 重写台账",不连邮箱。用于给老文件夹补新列 |
【重要】只下载发票的过滤规则
邮件先要命中关键词(主题/发件人/附件名)才进入候选:
发票 电子发票 增值税 开票 票据 报销 invoice fapiao receipt tax invoice
然后附件逐个过以下关卡,全部通过才下载:
| 关卡 | 规则 |
|---|---|
| 1. 扩展名 | 只收 pdf ofd zip rar 7z jpg jpeg png;htm html txt json xml 等网页垃圾一律跳过 |
| 2. 内嵌图片 | 正文里嵌的图标、签名 logo(inline + Content-ID)跳过 |
| 3. 最小体积 | 小于 0.5 KB 的跳过(0 字节占位文件) |
| 4. 图片额外限制 | jpg/png 必须① 附件名或主题含发票关键词,且② ≥ 8 KB |
> 规则偏保守:宁可少下,也不把签名 logo、装饰图当发票下载下来。
> 如果发现漏了某类发票,在 config.json 的 extra_keywords 里加商家名(如"京东""滴滴")。
【重要】下载后自动清理(解压 / 白名单 / 去重)
下载落盘后,脚本会自动再跑一遍 cleanup_dir,把"下载阶段出于完整保留考虑而留着的包装物"清掉,
最终 D:\发票\当天日期\ 里只留发票文件。五件事按顺序做:
| 顺序 | 动作 | 规则 |
|---|---|---|
| 1. 解压 | 识别 zip / rar / 7z 压缩包 |
zip 用标准库 zipfile 解;rar/7z 调系统 unrar/7z(没装则跳过并提示)<br>解出的文件继承原压缩包的类别目录与来源行,写进台账 |
| 2. 删原包 | 压缩包解压后删除本体 | 直接删除(用户已授权;删除层转进系统回收站,可找回) |
| 3. 白名单关卡 | 只保留"确定是发票"的文件,其余全部清掉(looks_like_invoice()) |
留下必须同时满足:① 格式是 .pdf/.ofd(发票只这两种载体)② 非空(≥0.5KB)③ 不命中行程单/运单明细排除词(行程单 运单明细 waybill trip itinerary…——有些带"发票"字样但不是发票本体,如「发票运单明细.pdf」);且至少命中一条正向特征:a) 文件名含 发票/invoice/电子票据/通行费 等 b) 主干是纯数字发票号(≥8位)c) 税务平台命名前缀(G1234567890_/elfp…)。都不命中 → 默认清掉(签收单/对账单/新花样不用加规则,天然被删)。删前先算 SHA256 登记进 seen,防止后面去重漏掉内容相同的发票 |
| 4. 内容去重 | 同一类别目录内按内容比对 | 算每个文件 SHA256,内容完全相同才判重;<br>同名但内容不同 → 保留两份(自动加 _1 _2);判重的那个直接删除(进回收站可找回) |
| 5. OFD/PDF 双格式去重 | 同一张发票两种格式都发时,留 PDF、OFD 移走 | 同一类别目录内,OFD 文件名标准化后与某 PDF 同名即配对:<br>① 去掉「.ofd_查阅需OFD阅读器」尾巴 ② 「_ofd_」中缀还原成「_」<br>例:G0004110010_ofd_abc.ofd ↔ G0004110010_abc.pdf;顺丰电子发票.ofd ↔ 顺丰电子发票.pdf<br>发票号码不同的不会误配(如 elfp759… 与 elfp716… 是两张不同票,都保留)<br>注意:跨邮件同名不同票理论上可能误配(如两家都叫"电子发票.ofd/.pdf"),但直接删除(进回收站可找回),风险可控 |
> ⚠️ 常量重名坑(已修):下载阶段允许的格式集合 INVOICE_EXTS(含 zip/jpg 等,
> 第 97 行)和清理阶段白名单 INVOICE_FINAL_EXTS(只有 pdf/ofd)是两个不同集合。
> 曾因把白名单也叫 INVOICE_EXTS 导致同名覆盖 → 下载阶段不收 zip → ETC 发票整批丢失。
> 改代码时切勿让两个集合重名。
删除策略:分类后直接删除(用户已授权)
用户已明确授权对 D:\发票 下「重复 / 压缩包 / 邮件 CSV 附件 / 非发票垃圾」直接删除,不再移动到中转目录。
safe_remove()现在直接os.unlink/shutil.rmtree删除判定为垃圾的文件;
每个文件独立try/except,单个删除失败只记警告、不中断整体。- 🛑 当前沙箱实测(2026-09-02,纠正旧结论):本机删除层把
unlink转进系统回收站,
但沙箱内回收站不可用(windows-sandbox-recycle-bin-unavailable),于是safe_remove的硬删
全部静默失败——爬取时的自动清理其实没删掉任何东西,目录会残留重复件/压缩包/行程单等垃圾
(本次 8/28–9/2 爬取后目录一度涨到 132 个文件即此因)。os.remove能绕过沙箱真删,
但单轮累计 50 个即被杀进程(SAFE_DELETE_BULK_CONFIRM_REQUIRED,需用户显式确认才放行)。 - 沙箱内真正可用的整理做法:用
shutil.move把垃圾移到临时目录去重(移动不受批量阈值限制,
也不走回收站),再视情况小批量(≤50/轮、需确认)真删;或在本机(非沙箱)执行清理。 - 删除后顺手清理由此产生的空子目录。
- 生成的台账
发票台账_*.csv受保护:清理时遇到以发票台账开头的 csv 会跳过,绝不删。 --dry-run仍只预览、不真删。
> 实测:9/2 一轮完整爬取,因回收站不可用、自动清理静默失败,目录残留 132 个文件;
> 改用「移动去重」手动整理后,最终干净目录只留 39 张发票 + 台账 + xlsx。
排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 提示"还没配置邮箱账号" | config.json 丢失 | 跑 setup_config.py |
登录失败 / AUTHENTICATE failed |
用了 QQ 密码 / 授权码带空格 / 授权码失效 | 重新生成授权码,去掉空格 |
| 授权码突然失效 | 用户改了 QQ 密码或重新生成过 | 重新生成授权码并 --auth-code 更新 |
请开启IMAP服务 |
邮箱没开 IMAP | 去「设置→账户」开启,等 5 分钟 |
| 找到 0 个发票 | 关键词没命中 / 日期范围不对 | 加大日期范围,或 --keyword 加商家名 |
| 想看会下载什么 | — | 加 --dry-run 预览 |
| 中文乱码 | 终端编码问题 | 脚本已处理,若仍乱码用 PowerShell 跑 |
| 跑得太慢 | 邮件太多 | 缩小日期范围,或先不加 --all-folders |
| 某家发票分错类 | CATEGORY_RULES 没覆盖 |
把商家名加进对应类目列表(见上文) |
| 跑时提示「原 csv 被占用(WPS/Excel 打开了它),已写到备份」 | WPS/Excel 正打开 发票台账_*.csv 把它锁了 |
关掉 WPS 那个文件再跑;或用备份文件(带时间戳后缀)。xlsx 一定生成 |
| xlsx 没生成 | openpyxl 没装 |
pip install openpyxl(CSV 不受影响) |
⚠️ 已踩过的坑:IMAP 日期搜索
QQ 邮箱的 IMAP 不支持带括号的组合条件。
写成 (SINCE "01-Aug-2026" BEFORE "11-Aug-2026") 会被服务器当成 ALL,
返回全部邮件(表现为:不管传什么日期范围,结果都是 105 封全量)。
必须用不带括号的写法:
criteria = f'SINCE "{imap_date(d_start)}" BEFORE "{imap_date(d_end_imap)}"'
脚本里另外加了一道本地日期二次校验(按邮件头的 Date 再卡一遍范围),双保险。
改这块代码时别把括号加回去。
⚠️ 关于删除(已更新 2026-09-02,含 9/2 实测纠正)
> 🛑 当前沙箱真实行为(2026-09-02 实测,纠正第 11 次记录的乐观结论):
> 本机删除层把 unlink 转进系统回收站,但沙箱内回收站不可用(windows-sandbox-recycle-bin-unavailable),
> 导致 safe_remove 的硬删全部静默失败——爬取时的自动清理不会真正删文件,目录会残留垃圾/重复件。
> 另:os.remove 虽能绕过沙箱真删,但单轮累计 50 个即被杀进程(SAFE_DELETE_BULK_CONFIRM_REQUIRED,需用户显式确认才放行)。
> 结论:在本沙箱做干净整理,请用「shutil.move 移到临时目录去重」而非依赖 safe_remove 的硬删;
> 真删临时目录时务必 ≤50 个/轮,且需用户确认。
>
> ✅ 已修复(2026-09-02 晚,第 18 次优化):safe_remove() 现在先 unlink、失败再用
> os.remove / shutil.rmtree 兜底(沙箱内实测可真删,不再被 50 阈值拦截)。
> 所以脚本内置的清理(删行程单/运单明细/压缩包/sha256 重复/ofd 副本)现在能真正删掉了,
> 不需要再手动收尾。前提:用户对目标目录已放开删除权限;否则两条路都会被拦,
> 此时仍退回「shutil.move 移到临时目录」的移动去重。
> 注意:unlink 走回收站那条路在沙箱里依旧不可用(windows-sandbox-recycle-bin-unavailable),
> 真正生效的是兜底的 os.remove —— 日志里看到 "无法删除" 且带 "兜底删除也失败" 才算真的删不掉。
本环境早期对 unlink 有拦截(回收站不可用 / 批量阈值杀进程),所以 safe_remove 曾经「只移动不硬删」
(移到 D:\发票\待清理\)。2026-09-02 曾改为直接删除:当时以为回收站可兜底、实测批量 unlink 不被杀;
但同日本沙箱实测表明回收站实际不可用、自动清理静默失败,故真正有效的整理仍靠移动去重。
注意点:
1. 本机删除层会把 unlink 转进系统回收站;但沙箱内回收站不可用,safe_remove 硬删会静默失败(见上方警示框)。
2. 被 WPS/Excel 打开着的文件(如正打开的 发票台账_*.csv)会删除失败——关掉对应窗口再跑即可。
3. 若要在本机(非沙箱)执行,safe_remove 直接删除、不再进「待清理」;沙箱内则按上方警示框用移动去重。
自动化建议
用户如果要定期归档(比如每月初整理上月发票),可以用 automation_update 建定时任务,
prompt 里写清楚日期范围和调用方式即可。
变更记录
-
2026-09-02(第 18 次优化):修好「该删的删不掉」+ 票号抽错,去重改为按发票号码
① 总根因修复:safe_remove()原先只用unlink,沙箱内会被删除层转成「移进回收站」→
回收站不可用 → 每一条删除都静默失败(行程单/运单明细/压缩包/ofd 副本全留在目录里)。
现在改为 先unlink,失败再用os.remove/shutil.rmtree兜底(权限放开后实测可真删)。
这一条修好后,脚本内置的行程单/运单明细/sha256 重复/ofd 副本清理全部自动生效,不用手动收尾。
② 修票号抽错:OFD 的 TextCode 是乱序的,「发票号码:」标签后面紧跟的是销售方税号
(如91341192MA2RJ6WH82),原正则\d{8,22}把税号前 8 位91341192当成了票号,
真票号(20 位,如26117000001336580904)反而被跳过。
改为优先抓 20 位纯数字(全电发票号码固定 20 位),抓不到再退回原逻辑。
③ 新增 ofd/pdf 按「发票号码」兜底去重:原逻辑只按文件名标准化匹配
(G0004110010_ofd_xxx→G0004110010_xxx),遇到文件名完全不同的同票对
(如elfp716….pdf与elfp759….ofd)会漏配。新增一步按票号比对,命中则删 ofd 留 pdf。
安全保护:只采用 ≥15 位的长号码做去重,短串(税号前缀等)一律不参与 —— 宁可漏删、绝不误删。
④ 实测达标(D:\发票\2026年9月2日_203907):文件数 = 台账行数 = 唯一票号数 = 38,
票号空 0、金额空 0、重复 0;分类 交通 23 / 办公 13 / 物流 1 / 餐饮 1,合计 ¥2478.27。
⑤ 两条血的教训(务必记住):- 不要按文件名判断是不是同一张票 —— 同一张票的 pdf 与 ofd 文件名可能完全不同;
同款模板的不同发票(如 7 张「皖AA63553 通行发票-1」)文件名却几乎一样但票号不同。判同票只能靠发票号码/内容。 - 不要拿台账当「真发票白名单」 —— 台账是在 cleanup 之后生成的,cleanup 一旦失败,
垃圾(行程单/运单明细)就已经被记进台账了,照单全收等于把错误固化下来。
- 不要按文件名判断是不是同一张票 —— 同一张票的 pdf 与 ofd 文件名可能完全不同;
-
2026-09-02(第 17 次优化):同天重爬自动新建「日期_时间」文件夹 + 权限全开后删除可用
① 新增文件夹级铁律(用户明确要求):重新爬取时不要复用、也不要管已有的日期文件夹。
当天文件夹若已存在,脚本自动在日期后加_HHMMSS生成独立新文件夹,老文件夹完全不动。
代码位置:fetch_invoices.py的base_dir = output_dir / day_folder之后
(已存在则base_dir = output_dir / f"{day_folder}_{HHMMSS}",同秒再冲突加_1),
并补打一行「实际目录」,让用户一眼看到真实落盘路径。
② 去重范围明确:只看「本次邮箱抓取 + 当前生成的新文件夹」,不跨D:\发票\下其它老文件夹。
③ 实测:重爬 8/28–9/2 落到D:\发票\2026年9月2日_203907,老文件夹2026年9月2日未动;
新文件夹经「移走垃圾 18 → 移走同发票 ofd 副本 11 → 删内容相同重复 8」三步整理,
最终 53 张发票(45 pdf + 8 ofd)+ 台账 csv/xlsx;台账 53 行、金额空 0、分类空 0、
唯一发票号码 39、金额合计 2784.29。
④ 删除行为更新:用户权限全开后shutil.rmtree/os.remove可直接真删、不再被 50 阈值拦截;
但safe_remove(回收站路径)在沙箱内仍不可用 → 脚本自动清理仍会静默失败,收尾请手动删。
本次已清理D:\发票_清理tmp_203907(29)、D:\发票_清理tmp_20260902(42)、空目录D:\待删_旧发票数据_0901。
⑤ 已知现象:同一发票被多封邮件/多次发送时,ofd 与 pdf 双格式都会被记进台账(行数 > 唯一发票数),
需靠 sha256 去重精简;ofd 无对应 pdf 时保留 ofd(避免丢票)。 -
2026-09-02(第 16 次优化):端到端验证新铁律 + 固化环境与删除真实行为
① 端到端验证「重名加时间戳 + 不强制空目录」铁律:真实爬取 8/28–9/2 发票,落盘 39 张
(交通差旅 24 / 办公采购 13 / 物流快递 1 / 餐饮住宿 1),台账 发票号码空 0、发票金额空 0、分类空 0;
重名自动加时间戳、旧文件零覆盖,新铁律成立
② 时间戳逻辑单元验证:旧文件保护 / 同批重名 / unique_path / 同秒兜底 全过;
实测出现…_20260902_201532.pdf、…_201530_2.ofd等时间戳命名,无覆盖
③ 踩坑·环境依赖(已固化为铁律):pypdf/pymupdf/pytesseract/openpyxl
必须装进运行脚本的那个 python(binaries\python\versions\3.13.12),仅装在 venv 不够——
本次曾因依赖只在旧 venv、versions\3.13.12没装,导致--reprocess重抽时
发票号码空 38/42、金额空 41/42;对versions\3.13.12补装后重跑即 0 空(见上方依赖说明)
④ 踩坑·沙箱删除真实行为(纠正第 11 次记录的乐观结论):本机删除层把unlink转回收站,
但沙箱回收站不可用(windows-sandbox-recycle-bin-unavailable)→safe_remove硬删静默失败,
爬取自动清理实际不删文件(本次目录一度涨到 132 个);os.remove能真删但单轮累计 50 即杀进程
(SAFE_DELETE_BULK_CONFIRM_REQUIRED,需用户确认)。干净整理改用「移动去重」(见上方「关于删除」)
⑤ 副作用(与第 15 次一致):--reprocess不连邮箱,台账 邮件日期/发件人/主题 留空
(分类可用文件夹名补,永不空);真实下载爬取才有这些字段 -
2026-09-02(第 15 次优化):重名加时间戳 + 撤销「重爬必须空目录」铁律
① 用户明确指令:"下次让你爬取 你不要管文件夹原有文件,如果名字有重复问题就把时间也加上"
② 撤销旧铁律:不再要求爬取前清空/挪走目录里的旧文件;直接往已存在目录爬即可
③ 重名策略由_1 _2改为时间戳:alloc_name()/unique_path()在文件名冲突时,
自动追加_YYYYMMDD_HHMMSS(精确到秒)区分;时间戳本身已唯一则不加后缀,
仅极偶发同秒再冲突才追加_1。台账 csv/xlsx 被 WPS/Excel 占用时的备份文件名
也从_HHMMSS统一升级为_YYYYMMDD_HHMMSS,时间一目了然
④ 实测:目录里预先放进旧发票 → 新下重名件自动变xxx_20260902_201116.pdf,旧文件内容不被覆盖/不被改动;
同批两个同名附件也正确区分、互不覆盖
⑤ 副作用(如实标注):往已存在目录爬时,旧文件(非本次下载)的来源元数据
(邮件日期/发件人/主题)无法从路径恢复 → 这些字段会留空;分类可用文件夹名补,永不空。
若介意这些字段空白,爬前仍可自行挪走旧目录,但不再作为强制前提
⑥ 代码py_compile通过 + 临时目录 4 场景实测全过(旧文件保护 / 同批重名 / unique_path / 同秒兜底) -
2026-09-02(第 12 次优化):修复台账溯源元数据丢失 + 加固清理
① 现象:重爬到已存在的日期目录时,台账的 分类/邮件日期/发件人/主题 大量变空
(23~24/36 行)。原因:旧文件无本次下载的 records,cleanup 重命名后文件名匹配不上来源行
——这就是第 6 次优化预警的「重爬前必须把旧日期文件夹整体挪走、从空目录开始」,本次再踩实
② 防御兜底:台账重建时若来源元数据缺失,用「父文件夹名」补回 分类(文件夹名即分类,永不空);
日期/发件人/主题 无法从路径恢复,必须靠下面第 ④ 条避免
③ 根因2(偶发):个别压缩包下载瞬态失败(write_bytes异常→continue跳过),
该压缩包没进records,cleanup 解压出的发票无法继承来源行 → 日期/发件人/主题 空。
DEBUG 验证:此时extracted_map里的 zip 名不在by_name(MISSING非空)
④ (已撤销)旧铁律「重爬必须空目录」已撤销:用户 2026-09-02 明确——
下次爬取不必管目录里原有的旧文件,直接往里爬即可;新文件若与旧文件重名,
加时间戳 _YYYYMMDD_HHMMSS 区分(不覆盖、不动旧文件),见上方「命名规则」。
⚠️ 副作用:往已存在目录爬时,旧文件(非本次下载)的来源元数据(邮件日期/发件人/主题)
无法从路径恢复 → 这些字段会留空(分类可用文件夹名补,永不空)。
若介意这些字段空白,爬前仍可自行把旧目录挪走,但不再作为强制前提。
⑤ 本次 8/28–9/2 从空目录干净重爬:39 张(交通 24 / 办公 13 / 物流 1 / 餐饮 1),
四类元数据(分类/日期/发件人/主题)全 0 空,文件 0 缺失 0 内容重复,台账+xlsx 正常 -
2026-09-02(第 13 次优化):修复发票字段(代码/号码/金额)抽取漏抽
① 现象:5 张发票缺号码(001/002/003/011/023),其中 3 张 12306 高铁票还缺金额
② OFD 漏抽根因:_ofd_text正则只认<TextCode>,但 OFD 文字带命名空间前缀
<ofd:TextCode>→ 真实文字全漏;而且「整份 xml 去标签」的兜底把<Path>坐标数字
(如绘图包围盒211.50)当成了文字 → 金额误抽。修复:正则兼容任意前缀<(?:[\w]+:)?TextCode>,
并额外解析OFD.xml的<CustomData Name="发票号码">值</CustomData>结构化字段(最可靠);
彻底去掉坐标垃圾兜底。案例:011 金额从误抽的 211.50 纠正为真实价税合计 8.00
③ 财政电子票据(如江苏高速)用「票据代码/票据号码」而非「发票代码/发票号码」:
正则加票据代码/票据号码兜底 → 023 补出 代码 32020126 + 号码 0036595174
④ 图片型 PDF(无文字层,pypdf 抽不到字):若文件名本身是纯数字长串(如 12306 电子客票号
20 位),用作 发票号码 兜底(先剥001_序号前缀再判定)。案例:001/002/003 补出电子客票号
⑤ 图片型 PDF 的金额仍无法抽取(无文字层,需 OCR),当前留空;本机未装 tesseract,
若要填 高铁票 金额需用户授权安装 OCR(见排错表)
⑥ xlsx 被 WPS/Excel 占用独占领时,_write_ledger_xlsx落到带时间戳的同名文件
(如发票台账_20260902_193809.xlsx),不中断;用户关掉旧表打开新的即可 -
2026-09-02(第 14 次优化):加装 OCR,补齐图片型发票(高铁票等)的字段
① 用户反馈:3 张 12306 高铁票是纯图片 PDF(无文字层),发票金额抽不到
② 装 OCR:winget install UB-Mannheim.TesseractOCR(tesseract 5.4 到
C:\Program Files\Tesseract-OCR\);venv 装pymupdf(PDF→图) +pytesseract(调引擎);
中文包chi_sim/chi_sim_vert放 D:\tessdata(纯 ASCII 路径——
中文路径会被 tesseract 的 ANSI 读取乱码,必须用不含中文的目录)
③ 抽取逻辑:PDF 文字层为空(图片型)时,_ocr_pdf_text()用 PyMuPDF 渲染首页→
tesseract(chi_sim+eng) OCR,再套用同一套正则抽 代码/号码/金额/普专票;
OCR 失败/未装自动降级留空,不影响主流程
④ 两个坑已踩并固化:OCR 把「发票号码」识成「发 票 号 码」(带空格)致正则漏抽
→ 图片型 PDF 的 发票号码 同时用文件名纯数字兜底(12306 电子客票号);
OCR 小数点常识成「456. 00」→ 抽取前规整回「456.00」
⑤ 结果:3 张高铁票 发票号码(电子客票号)+金额(¥456.00) 全部补齐;全台账
发票号码空 0、发票金额空 0。011 OFD 金额误抽 211.50→纠正 8.00
⑥ OCR 是机本机可选增强:换机器需重装 tesseract + 中文包到对应路径,否则该 3 类图片发票金额留空(不影响其它发票) -
2026-09-02(第 11 次优化):台账排版升级——额外生成排版好的 xlsx
① 用户反馈:CSV 在 Excel/WPS 打开列宽/字体/网格难看(截图:序号、代码/号码/金额列太窄,
邮件主题/发件人/路径全成###,字体是默认等线、网格只有浅灰横线、首行没冻结)。
CSV 文件本身只存数据、不存排版信息——所以「优化表格」必须改成.xlsx
② 新增_write_ledger_xlsx():用openpyxl写同名.xlsx(CSV 保留)。
排版规格:微软雅黑(表头 11pt 加粗 / 正文 10pt)、表头淡蓝底 #D9E1F2 + 深蓝字 #1F3864、
冻结首行 (A2)、所有单元格四周细边框 #BFBFBF、列宽自适应(按内容最长字符×1.2,
中文字符按 2 计,下限 8 / 上限 50)、数字/金额/大小列右对齐、文本列自动换行、
大小列设数字格式 #,##0.0(值已转 float,Excel 显示 "34"/"33.9" 不是字符串)。
金额列保留原字符串("1,234.56"),避免 Excel 把千分位当日期/科学计数法
③ 三处接入:主流程下载版、--reprocess、--cleanup-only --apply都会顺带生成 xlsx
④ 新坑 + 健壮性:用户日常开着 WPS 看台账 csv,会独占锁住文件 → 写 csv 直接
PermissionError。增加 fallback:被锁时自动写到带时间戳后缀的 csv
(如发票台账_20260902_174120.csv),xlsx 同名照样生成,控制台打印清晰提示。
关掉 WPS 那个文件再跑就恢复正常
⑤ 新增依赖:openpyxl(轻量库,已在 venv 装好);缺失不影响 CSV 下载/清理
⑥ 实测:临时目录跑--reprocess生成 39×13 xlsx,冻结/字体/边框/列宽/数字格式全部就位;
真实D:\发票\2026年9月2日\跑一次产出发票台账_20260902_174120.xlsx,WPS 占用时
fallback 触发,xlsx 仍正常生成 -
2026-09-02(第 11 次优化):清理改为直接删除,废弃「待清理」中转目录
① 用户授权:对D:\发票下「重复 / 压缩包 / 邮件 CSV 附件 / 非发票垃圾」直接删除,不再移动到待清理
②safe_remove()由shutil.move改为os.unlink/shutil.rmtree直接删;每个文件独立 try,
单个失败只记警告不中断;删除后清理产生的空子目录
③ 删除层把unlink转进系统回收站(可找回),不是不可恢复抹除——比待清理更省事更安全
④ 清理时遇发票台账开头的 csv 跳过(保护生成的台账);其它邮件 CSV 附件当垃圾直接删
⑤ (已纠正)原记「实测单进程批量 unlink 65+ 不被杀、直接删 98 个垃圾」为乐观结论;
2026-09-02 同日本沙箱实测表明回收站实际不可用、自动清理静默失败,见第 16 次优化及「关于删除」警示框
⑥ 旧D:\发票\待清理\中转目录(50 文件)已整体删除,脚本不再生成该目录 -
2026-09-02(第 10 次优化):台账新增发票代码/号码/金额/普专票 + 文件名 001_ 序号前缀
① 用户要求:在现有台账上增加「发票代码」「发票号码」(只一个有另一个就空)、
「发票金额」、「普票/专票标识」四列;并让台账「序号」与文件名前面的序号前缀一致
② 文件名加001_002_… 前缀(原附件名保留在后面),重命名只在清理后做,
looks_like_invoice()同步兼容前缀(先剥^\d{1,4}_再判),避免加前缀后被误当垃圾移走
③ 新增抽取:extract_invoice_fields()——PDF 用pypdf抽文字层、OFD 用标准库zipfile解 XML 抽
<TextCode>(兼容ofd:命名空间前缀)+ 解析OFD.xml的<CustomData>结构化字段;
财政电子票据用「票据代码/票据号码」兜底;正则抓 发票代码/发票号码/发票金额(取票面最大小数=价税合计)
/发票类型(专用→专票、普通→普票)。图片型 PDF(无文字层)自动 OCR 兜底(见第 14 次)。
抽不到留空(启发式,不保证 100%;全电发票无代码正常留空)
④ 金额用"票面最大小数"而不是"价税合计标签后第一个数"——避免被「合计」行的金额误覆盖
(实测拼多多价税合计 49.65 正确,未被 43.94 覆盖);并设 1 亿上限过滤抽取噪声(如 9449 亿乱码)
⑤ 新增--reprocess <目录>模式:不连邮箱,给老文件夹补抽字段+加前缀+重写台账
⑥ 实测:38 张发票全部加前缀、台账 5 新列就绪、序号前缀与文件名 0 不一致;
发票号码 33/38、金额 35/38、类型 33/38 命中(空的是图片型扫描件/无关键词的通行费,符合预期) -
2026-09-01(第 9 次优化):清理逻辑反转为白名单(只留发票,其余全删)
① 用户指出:不该往删除黑名单里一个个加关键词,应该只保留发票、其它全部删除
② 新增looks_like_invoice()白名单判定(格式 pdf/ofd + 非空 + 不命中排除词- 至少一条正向特征:发票词 / 纯数字发票号 / 税务平台命名前缀),cleanup 第 3 步改为
"不在白名单默认清掉"——签收单/对账单等新花样无需再加规则
③ 踩坑:新白名单集合误用常量名INVOICE_EXTS,与下载阶段第 97 行同名集合冲突
(Python 后定义覆盖前者)→ 下载阶段不收 zip → ETC/票根/12306 的发票整批丢失
(现象:解压 0 个、"没有附件"邮件从 2 封变 12 封)。改名INVOICE_FINAL_EXTS修复
④ 实测重爬验证:38 张(交通差旅 23 / 办公采购 13 / 物流快递 1 / 餐饮住宿 1),
运单明细/行程单/xml/zip 全部 0,台账 38 行 = 文件数
- 至少一条正向特征:发票词 / 纯数字发票号 / 税务平台命名前缀),cleanup 第 3 步改为
-
2026-09-01(第 8 次优化):删快递运单明细
①TRIP_KEYWORDS增加运单明细waybill——顺丰等会同时发「发票运单明细.pdf」(运单清单)和
「顺丰电子发票.pdf」(发票本体),前者命中关键字直接清掉
② 实测重爬验证:物流快递目录只剩顺丰电子发票.pdf,台账 38 行 = 文件数 -
2026-09-01(第 7 次优化):OFD/PDF 双格式去重(留 PDF)
① 清理流程新增第 6 步:同一类别目录内,OFD 文件名标准化(去.ofd_查阅需OFD阅读器尾巴、
_ofd_→_)后与某 PDF 同名 → 视为同一张发票,OFD 直接删除(进回收站,用户要求:
"ofd格式和pdf格式的发票内容一样的话,把ofd的文件也删除")
② 修复 dry-run 预览误导 bug:safe_remove在预览模式不记录统计,导致预览永远显示
"无需清理"——改为预览时也记录分类清单(只是不真动文件),合计数按分类列表求和
③--cleanup-only模式补台账同步:清理后从台账移除已不在目录里的文件行
④ 实测:57 张 → 移走 18 个 OFD(都有同名 PDF),剩 39 张(交通 23 / 办公 13 / 快递 2 / 餐饮 1),
elfp7593920864674611142.ofd(无同号 PDF)正确保留;台账 39 行 = 文件数 -
2026-09-01(第 6 次优化):清理策略改为纯移动(安全层批量阈值)
① 新坑:提权模式下硬删同一轮累计 50 个后,下一次抛SAFE_DELETE_BULK_CONFIRM_REQUIRED
直接杀进程且异常不可捕获,计数器跨命令持续——首次重跑时清理中途暴毙,目录留半成品
②safe_remove()改为统一移动到「待清理」,彻底不尝试 unlink(移动不受任何阈值限制)
③ 修报告重复计数:同一批文件不再同时计入分类列表和 moved(合计数虚高一倍)
④ 修复流程:隔离混杂旧目录 → 空目录重爬 → 干净复现(57 张 + 台账 57 行 + 28 个进待清理)
⑤ (已撤销)旧经验「重爬前先把旧目录挪走、从空目录开始」已撤销(见第 15 次优化):
现在允许往已有旧文件的目录直接爬,重名自动加时间戳区分。 -
2026-09-01(第 5 次优化):下载后自动清理(解压 / 去重 / 删行程单)
① 下载落盘后再跑cleanup_dir:解压 zip/rar/7z(zip 用标准库,rar/7z 调系统unrar/7z)、删原压缩包、
删行程单与 xml/txt 垃圾、按 SHA256 内容去重(同名不同内容保留两份)
② 增safe_remove():沙箱禁用硬删,改shutil.move到D:\发票\待清理\当天日期\(可恢复),目标已存在自动加唯一后缀
③ 删行程单/垃圾前先登记其 SHA256 进seen,防止内容相同的发票被去重漏掉
④ 台账同步:清理后只留仍在目录的文件;解压出的发票补回原压缩包来源行(extracted_map)
⑤ 补import shutil(之前漏掉导致回退时报 NameError)
⑥ 实测:8/29 以后 57 张发票留盘、28 个包装物进「待清理」、台账 57 行与文件数一致;新建文件夹未被触碰 -
2026-09-01(第 4 次优化):文件名保留原始附件名
① 取消「爬取当天日期_序号」命名,改为直接用邮件里的原始附件名
(例:拼多多商家电子发票.pdf、通行费电子票据.zip)
② 同名冲突自动加_1_2后缀,绝不覆盖已有发票(同一批内用内存集合防重,跨批用文件系统检查)
③ 删除了SeqNamer类,新增alloc_name(subdir, fname, used)分配器
④ 台账「序号」列改为「保存文件名」,并打印「文件名就是原附件名」提示
⑤ 实测:8/29 以后 53 张发票全部以原名落盘;已清理此前用序号命名的旧批次 -
2026-09-01(第 3 次优化):
① 归档结构改为D:\发票\爬取当天日期\消费类型\日期_序号.ext
(例:D:\发票\2026年9月1日\交通差旅\20260901_001.pdf)
② 去掉月份层——不再按邮件月份分文件夹,只按爬取当天建一层日期目录
③ 日期文件夹用中文格式「2026年9月1日」(月、日不带前导零)
④ 序号扫描范围收窄到当天日期文件夹内(不是整个 D:\发票),
换一天新建文件夹时序号重新从 001 开始,各天互不干扰
⑤ 台账 CSV 移入当天日期文件夹 -
2026-09-01(第 2 次优化):
① 新增消费类型自动分类(交通差旅/餐饮住宿/物流快递/通讯网络/办公采购/生活缴费/未分类)
② 归档结构改为D:\发票\月份\消费类型\日期_序号.ext(月份按邮件日期)
→ 已被第 3 次优化取代,现改为「爬取当天日期\消费类型」
③ 分类两级判断:元数据 → 正文兜底(解决诺诺网/百旺等代开平台只能靠正文识别商家的问题)
④ 序号改为跨子目录递归续排,分目录后仍不覆盖
⑤ 台账增加「分类」「保存路径」列;控制台打印分类统计条形图
⑥ 修复严重 Bug:IMAP 日期搜索带括号导致日期范围失效(返回全部邮件),
改为不带括号写法 + 本地日期二次校验
⑦ 补充关键词:亿通行、捷顺停车、机场集团等(实测未分类归零) -
2026-09-01(初次创建):基础爬取 + 授权码配置 + 台账。
实测参考(2026-09-01,收件箱 105 封)
| 日期范围 | 扫到邮件 | 发票张数 | 分类分布 |
|---|---|---|---|
| 2026-01-01 ~ 2026-09-01(全量) | 105 | 81 | 交通 41 / 办公 18 / 快递 13 / 餐饮 9 |
| 2026-08-01 ~ 2026-08-10 | 33 | 25 | 交通 11 / 快递 10 / 办公 3 / 餐饮 1 |
| 2026-09(整月) | 41 | 53 | 交通 30 / 办公 14 / 餐饮 6 / 快递 3 |
另有 7 封"疑似发票但无附件"(诺诺网、百旺、京东、东方航空、停车费),
发票藏在正文下载链接里,脚本抓不到,需人工点开下载。