☰
企查查爬虫实战:破解key、value与x-pid动态签名机制
2026/10/9 10:59:29 网站建设 项目流程

说点实在的,企查查这类的爬虫,技术难点从来不在“怎么发请求”,而在“怎么让请求看起来像真的”。我一开始照着网上教程套requests,结果被一串key、value、x-pid拦得死死的,返回的不是{"code":4001,"msg":"请求非法"}就是干脆空数据。后来花了两天时间把前端JS翻了个底朝天,才把这三个参数的关系理清楚。这篇就把整个分析过程和实操思路写透,给后来人省点时间。

先明确一点:本文讨论的是数据采集技术原理与前端反爬机制分析,所有内容仅供学习交流,实际使用请遵守目标站点的Robots协议和相关法律法规,别拿去做违法乱纪的事。

1. 企查查反爬体系拆解:为什么偏偏是这三个参数

1.1 前端请求链路先看明白

企查查的Web端数据加载,走的不是传统的服务端渲染,而是典型的SPA(单页应用)架构。你在页面上点“查一下”,浏览器不会刷新页面,而是由JS动态构造一个XHR请求打到后端接口,拿到JSON数据后再用前端框架渲染成表格。

这就意味着,所有关键数据都存在几个固定的API接口里,比如企业列表、工商信息、股东信息这些。理论上只要找到这些接口,直接请求就能拿到数据。但难点在于,请求并不是裸奔的——每次请求都得带上几个动态生成的参数,后端会先校验这些参数,校验不过直接拒绝响应,压根不给你数据。

我把完整的请求拆开看过,去掉Cookie和UA这些常规字段后,真正决定“活不活”的参数就三样:

  • URL里的key查询参数
  • 请求体里的value表单字段
  • Headers里的x-pid自定义头

这三个参数是成套出现的,缺一个或者算错了,后端直接不认。早期企查查的反爬还没这么严,有些教程里的代码写死一个固定key也能跑一两天,现在基本不可能了,因为这三个参数几乎每次请求都在变。

1.2 key、value、x-pid在请求里分别扮演什么角色

先说结论:key是“门票”,value是“请求内容”,x-pid是“会话标识”。但三者之间不是独立的,而是由同一套签名逻辑生成,互相咬合。

  • key:通常出现在查询参数里,看起来像一长串URL编码后的字符串,里面有 base64 的痕迹,解码后能看到timestamp(时间戳)、nonce(随机数)、x-service(服务名)之类的字段。它的作用是告诉后端“我这个请求是合法的”,本质是一个签名凭证。
  • value:这个字段最坑。它在POST请求体里,表面看是请求的业务参数,比如你要查的企业名称、页码,但实际经过了一层加密或编码,不是明文。直接往里塞{"name":"某某公司","page":1}是没用的,后端解析出来是个空对象。
  • x-pid:这是一个自定义请求头,后端用它来做会话维度的校验。但采集时x-pid是动态生成的,还跟Cookie里的某些值关联。如果只带固定的x-pid,请求频率一高就会被封。

我踩过最大的坑就是只盯住了key,以为把它搞定就万事大吉,结果x-pid没过,请求照样被拒。这三者必须作为一个整体去模拟,不能拆开来单独处理。

2. 核心参数来源定位:怎么把“key值未知”变成已知

2.1 抓包工具的正确使用姿势

很多新手上来就用Fiddler或Charles,结果抓到的全是乱码或HTTPS证书报错,搞了半天连请求都看到不完整的。我的建议是直接用Chrome DevTools,省事且直观。

打开浏览器的开发者工具,切到Network面板,勾选Preserve log(保留日志),因为SPA页面会在切换路由时清空网络记录。然后在页面上触发一次查询,比如搜索一个企业名称,找到对应的XHR请求。

这里有个筛选技巧:不要一个一个翻,直接在Filter框里输入api或/api/,企查查的接口路径里通常带这个关键字。我通常还会先清空一次日志,再触发操作,这样留下来的请求基本都是本次操作产生的,定位准确。

