从50张发票到一键合并:我用openclaw搞定了最烦人的报销琐事
每到报销季,翻邮箱找发票、下载、整理、打印……光是这些重复劳动就能耗掉半天。直到我把这件事交给了 OpenClaw。
一、痛点:发票管理到底有多烦?
你有没有经历过这样的场景:
-
• 报销截止日期快到了,邮箱里散落着几十封发票邮件 -
• 京东、盒马、大润发、拼多多……每个平台的发票格式都不一样 -
• 有的发票是横版的,有的是竖版的,混在一起根本没法直接打印 -
• 手动一张张下载、重命名、排版打印,一下午就没了
我上个月出差加上日常采购,积累了 51张电子发票,分布在邮箱的不同文件夹里。按照传统做法,我需要:
-
1. 打开邮箱,一封封找发票邮件 -
2. 点击下载,保存到本地 -
3. 打开PDF阅读器,逐个检查内容 -
4. 用 Word 或其他工具排版,每页放两张发票 -
5. 打印
光是第1步和第2步就够折腾了——有些发票藏在促销邮件堆里,有些附件是压缩包,有些邮件标题根本看不出是发票。
但现在,这一切只需要一句话。
二、一句话搞定:告诉 AI 你要做什么
打开 OpenClaw,我只需要说:
“帮我从邮箱搜索最近的发票邮件,下载所有发票附件,然后合并成每页两张的PDF,方便打印。”
OpenClaw 会自动理解你的意图,然后开始执行。不需要写代码,不需要配置脚本,就像给一个靠谱的同事交代任务一样。
接下来,我们拆开看看每一步的技术细节——这比你想象的要复杂得多。
三、第一步:连接邮箱——看起来简单,坑最多
3.1 用户需要提供什么?
要让AI访问你的邮箱,你需要提供以下信息:
邮箱地址:你的完整邮箱,比如 user@qq.com
IMAP服务器地址:邮件接收服务器,比如 imap.qq.com
端口号:通常是993(SSL)
密码/授权码:注意,这不是你的登录密码,是专门的应用授权码
注意: 这里第一个坑就来了——大多数邮箱服务商现在不让你用登录密码做 IMAP 连接,你需要去设置里生成一个「授权码」或「应用专用密码」。
不同邮箱的设置路径不一样,但流程都类似:先进邮箱设置,找到IMAP服务开关,开启后生成一个授权码。
QQ邮箱:设置 – 账户 – POP3/IMAP/SMTP服务 – 开启IMAP – 生成授权码
163邮箱:设置 – POP3/SMTP/IMAP – 开启IMAP – 客户端授权密码
Gmail:设置 – 转发和POP/IMAP – 开启IMAP – 用App Password
Outlook:安全设置 – 应用密码 – 生成
很多人卡在这一步。邮箱默认不开IMAP,或者不知道授权码和登录密码是两回事。
3.2 IMAP 连接的技术细节
OpenClaw 通过 Python 的 imaplib 库建立 IMAP 连接,核心流程:
import imaplib# 1. 建立 SSL 连接mail = imaplib.IMAP4_SSL("imap.qq.com", 993)# 2. 登录(用授权码,不是密码)mail.login("user@qq.com", "授权码")# 3. 选择邮箱文件夹mail.select("INBOX") # 收件箱,也可选其他文件夹# 4. 搜索邮件status, messages = mail.search(None, 'ALL')
看起来很简单对吧?但实际执行中,坑一个接一个:
坑1:连接超时
很多邮箱服务器对 IMAP 连接有超时限制,如果网络不稳定或者服务器响应慢,连接会突然断开。需要设置合理的超时时间,并做好断线重连。
坑2:SSL 证书问题
某些自建邮件服务器或企业邮箱的 SSL 证书可能不被 Python 默认信任,需要手动处理证书验证。
坑3:中文编码混乱
这是最恶心的问题。IMAP 协议本身用 ASCII 传输,中文邮件的发件人、主题等信息需要通过 RFC 2047 编码(类似 =?GBK?B?...?=)。不同邮箱服务商用的编码不一样:
-
• QQ 邮箱:常用 UTF-8 或 GBK -
• 163 邮箱:常用 GBK -
• 企业邮箱:可能是 GB2312
如果解码方式不对,你看到的发件人就是一堆乱码:
# 解码前=?GBK?B?5paw5YiX5Zyo?=# 解码后京东商城
OpenClaw 需要自动检测编码并正确解码,否则后续搜索根本匹配不到。
坑4:文件夹结构
不同邮箱的文件夹命名不一样。QQ 邮箱有「收件箱」「广告邮件」「订阅邮件」等多个分类,发票邮件可能被归到了非收件箱的文件夹里。需要遍历所有文件夹搜索,不能只搜 INBOX。
3.3 连接成功后,搜索发票邮件
连接只是开始。接下来要从成百上千封邮件中找出发票邮件。
搜索策略是多维度的:
发件人关键词匹配: 京东(jd.com)、盒马(hemaxiansheng)、大润发(rt-mart)、拼多多(pinduoduo)、百望(baiwang)、诺诺网(nuonuo),以及其他常见开票平台。
主题关键词匹配: 发票、电子发票、invoice、报销凭证、收据、订单、交易(部分平台标题不含「发票」二字)。
附件特征: MIME 类型为 application/pdf 的附件。
时间范围: 用户指定的月份,如 2026年4月。
但光靠关键词匹配还不够。实际中会遇到:
问题1:邮件标题不含”发票”
比如京东的邮件标题可能是「您的订单已开具电子发票」,但有些平台的标题是「您的订单已完成」或「交易成功通知」,发票只是作为附件存在。需要同时匹配多个关键词。
问题2:促销邮件干扰
搜索关键词「京东」会命中大量促销邮件。需要通过「发件人+主题+附件」三重过滤来排除干扰。
问题3:同一发票多封邮件
比如大润发可能先发一封「开具成功通知」,再发一封「发票明细」,实际发票只有一个。需要去重。
四、第二步:下载发票——附件和链接,两种完全不同的战场
这是整个流程中最复杂的部分。你可能以为发票就是邮件附件,下载就完了。但现实是,发票有两种存在形式:
4.1 形式一:直接附件(简单模式)
大约 60% 的发票以 PDF 附件形式存在于邮件中。这种情况相对简单:
import email# 解析邮件msg = email.message_from_bytes(raw_email)# 遍历所有附件for part in msg.walk(): content_type = part.get_content_type() filename = part.get_filename() if content_type == "application/pdf" and filename: # 找到 PDF 附件,下载 with open(filename, "wb") as f: f.write(part.get_payload(decode=True))
但即使是「简单模式」也有坑:
坑1:附件名编码
中文附件名经常被 RFC 2047 编码,直接读取会是乱码:
# 原始值=?UTF-8?B?5rWL6K+V5oql5YiwX18yMDI2MDQwMi5wZGY=?=# 解码后发票_京东_20260402.pdf
需要用 email.header.decode_header() 正确解码。
坑2:附件名重复
同一封邮件可能有多个 PDF 附件(比如发票+明细),需要根据文件名或大小区分哪个是发票。
坑3:附件是压缩包
部分平台(如某些政府开票系统)会把发票放在 zip 压缩包里。需要先解压再提取 PDF。
坑4:邮件是 multipart/mixed 结构
一封邮件可能包含正文+多个附件,需要正确解析 MIME 结构,跳过 HTML 正文,只提取 PDF 部分。
4.2 形式二:邮件内链接(困难模式)
这才是真正的噩梦。
大约 40% 的发票不是以附件形式存在,而是以 链接 形式嵌在邮件正文中。你点击链接,跳转到一个网页,然后在网页上查看或下载发票。
比如:
-
• 百望云 – 邮件正文有一个「查看发票」按钮,链接到 https://fanke.baiwang.com/... -
• 诺诺网 – 邮件里有一个发票链接,需要在网页上点击「下载PDF」 -
• 微信支付 – 邮件里有「查看详情」链接,跳转到小程序页面 -
• 支付宝 – 类似,链接到支付宝网页版
为什么这是噩梦?因为要访问这些链接,你需要:
步骤1:从邮件 HTML 正文中提取链接
from bs4 import BeautifulSoup# 邮件正文是 HTMLhtml_body = msg.get_payload(decode=True).decode("utf-8")soup = BeautifulSoup(html_body, "html.parser")# 找到所有链接links = soup.find_all("a", href=True)for link in links: href = link["href"] if "invoice" in href or "fapiao" in href or "发票" in link.text: print(f"找到发票链接: {href}")
步骤2:处理各种链接类型
找到链接后,面临的问题更多:
问题1:短链接/重定向
很多邮件里的链接是短链接(如 https://dwz.cn/xxxxx),需要先请求一次获取真实 URL。有些经过多次重定向,需要跟踪整个重定向链。
问题2:链接有时效性
很多发票链接带有 token,过期后就无法访问。比如:
https://invoice.example.com/view?token=abc123&expire=1680000000
如果发票是上个月开的,链接可能已经过期了。
问题3:需要登录才能访问
最恶心的情况。你点击链接,跳转到一个登录页面,要求输入手机号+验证码。这意味着 AI 根本无法自动访问——它没有你的手机号,也无法接收验证码。
实际遇到的案例:
-
• 百望云发票链接 – 直接可以访问,但页面需要 JavaScript 渲染,普通 HTTP 请求拿不到内容 -
• 诺诺网发票链接 – 有时需要微信扫码才能查看 -
• 某些银行开票系统 – 链接打开后是登录页面,无法自动获取
步骤3:渲染 JavaScript 页面
即使链接不需要登录,很多发票查看页面是 SPA(单页应用),内容由 JavaScript 动态加载。用普通的 HTTP 请求拿到的只是空壳 HTML:
import requests# 普通请求 —— 拿到的是空壳resp = requests.get(invoice_url)# <div id="app"></div> ← 内容是空的!
要拿到真实内容,需要用无头浏览器(如 Playwright、Selenium)渲染 JavaScript:
from playwright.sync_api import sync_playwrightwith sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto(invoice_url) page.wait_for_load_state("networkidle") # 现在才能拿到渲染后的内容 content = page.content()
但无头浏览器又带来新问题:
-
• 资源占用大 – 启动一个 Chromium 实例需要几百 MB 内存 -
• 速度慢 – 渲染一个页面可能需要几秒到十几秒 -
• 反爬检测 – 有些网站检测无头浏览器特征,返回验证码或直接拒绝 -
• 需要安装浏览器 – 服务器上不一定有 Chromium
步骤4:从网页中提取/下载 PDF
页面渲染出来后,还要找到 PDF 下载按钮或直接获取 PDF URL:
# 找到下载按钮download_btn = page.query_selector("a.download-btn")pdf_url = download_btn.get_attribute("href")# 下载 PDFpdf_resp = requests.get(pdf_url)with open("invoice.pdf", "wb") as f: f.write(pdf_resp.content)
有些网站的「下载」按钮实际上是调用 JavaScript 生成 PDF(比如用 html2pdf.js),这时候需要拦截网络请求或模拟点击。
4.3 实际的坑汇总
整个下载过程中,我遇到的真实问题:
-
• 中文附件名乱码 – 文件名不可读,用 RFC 2047 解码 -
• 附件是 zip 不是 pdf – 无法直接使用,自动解压提取 -
• 链接过期 – 无法下载,尽早处理或跳过并标记 -
• 链接需要登录 – 完全无法自动获取,跳过提示用户手动下载 -
• 页面需要 JS 渲染 – 拿到空壳 HTML,用 Playwright 渲染 -
• 反爬检测 – 被拒绝访问,设置合理的 User-Agent 和延迟 -
• 同一发票多个链接 – 重复下载,URL 去重 -
• 邮件编码问题 – 搜索匹配失败,自动检测编码并解码
五、第三步:合并打印——最后的技术关卡
51张发票下载完成后,接下来要合并成方便打印的 PDF。这一步看似简单,但技术选型踩了不少坑。
5.1 渲染引擎的选择
中国电子发票的 PDF 有一个特殊性:很多使用 GBK 编码的中文字体,且字体未嵌入。
这意味着渲染引擎必须能在系统中找到对应的中文字体,否则中文就会变成方块或乱码。
我测试了三种方案:
-
• Ghostscript (gs) – 需要安装宋体/黑体,pngalpha 有黑边,png16m 丢内容 -
• pdftoppm (poppler) – GBK 编码失败,部分中文显示为方块 -
• PyMuPDF (fitz) – 原生支持 GBK,完美渲染
最终选择 PyMuPDF,它能正确处理 GBK、GB2312、Identity-H 等各种中文编码,不需要额外安装字体(虽然装了更好)。
5.2 输出格式的选择
渲染成图片后,需要合成 PDF。这里又有一个大坑:
用 Pillow 直接保存 PDF 会导致文字乱码。
# 错误做法 ❌page_image.save("output.pdf", "PDF")# Pillow 的 PDF 输出使用 JPEG 压缩(DCTDecode)# 再次渲染时文字会模糊、变形
# 正确做法 ✓import img2pdfwith open("output.pdf", "wb") as f: f.write(img2pdf.convert(["page1.png", "page2.png"]))# img2pdf 使用 FlateDecode(无损压缩)# 文字清晰,无质量损失
这个坑是实际测试中发现的——用 Pillow 生成的 PDF,打印出来文字发虚,尤其是小字号的金额数字几乎看不清。
5.3 竖版 vs 横版
最初的方案是横版A4、左右并排两张发票。但实际打印后发现:
-
• 中国电子发票绝大多数是横向的(宽 > 高) -
• 横版A4左右并排,横向发票放进去后上下大片空白 -
• 打印出来效果很差,浪费纸张
改进方案:竖版A4、上下排列。横向发票正好填满竖版A4的宽度,空间利用率大幅提升。
改进前(横版A4,左右并排): 横向发票放进去后上下大片空白,浪费纸张。
改进后(竖版A4,上下排列): 横向发票正好填满宽度,两张发票上下排列,空间利用率大幅提升。
5.4 最终效果
-
• 51张发票(53页,部分发票有2页) -
• 合并为 27页竖版A4 -
• 每页上下各一张发票,间距合理 -
• 300 DPI,打印清晰 -
• 文件大小:26MB -
• 无损压缩,文字无模糊
六、为什么是 OpenClaw 而不是其他工具?
你可能会说:这些事情用 Python 脚本也能做啊。
确实可以。但区别在于:
1. 不需要写代码
传统方式你需要:
-
• 写 IMAP 连接和认证代码 -
• 处理各种中文编码(GBK、GB2312、UTF-8) -
• 解析 MIME 结构,提取附件 -
• 写 HTML 解析逻辑,提取发票链接 -
• 处理链接重定向、过期、登录拦截 -
• 用无头浏览器渲染 JavaScript 页面 -
• 从渲染后的页面提取 PDF -
• 写 PDF 渲染和合成逻辑 -
• 处理中文字体渲染问题 -
• 调试十几种边界情况
用 OpenClaw,你只需要说人话:
“帮我搜索邮箱里的发票,下载,合并打印”
AI 会理解你的意图,调用合适的工具,自动处理所有技术细节。
2. 自带踩坑经验
OpenClaw 的发票合并技能里,记录了大量实战经验:
-
• 不能用 Pillow 保存PDF — 会导致文字乱码 -
• 不能用 pdftoppm 渲染 GBK 中文 — 会显示方块 -
• 必须用 img2pdf — 无损压缩 -
• 竖版比横版更适合中国发票 — 空白最少 -
• IMAP 授权码不等于登录密码 — 很多人卡在这里
这些不是文档里能查到的,是实际踩坑后总结的。AI 把这些经验内化了,下次遇到同样问题不会重复犯错。
3. 可迭代、可记忆
第一次合并出来是横版的,你说”打印出来左右两边大片空白”,OpenClaw 会:
-
1. 理解你的反馈 -
2. 修改方案,改为竖版A4 + 上下排列 -
3. 重新执行 -
4. 更新技能文档,下次自动用竖版
这种”做-反馈-改进-记住”的循环,是 OpenClaw 作为 AI 助手的核心价值。它不是一次性的脚本,而是一个会成长的工具。
4. 解决的是真实场景
这不是 demo,不是 hello world,而是实实在在的日常需求:
-
• 报销整理发票 -
• 财务归档票据 -
• 出差费用汇总 -
• 个人记账分类
每个月花在发票管理上的时间,可能有2-4小时。用 OpenClaw,缩短到几分钟。
七、延伸:还能做什么?
发票合并只是 OpenClaw 解决实际问题的一个缩影。同样的思路可以应用到更多场景:
-
• 邮件自动分类 – 收到发票自动打标签、归档 -
• 报销单自动生成 – 发票信息提取、汇总表生成 -
• 财务报表整理 – 按月/按类别汇总消费 -
• 合同管理 – 搜索、下载、归档电子合同
关键不在于 AI 有多聪明,而在于它能接入你的真实工作环境——你的邮箱、你的文件系统、你的工具链——然后用自然语言驱动这一切。
八、总结
回到开头的问题:51张发票,从搜索到打印,传统方式需要半天,OpenClaw 需要多久?
一句话交代任务,几分钟自动完成。
但在这「几分钟」的背后,是 IMAP 协议对接、中文编码处理、邮件结构解析、附件提取、链接追踪、JavaScript 渲染、中文字体兼容、PDF 无损合成……一连串技术问题的自动解决。
这就是 AI 助手的真正价值:不是让你不用思考,而是让你不用踩坑。
那些你不需要知道的细节,AI 帮你搞定了。你需要做的,只是说一句话。
工具的价值,不在于它能做什么,而在于它能帮你省下什么。
本文所有操作基于 OpenClaw 开源项目,部署在个人服务器上,数据完全本地化,隐私安全有保障。
夜雨聆风