Python逆向发票查验平台:从抓包定位加密参数到批量自动化实战

发布时间:2026/10/5 12:53:43
Python逆向发票查验平台:从抓包定位加密参数到批量自动化实战
如果你跟我一样每天要和增值税发票打交道大概能理解这种痛手里二三十张发票要一张张打开查验平台、录入发票代码、发票号码、开票日期、价税合计、校验码再等页面转一圈给出结果手动抄到表格里。一张两分钟三十张就是一个小时中间稍微走神第27张可能就录错了。所以我很早就想用 Python 爬虫把这件事自动化。但真上手才发现发票查验平台不是那种打开就能直接 POST 的简单站点请求参数里藏着一套带签名逻辑的前端算法必须把它的生成过程“逆向”出来才能让 requests 模拟得像真人操作一样。这篇文章就把我完整走通的思路和代码骨架写出来——从开发者工具抓包定位加密参数到用 Python 组装合法请求、解析返回结果再到验证码、频率控制、批量并发这些实战里绕不开的问题。适合正在学爬虫、想接触逆向工程、或者有批量查验发票需求的朋友参考。1. 为什么盯上发票查验平台业务痛点与目标拆解1.1 从手动核验到自动化脚本我最初的场景是这样的每月底财务要对账手里攒了一批进项发票需要核验真伪有时候电商运营也要确认用户申请的发票是否真实开具。逐个打开网页录入五要素等结果再记录状态重复几十次之后整个人都是机械状态。更麻烦的是录入过程还不能出错。发票号码多一位少一位校验码大小写写错都会导致查验失败还得回头对原始票据。几次之后我就下了决心这类重复劳动应该交给脚本。所谓“逆向工程 Python 爬虫”在这个项目里其实就一句话——搞清楚网页背后到底发了什么请求、参数怎么生成、返回怎么解析然后用 Python 把这条链路完整复现出来。很多人一听到“逆向”就觉得门槛高实际上在发票查验这类场景里目标很明确接口就那几个参数也就几组比很多 App 的加密简单多了。你不需要逆向整个前端框架只需要盯住“发起查询那一刻”发生的所有事。1.2 平台到底在查什么在写代码之前得先明白发票查验平台要的是什么。通常一次查询需要这几项信息发票代码发票号码开票日期价税合计或不含税金额校验码这五个要素合在一起本质上是在问这张发票在税务系统里是否存在且这些票面信息是否一致。所以请求参数的设计是有业务逻辑的每个字段都不是平白无故出现的。理解了这一点你在抓包时就能快速对上号页面表单里填的那几个输入框最终会变成 POST 请求里的字段。你在浏览器里做的一切操作都是可以脚本化的脚本要做的就是用程序代替人手去填表、提交、读结果。1.3 先想清楚合规边界在动手之前我要先把边界说清楚这也是我做完这个项目之后最想提醒大家的只查询自己有权查询的发票比如自己公司收到的发票、报销单对应的发票。遵守平台的服务协议和访问频率限制不做大规模、高频抓取。不绕过身份验证不伪造票据信息不把技术用于黑灰产场景。如果平台提供了官方批量查验接口或合作通道优先采用官方方案。本文的所有代码和思路定位是“自动化你本来就会手动做的事情”而不是“攻击一个公共服务”。把这个心态摆正后续遇到验证码和频率限制时你的处理方式也会更理智。2. 逆向切入点从开发者工具到加密参数定位2.1 打开开发者工具先看请求全貌第一步永远是抓包。打开查验平台的查询页面按 F12 进入开发者工具切到 Network 面板勾上 Preserve log保留请求日志因为页面在提交查询前通常还会有一些初始化请求不保留日志很容易漏。然后正常填一张测试发票点查询观察整个过程。你会看到类似这样的请求序列打开页面时有一个 GET 请求用来加载页面 HTML 和 JS同时种下一些 Cookie可能还有一个获取图形验证码的请求有的场景才触发返回一张图片真正提交查询时会有一个 POST 请求Payload 里带着发票五要素外加一两个看起来不明所以的隐藏字段。我的习惯是先看最后一个提交查询的 POST 请求。点开它的 Headers看请求地址、请求方法、Content-Type再看 Payload 里有哪些字段。这里的关键是搞清楚哪些字段来自用户输入哪些字段是页面动态生成的。通常发票代码、发票号码、开票日期这几个是用户输入直接对应表单。但那个不明所以的隐藏字段就值得警惕了——它很可能就是加密签名是整个逆向工程的核心。2.2 定位加密参数的家找到可疑参数后下一步是找出它的生成代码。我的做法分三步在 Network 面板里复制这个参数名切到 Sources 面板按 CtrlShiftF 全局搜索。前端 JS 文件一般不会做太强的混淆搜索大概率能直接命中。如果搜不到就在发起 POST 请求的代码处下 XHR 断点在 Sources 面板右侧的 XHR/fetch Breakpoints 里添加包含接口路径的 URL 片段。这样每次真正发请求之前浏览器都会自动断住然后顺着 Call Stack调用栈往回找一层一层看是哪个函数构造了这些参数。在可疑函数里打 console.log 或者手动断点单步执行观察参数每一步是怎么变出来的。这个过程没有太多捷径就是耐心。我第一次定位时花了大半个小时在一个混淆过的 JS 文件里翻来翻去后来发现真正有用的函数只有十几行前面大半段都是干扰代码。所以我的建议是不要试图读懂整个 JS 文件只关注和请求参数有关的那些函数其他的全跳过。2.3 还原签名逻辑排序、拼接、加密不同平台的签名逻辑各不相同但发票查验这类网站往往有一个共性把请求参数按一定规则排序拼接加一段固定字符串作为“盐”再做一次哈希或对称加密最后把结果作为签名参数带上有时还会附加当前时间戳防止请求被重放。这里给一个通用思路的示意代码实际平台的算法需要你自己按抓包结果去还原import hashlib import time def build_sign(params: dict, salt: str) - str: # 1. 参数按 key 的字典序排序 # 2. 拼接成 k1v1k2v2 的形式 raw .join(f{k}{params[k]} for k in sorted(params)) # 3. 拼接平台下发的固定盐值 raw raw salt str(int(time.time() * 1000)) # 4. 做哈希常见的有 MD5 / SHA1 / SHA256 return hashlib.md5(raw.encode(utf-8)).hexdigest()你可能会问平台为什么要在前端做这么一层主要目的有两个一是防止请求被篡改二是防止别人直接脱离页面脚本调用接口。所以签名算法还原得准确与否直接决定了你的请求能不能通过校验。常见的坑是排序规则不是字典序而是按固定顺序拼接时不光有参数还有固定的前缀或后缀加密前先做了 URL 编码时间戳用的是毫秒而不是秒。这些细节差一步签名结果就不一样。我建议你还原的时候先把平台自己发出去的请求参数原样保存下来自己写个脚本尝试复现同样的签名值能对上再往下走。顺带提一句网上“免费 python 源码大全”里确实能搜到各种发票查验相关的脚本但很多拿回来直接跑不通——原因基本都出在签名块要么写死了参数顺序要么盐值已经过期要么时间戳格式不对。这种代码的参考价值有限真正有价值的是你自己定位签名逻辑的过程。3. Python 落地Session、签名请求与结果解析3.1 先把运行环境准备好我用的环境很常规Python 3.8主要依赖是 requests、BeautifulSoup4、lxml以及 openpyxl最后写 Excel 用。如果你在签名还原中遇到比较复杂的 JS 逻辑可以再装 pyexecjs用 Python 直接调用 Node.js 去执行还原出来的函数省去手工重写加密逻辑的麻烦。很多新手卡在环境配置上这里不展开讲只提醒一点如果你的代码需要执行 JS务必确保本机装了 Node.js并且在代码里配置好 execjs 的运行路径。VSCode 里配置 Python 环境、安装解释器这些基础操作教程很多十分钟就能搞定不值得在这里占用篇幅。3.2 Session让 Cookie 和 Token 自动流转发票查验平台和大多数政务类网站一样靠 Cookie 维持会话。你在浏览器里能正常查询是因为页面初始化时已经建立了一套会话上下文脚本直接上来就 POST很容易被判定为无会话请求直接拒绝。所以我的代码里第一步永远是创建一个 requests.Sessionimport requests from bs4 import BeautifulSoup session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36, Referer: https://inv-veri.chinatax.gov.cn/, # 按实际域名替换 }) def init_session(): # 先访问一次查询首页拿到会话 Cookie 和页面里的动态参数 resp session.get(HOME_URL, timeout10) soup BeautifulSoup(resp.text, html.parser) # 这里根据页面结构提取后续请求需要的隐藏字段或 token token soup.find(input, {name: token})[value] return token这里有几个细节值得注意一定要设置 User-Agent很多站点对无 UA 的请求直接拦截。Referer 也尽量带上模拟从页面发起的请求。首页 HTML 里可能会藏着后续 POST 需要的动态参数比如一个 session token 或者随机数需要先用 BeautifulSoup 解析出来。不要图省事每次查询都新建一个 Session那样等于每次都重新开浏览器不仅慢还容易触发风控。3.3 组装请求并完成一次查询当签名逻辑和会话链路都理顺之后查询函数本身就很直白def query_invoice(invoice_code, invoice_number, invoice_date, amount, check_code): params { fpdm: invoice_code, # 发票代码 fphm: invoice_number, # 发票号码 kprq: invoice_date, # 开票日期 je: amount, # 价税合计 jyzm: check_code, # 校验码 } params[sign] build_sign(params, SALT) params[timestamp] int(time.time() * 1000) resp session.post(QUERY_URL, dataparams, timeout15) data resp.json() return data这段代码把前两步的成果串了起来Session 保证会话上下文sign 保证请求不被拦截timestamp 保证请求可重放窗口。实际开发中QUERY_URL 和 SALT 等常量需要你在逆向阶段拿到手后填入。3.4 结果判定与业务落库接口返回的内容一般是 JSON里面带状态码和消息。不同状态下返回结构略有差异我把常见情况整理成了表格状态含义处理建议查验通过发票真实且要素一致标记为正常等待后续入账查验不一致发票存在但要素对不上人工复核原始票据查无此票系统中没有对应发票优先排查录入错误再考虑是否为假票请求失败网络或参数问题按错误码重试或告警拿到返回结果后不要只打印在终端里建议直接落库。我习惯写到一个 CSV 或 Excel 文件每一行对应一张发票记录发票代码、号码、查询时间、返回状态、原始消息。这样既方便后续对账也方便出问题时回溯。4. 反爬与稳定性验证码、频率控制和异常排查4.1 出现验证码先别想着绕过这可能是整个项目里我最有感触的部分。第一次跑批量脚本时查了二十多张就弹出了验证码。当时第一反应是找识别方案后来想明白一件事验证码出现不是平台针对你个人而是你的请求频率触发了风控规则。正确做法是降低频率让请求间隔更接近人工操作的速度。如果验证码频繁出现直接停一会儿再继续必要时休息几分钟。在工程上这种策略叫“退避”backoff比任何识别技术都稳妥。你可能觉得这样很慢但发票查验本来就不是抢票场景稳定比速度重要得多。设置再好的验证码识别一旦被风控标记后面整个会话都可能被限制得不偿失。4.2 频率控制和指数退避怎么设计我最终采用的是一套带指数退避的重试机制。核心逻辑是如果请求遇到验证码或限流错误就等一段时间再重试并且每次失败后等待时间翻倍直到达到上限import time def query_with_retry(func, max_retry5, base_wait2): for attempt in range(max_retry): try: return func() except RetryableError as e: wait_time base_wait * (2 ** attempt) print(f触发限流等待 {wait_time} 秒后重试{e}) time.sleep(wait_time) raise RuntimeError(重试次数耗尽需要人工介入)实际运行中这个策略解决了我 80% 的稳定性问题。你不需要追求单次请求有多快只要保证整体流程能跑完就是胜利。4.3 失败与异常的排查顺序脚本跑久了总会遇到失败。我的排查顺序基本是固定的按照从外到内的原则网络问题超时DNS 解析失败先确认自己的网络环境正常。参数格式发票代码有没有转成字符串金额是不是保留了两位小数日期格式是否和平台要求一致会话过期Session 里的 Cookie 是否失效重新调用 init_session() 能否恢复签名过期/错误时间戳是否过期盐值是否已经更新签名算法是否与当前版本一致平台改版页面结构、接口路径、参数名是否发生了变化经验告诉我前三类问题占了绝大多数。尤其是签名里带时间戳的一旦脚本暂停了一段时间再继续跑时间戳过期会导致所有请求都被拒绝但表面上看起来就是“查询失败”很容易误判成网络问题。4.4 日志与无人值守批量脚本一旦跑起来经常是挂在后台或者定时任务里。这时候没有日志出了问题很难复盘。我用 Python 自带的 logging 模块把每次查询的请求参数、返回结果、耗时、异常全部记下来import logging logging.basicConfig( filenameinvoice_check.log, levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s ) logging.info(查询发票 %s 结果%s, invoice_number, result)如果公司有企业微信或钉钉我还会加一步当重试次数耗尽或者连续失败超过阈值时通过机器人 Webhook 推一条告警到群里。这样脚本挂在服务器上跑出问题第一时间就能知道不需要每天打开电脑去看输出。5. 进阶扩展批量轮询的并发设计与周边集成5.1 并发设计协程、多进程、分布式到底选哪个网上关于“爬虫并发设计到底哪个好”的讨论很多结论永远是四个字看场景。发票查验平台这类公共服务瓶颈根本不在你的本地 CPU 或网络 IO而在对方服务端的频率限制。你把并发从 1 提到 10成功率并不会线性提升反而更容易触发验证码和风控。我的实测结论是同步循环加 1 到 3 秒随机间隔最稳如果量比较大可以用协程把并发控制在 2 到 3 个收益已经到头了。多进程在这个场景里没有任何优势因为任务本身是 IO 密集型的而且多进程会带来 Cookie 隔离问题——每个进程的 Session 是独立的反而更麻烦。至于分布式爬虫那是跨站点、千万级数据采集才需要考虑的架构。项目上到分布式意味着你要处理任务队列、节点调度、去重、结果汇总一堆问题对发票查验这种单站点小规模任务来说完全是过度设计。5.2 批量查询的正确打开方式批量查询时我建议分三步走而不是写一个 for 循环直接把所有发票倒进去把待查验的发票列表随机打乱。原因很简单连续查询同号段、同开票日期的发票行为特征太明显容易触发风控。按批次处理每批 20 到 50 张批与批之间停顿 10 到 30 秒。每批结束输出一个统计已查多少、通过多少、失败多少、失败原因分布。伪代码如下import random invoices load_invoice_list(invoices.xlsx) random.shuffle(invoices) BATCH_SIZE 50 for i in range(0, len(invoices), BATCH_SIZE): batch invoices[i:i BATCH_SIZE] for item in batch: result query_invoice(**item) save_result(item, result) print(f批次 {i // BATCH_SIZE 1} 完成已处理 {min(i BATCH_SIZE, len(invoices))}/{len(invoices)}) time.sleep(random.uniform(10, 30))打乱顺序这个细节是我在踩了多次验证码之后总结出来的。别小看这个随机化它和随机 User-Agent 一样都是让行为更接近真实用户的有效手段。5.3 接入 Excel 和 IM 机器人脚本的最终价值是让不懂编程的同事也能用。我的做法是把结果写回 Excel然后推一条汇总消息到群里。Excel 部分用 openpyxl 就能搞定from openpyxl import Workbook wb Workbook() ws wb.active ws.append([发票代码, 发票号码, 查询时间, 查验结果, 原始消息]) for row in results: ws.append(row) wb.save(查验结果.xlsx)推送消息到企业微信或钉钉更简单requests 直接 POST 一个 Webhook 地址即可webhook_url https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxxx def send_alert(text): requests.post(webhook_url, json{ msgtype: text, text: {content: text} })这样一来财务同事只需要把发票清单丢进 Excel跑一次脚本就能在群里收到汇总结果完全不接触代码。这也是我建议所有爬虫工具都该有的思路一定做成一个能交给别人用的东西而不是只有自己能跑的命令行玩具。6. 踩坑实录与平台改版后的快速恢复6.1 三个最容易翻车的细节第一个坑是金额精度。发票上的价税合计经常是“1234.50”这种格式但有些接口要求保留两位小数有些要求以“分”为单位还有的会把“0”省略掉。我最初直接把浮点数转成字符串传上去结果签名一直对不上。后来统一用 Decimal 处理金额再按平台要求的格式转字符串问题才解决。第二个坑是日期格式。发票查验要求填开票日期但不同页面展示的格式可能是 20250101、2025-01-01 或 2025/01/01。如果平台内部用的是“YYYY-MM-DD”你传了“YYYYMMDD”签名算法里拼接出来的字符串就和页面不一致结果必然失败。所以参数格式必须和浏览器里实际抓到的完全一致。第三个坑是编码问题。校验码有可能是大写字母如果代码里没做.upper()处理或者中文备注字段编码不一致拼接出的待签名字符串和浏览器里的就是两份不一样的东西签名校验不可能通过。遇到签名失败的怪问题时先把待签名的原始串打印出来和浏览器的请求一字节一字节对比很快就能发现问题。6.2 平台改版后的快速恢复流程没有任何一个爬虫项目能一劳永逸。发票查验平台在前端迭代上虽然不算频繁但也会调整页面结构、升级加密方式。最典型的改版迹象是脚本突然大面积失败但单张发票在浏览器里手动查询完全正常。我的恢复流程固定四步重新打开开发者工具对比现在的请求和代码里请求的差异锁定变化点。如果参数名变了全局搜索新参数名定位到新的生成位置。如果加密算法变了用 XHR 断点重新走一遍调用栈还原新逻辑。改代码后先用一张已知结果的真发票做灰度验证通过了再跑批量。这套流程其实比第一次逆向还快因为你对整体结构已经足够熟悉。真正难的从来不是逆向后还原一次而是长期维护它跟着平台一起变。6.3 关于这套方案我最后想说的做完这个项目我最大的体会是自动化查验这件事本身不难真正难的是让它长期稳定地运行。遇到验证码、限流、改版这些事第一反应应该是调整自己的频率和策略而不是想着怎么硬碰硬。把工具当成一个替你省时间的快捷键而不是一个替你抢资源的轰炸机这个项目就能用得长久也经得起合规层面的推敲。如果你只是偶尔查几张发票手动操作完全够用没必要写脚本。但如果你的工作量已经到了按百张计那花半天时间做这么一个小工具后面每一个月都能省回来。这种“一次投入、长期复用”的小项目恰恰是 Python 爬虫最值得做的方向之一。