选中那条关键的XHR请求后,重点看三块:

  • Headers里的Request Headers:看有没有x-pid或类似的非标准头。
  • Payload(或Request):看提交的Form Data里有没有key、value。
  • Query String Parameters:看URL尾巴上跟的参数名是什么。

第一次抓的时候大概率会看到key那一长串字符,而value在Payload里显示为一段被转义的字符串。到了这一步,很多人会陷入一个误区:直接把这段抓到的key复制到代码里用。

千万别这么干。我试过,当时请求是成功了,但半小时后突然大面积失效,原因就是服务端会校验时间戳和nonce的时效性,旧的key过几分钟就作废了。所以Copy下来只能用来临时验证代码逻辑,不能作为长期方案。

2.2 从URL到Headers,逐步锁定生成位置

确定参数名之后,核心任务就是找到这些值是在哪段JS里生成的。

第一步,在DevTools的Sources面板里,按Ctrl+Shift+F(全局搜索),输入x-pid或x_pid。注意,有的代码会写headers['x-pid'] = xxx,有的会写headers['xPid'],大小写和下划线都可能不一样,多试几种写法。

如果运气好,直接就能搜到赋值语句,比如:

headers['x-pid'] = getPid();

然后跟进去找getPid这个函数的定义。但企查查这种体量的站点,前端代码是打包压缩过的,文件名可能叫app.8a9f3c.js,一打开全是1行几百KB的压缩代码。直接找函数名基本不可能,因为压缩后变量名都被替换成a、b、c之类的短名字了。

这时候就得换思路:用接口关键词回溯。在DevTools里点开那一条API请求,在Initiator(发起者)面板里能看到调用链。它展示了是哪个JS文件、哪个函数发起了这个XHR。点进去之后,在对应的代码位置打断点,刷新页面重新触发请求,就能在调用栈里看到参数是怎么一步步组装出来的。

这一步是整个逆向的核心,也是最费时间的。

2.3 断点调试与调用栈分析的关键细节

断点调试看起来高大上,实际操作起来有几个细节需要注意。

第一个细节:要在发起请求之前打断点,而不是在请求完成之后。因为等数据返回了,函数的调用栈已经结束了。做法是在Initiator里找到发请求的那一行,点行号设置断点,然后重新触发页面操作(比如再搜一次企业)。

第二个细节:看清作用域里有什么。断点停下来后,右侧的Scope面板会列出当前作用域的所有变量。你重点找名字里带key、sign、token、pid、timestamp、nonce的变量。如果某个变量值跟抓包抓到的key一模一样,那它就是你要找的目标。

第三个细节:用Watch面板跟踪变量变化。在Watch里添加/key/这样的正则表达式,就能实时看到跟key相关的所有变量的变化过程,省得手动翻作用域列表。

我记得当时就是从断点位置往上推了两层调用栈,发现key和x-pid是在同一个函数里生成的——先算一个加密串,然后一部分塞进URL,一部分塞进Headers,一部分塞进请求体。这就解释了为什么三个参数必须配套使用,因为它们压根就是一条生产线上的产品。

3. 解密思路:动态key与value背后的加密逻辑

3.1 加密参数的前世今生

从公开的逆向资料和我自己分析的结果来看,企查查这一类商查平台的签名机制,基本都遵循同一个套路:

  • 前端收集业务参数(查询条件、页码、时间戳、随机数)
  • 把参数按约定的顺序拼接成字符串
  • 经过某种摘要或加密算法(常见的有MD5、SHA系列、AES,或者自定义的Base64变体)
  • 再把加密结果加上原参数一起做编码,生成最终请求串
  • 后端拿到后,先检查时间戳是不是在合理窗口内,再按同样的算法重新计算一遍,比对一致才放行

为什么要这么设计?后端接口如果裸奔,任何人都能无限刷数据,服务器压力大,数据也容易被批量搬走。加一层动态签名后,起码能把门槛提起来——你不能直接在浏览器里改个页码就翻页,还得带上每次都变的签名。

key和value的差异在于载体不同。key通常承载的是“签名+时间戳”这类元信息,value承载的是“实际的业务数据”加了一层编码。有些版本里二者还会互相引用,比如key里带了一个加密因子,value用这个因子加密,后端校验时先解出因子再解value,甭提多绕。

3.2 常见混淆手段与识别特征

为了不让逆向的人太轻松,前端JS还会做混淆。识别混淆方式是决定后续工作量的关键。

先说最基础的:变量名压缩。这个在工程打包时默认就做了,基本没法避免。所有的getCompanyInfo都会变成a或b,阅读性归零。应对办法是不要试图读源码,而是靠断点看值。

再常见一点的是字符串拼接。搜x-pid搜不到完整字符串,因为源码里写的是:

var t = "x-"; var h = t + "pid";

这种散装字符串是专门对付全局搜索的。对策是全局搜索时只搜一部分,比如搜"pid"或"x-",有时候甚至要搜URL编码后的写法。

还有一类是加密函数特征。比如你看到JS里调用了md5()、sha256()、AES.encrypt,那基本能确认签名用的算法。但如果代码里写的是function S(a){ return btoa(unescape(encodeURIComponent(a))) },那可能是自定义的Base64处理。

我强烈建议:与其硬啃混淆代码,不如动态调试。动态调试的本质是让浏览器替你执行JS,你只需要在下游观察变量的值,比静态分析效率高一倍不止。

3.3 模拟生成时的参数计算与组装

当你通过调试摸清了生成逻辑,下一步就是把它翻译成Python代码。这里我不放完整的逆向代码,毕竟每个版本都在变,直接抄没有意义,但可以把共性的组装过程讲清楚。

一般在代码里,最终生成出来的key长这样(示例结构):

key=eyJ0aW1lc3RhbXAiOiIxNzM2MDAwMDAwIiwibm9uY2UiOiI4YzY3Y2I...

这串字符你看着眼熟吗?去掉URL编码后,开头是eyJ,这是Base64编码JSON的固定前缀(因为{"的Base64就是eyJ)。所以第一步永远是:解码看看里面到底是什么。

在Python里可以这样快速验证:

import base64 raw = base64.b64decode(key) print(raw)

如果解出来是{"timestamp": 1736000000, "nonce": "xxx", "x-service": "yyy"}这种结构,恭喜你,整条链路就豁然开朗了——你只需要在Python里重新构造这个JSON,再用同样的算法算出后面那截签名即可。

value的处理也类似,它可能是把完整业务参数JSON做了一次编码,也可能是在业务参数外又包了一层签名。判断方法很简单:把value解出来,看看里面是不是包含了你输入的企业名称,如果包含,说明只是编码;如果不包含、是一段乱码,说明是加密。

x-pid这个东西比较特殊。从经验看,它更像是一个会话级标识,跟Cookie、设备指纹、时间戳都有关系。有些版本里它就是把当前时间反转、加密一下得到一个类似pid的字符串。有些版本里它会跟服务器下发的某个Cookie值联动,请求前要先GET一次首页拿种子。我的建议是:如果断点里能看到独立的getPid()函数,尽量把它整个调用过程理清,别只抄返回值。

提示:逆向分析一个动态参数时,最快的路径不是通读全部源码,而是找到“生成点 -> 赋值点 -> 传输点”这条最短链路。其余代码哪怕看不懂,也不影响最终落地。

4. 实操过程:从抓包到跑通一次完整请求

4.1 第一步:先用浏览器手动复现

在写任何代码之前,先在浏览器里把请求完整复现一遍。这一步的目的不是拿数据,而是确认“参数的有效性”和“请求的顺序”。

我习惯这样操作:

  1. 打开无痕窗口,访问企查查首页。
  2. 打开DevTools,切Network,勾Preserve log。
  3. 手动搜索一个公司名称,找到XHR请求。
  4. 把请求的URL、Headers、Payload全部复制下来。
  5. 关掉页面,重新打开无痕窗口,再搜一次。

注意无痕窗口这个细节。因为普通窗口会复用之前的Cookie和缓存,第二次请求可能已经带着某些隐式参数了。用无痕窗口可以确保每次都是全新状态,便于对比两次请求里哪些参数是稳定的、哪些是动态的。

我对比之后发现:Cookie里的某些字段每次都不一样,x-pid每次也不一样,而key和value每次也都在变。这说明整条链路完全是动态的,任何一个写死的方案都撑不过一天。

4.2 第二步:用Python会话模拟请求

确认逻辑后,用Python的requests库搭建一个会话。核心要点是:用同一个Session对象发请求,让它自动管理Cookie。

基础框架长这样:

import requests session = requests.Session() # 1. 先访问首页,获取初始Cookie和种子值 resp = session.get("https://www.qcc.com/", headers=base_headers) seed = extract_seed(resp.cookies) # 2. 根据种子和当前时间生成key、value、x-pid key = generate_key(seed) value = generate_value({"name": "某企业", "page": 1}) pid = generate_pid(seed) # 3. 组装请求头和数据 headers = { "User-Agent": "...", "x-pid": pid, "Referer": "https://www.qcc.com/search?key=某企业", } data = { "key": key, "value": value, } # 4. 发起请求 resp = session.post("https://www.qcc.com/api/xxx/search", headers=headers, data=data) print(resp.json())

这里有几个容易错的地方,挨个说:

  • User-Agent必须跟浏览器一致,有些反爬会校验UA的版本和浏览器指纹。
  • Referer不能丢。SPA页面发起请求时,Referer是当前页面的URL,后端可能会校验这个来源是否合理。
  • 请求体里的数据格式要看清。企查查有些接口用application/json,有些用application/x-www-form-urlencoded,搞错了后端解析不了就报错。

至于generate_key、generate_value、generate_pid这三个函数的内容,就是你在第3节逆向出来的逻辑。如果逆向不完整,可以先写一个“临时函数”,把从浏览器里抓到的值硬编码进去,先跑通请求,再逐步替换成动态生成。

先连通,再完善,这是我在所有爬虫项目里坚持的原则。

4.3 第三步:处理Cookie、会话保持和频率控制

跑通一次请求不难,难的是跑通了还能持续跑。这里最大的坎就是Cookie和频率控制。

我第一次实现动态生成参数后,能正常拿数据了,但跑到第30次请求时,突然返回要求验证。排查后发现是频率太猛,同一个Session在几秒内连发了30次请求,风控直接盯上了。

解决方案分两个层面:

  • 代码层面:在每次请求间隔加随机延时,比如time.sleep(random.uniform(2, 4))。注意是随机延时,不是固定延时。固定延时会被统计规律识别出来,随机延时至少能干扰一下。
  • 策略层面:不要让一个Session工作太久,跑一段时间就换一组Cookie。我当时写了一个简单的Cookie池,每跑50次请求就重新开个Session,相当于换了一个“新用户”的身份。

关于IP这块我不展开,但自己测试的时候建议谨慎,别把个人常用IP玩进风控名单。真要大批量跑,优先考虑合规的代理方案,并且控制并发量。

4.4 数据解析:xpath与text函数提取关键字段

数据拿到手之后,就是解析环节。企查查的接口返回的是JSON,按理说直接解析JSON就够了,但有些场景下你会拿到HTML片段,或者某个字段里嵌套了HTML表格,这时xpath和text()函数就派上用场了。

Python里用lxml库做解析,示例:

from lxml import etree html = etree.HTML(response_text) names = html.xpath('//a[@class="company-name"]/text()')

这里text()函数的作用是提取标签内的文本内容。需要注意,text()匹配的是“直接子文本节点”,如果标签内部还嵌套了子标签,text()可能拿到空值或只拿到部分内容。这时候用string(.)更保险:

names = html.xpath('//a[@class="company-name"]/string(.)')

string(.)返回当前节点下所有文本拼接后的结果,不会因为嵌套标签而丢内容。

金融、风控、市场调研场景里,很多数据其实是混合结构的——JSON里嵌一段HTML,HTML里嵌一串JSON。这时候要灵活切换解析方式,不能只认准一种工具。我一般会写一个辅助函数,先判断字符串是JSON还是HTML,再走不同的解析分支。

5. 常见问题与排查技巧实录

5.1 高频报错速查表

实操过程中,我整理过一份高频问题表,直接贴出来:

现象可能原因排查方向
返回{"code":4001,"msg":"请求非法"}签名校验失败,key或x-pid算错了重新断点确认生成逻辑,核对时间戳是否正确
返回内容带了验证码链接频率过高触发风控降频、换Session,降低单个会话的请求数
请求直接超时或连接重置IP被限换网络环境,或调整请求间隔
返回空数组但状态码200value里的业务参数编码有问题解码value,确认业务参数是否被正确包进去
单独请求接口能通,代码里不通Header字段缺失或Cookie不带检查Referer、UA、Cookie的传递链路
key解出来是乱码,JSON结构都没有解压/编码层级没剥完多尝试几次URL解码、Base64解码,观察是否有嵌套编码

遇到4001的时候,别急着改代码。先在浏览器里把当前请求的key复制出来,拿到Python里跑一遍同样的请求,如果能通,说明是签名生成部分有BUG;如果也不能通,说明签名逻辑本身已经变了,得重新断点分析。

5.2 反爬升级的应对策略

企查查这类平台的反爬升级很勤快。今天能用的方案,下周可能就废了。我总结过几条应对经验:

  • 做好断点调试的功课,别依赖网上教程。教程里的代码只是某个时间点的快照,站点一升级就失效,而断点调试能力是通用的,不管参数怎么变,都能重新定位。
  • 把生成逻辑模块化。在代码里把key生成、value生成、pid生成、请求发送拆成独立函数,每次更新只需要改对应函数,不用全盘重写。
  • 保留历史请求样本。我习惯每次遇到异常,先把浏览器里的完整请求(URL、Headers、Body)存成一个文本文件,方便跟代码里的请求对比。很多BUG一眼就能看出来——字段名大小写、多了个空格、body格式错了,都是对比才能发现的。

还有一点:如果目标数据量很大,建议优先评估目标平台是否提供官方API或数据服务。很多商查平台现在都有付费数据接口,稳定性远高于自己去逆向。爬虫能解决“能不能拿到”的问题,但拿不到“稳定可持续”的保障。

5.3 合规与稳定性的平衡

最后说点容易被忽视但很重要的东西——合规和自我约束。

爬虫这个东西,技术本身是中性的,但用起来边界特别清楚。我在做数据采集项目时,给自己定了几条硬规矩:

  • 只采集公开可见的数据,不碰任何需要登录后才能看到的非公开信息。
  • 控制访问频率,不给目标服务器造成压力。我的标准是单IP并发不超过3,请求间隔不低于2秒。
  • 不对目标平台的核心业务(比如账号注册、支付流程)发起任何自动化请求。
  • 数据仅用于个人学习或合法业务分析,不转卖、不公开。

这几条规矩看起来保守,但能保证自己在安全线以内活动。毕竟爬虫的本质是“访问Web服务”,如果访问方式越界了,性质就变了。

最后分享一点个人经验

折腾企查查这套参数下来,我最大的体会有两点。第一,解密类的工作,80%的时间都花在“定位”上,只有20%花在“实现”上。定位做扎实了,后面的代码都是体力活;定位偷懒了,后面全是返工。第二,不要迷信某一个固定方案的“永久有效”。任何反爬对抗都是动态博弈,今天你解开了key和value,明天人家可能就换了算法、加了验证码。保持能快速定位问题、快速重新逆向的能力,才是这类项目里真正值钱的东西。

如果你也正在被x-pid折磨,我的建议是:先把浏览器断点玩熟,搞清楚这三个参数怎么生成的,再谈写代码的事。工具和库都是其次的,逆向思路才是核心。希望这篇记录能帮你少踩几个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询