简介:面向爬虫逆向与电商数据采集开发者,这份资料围绕拼多多anti-content参数展开,提供了JS解密与全站抓取代码思路。资源包共45个文件,压缩后约183KB,核心代码以16个Python脚本和7个JS文件为主,另有pyc、json、xml等运行配置与缓存文件,适合已有Python爬虫基础、希望突破Web端反爬限制的读者。内容以真实采集流程为线索,从搜索场景的anti-content参数定位开始,梳理了JS加载、补环境、动态调试等环节,并给出多个入口脚本和JS处理模块,方便对照理解参数生成与请求构造过程。包内还包含爬虫框架配置、README说明和项目结构文件,便于直接复现或改造到自己的数据抓取任务中。已有1425人学习或下载该资源,对于关注拼多多数据采集、JS逆向与自动化抓取的开发者来说,是一个结构紧凑、可直接落地的参考包。
1. pdd 爬虫的密钥在 JS 里:anti-content 到底是什么
做 pdd(拼多多)爬虫的人大概率都见过这种场面:requests 把请求发出去,返回的 JSON 里既不是商品列表也不是错误说明,而是errorCode=4001或者一串含糊的"request illegal"。问题不在 URL、不在 UA,而在请求体里多出来的那个参数——anti-content。这个参数不是后端随机生成的,而是前端 JS 在页面运行时动态算出来的签名,每次请求不一样,换一个参数值、换一个字段顺序,结果就完全不同。抓包截下来的 anti-content 基本没法重放复用,必须把生成它的那段 JS 抠出来,在本地用同样的逻辑算出来再拼进请求里。
本篇文章要解决的就是这条完整链路:怎么定位 anti-content 的生成入口、怎么把加密函数搬到本地执行、怎么围绕它搭一套可持续抓取搜索、商品、评价等全站数据的代码思路,以及最容易被忽略的几个风控和签名坑。适合已经能跑通普通 requests 抓取、但被前端签名卡住的人。
2. 从抓包到断点:定位 anti-content 的生成入口
2.1 先抓一条带 anti-content 的搜索请求
做 JS 逆向的第一步永远是抓包,不是翻源码。建议直接用电脑浏览器打开拼多多 H5 端,按 F12 切到 Network 面板,勾选 Preserve log,然后在搜索框输入一个词并点搜索。此时页面会发起一堆请求,真正包含商品数据的往往集中在/proxy/api/开头的接口里,比如搜索接口通常长这样:
https://mobile.yangkeduo.com/proxy/api/search?pdduid=xxx&xxx在这个请求的 Payload(或者叫 Request Body)里,能看到一个对象,其中必然带着anti-content字段,值是一长串看起来像十六进制的东西,长度可能是 32、64 或者更长,取决于算法。同时留意以下字段:
page=1&size=10&query=手机&sort_type=default&anti-content=xxxxxxxx参数说明:page和size是分页参数,query是搜索词,这些内容就是你后端请求要拼的参数。anti-content的值不是跳转链接里带过来的,也不是 Cookie 里现成的,它是一个由前端脚本用请求参数现场计算的结果。判断依据很简单:把请求复制成 cURL,稍微改一下page的值再重放,返回结果会变成非法请求——说明签名跟参数内容绑定,不能只保留旧签名。
2.2 在 DevTools 里搜参数名,找到赋值现场
拿到请求样本后,切到 Sources 面板,按 Ctrl+Shift+F 打开全局搜索,输入anti-content。这个操作会扫所有已加载的 JS 文件,定位到这个参数名第一次出现的位置。注意搜索时建议同时搜anti_content下划线写法,因为 JS 里变量命名可能有驼峰、下划线、中划线三种写法,后端接收时统一转成了anti-content,前端代码里大概率是anti_content或antiContent。
常见情况是:搜索到的第一个命中点出现在某个 chunk 文件里,代码大致长这样:
var s = { page: e.page, size: e.size, query: e.query, sort_type: e.sort_type }; s.anti_content = Object(d.a)(JSON.stringify(s));这里的Object(d.a)(...)就是真正的签名函数。d.a是一个被 webpack 打包过的模块导出,具体函数名在压缩代码里已经被简化,不能直接看出算法。此时不要急着把整个文件抠下来,先在上面的赋值那一行打一个断点,刷新页面再触发一次搜索,让断点停在签名前。停住后,在 Console 里手动执行:
JSON.stringify(s)记下这次序列化的结果,它就是等一下本地复算时要比对的标准输入。再在 Console 里直接调用Object(d.a)(JSON.stringify(s)),把得到的值和 DevTools Network 面板里的 anti-content 值对比,一致就说明定位准确。
2.3 顺着调用栈走:找盐、找算法、找入参
断点停住后,右侧 Call Stack 里能看到Object(d.a)是从哪个函数被调出来的。点进d.a的源码位置,观察它接收了什么参数、后面又拼了什么内容。常见的套路有三种:
- 直接对
JSON.stringify(s)做 MD5 或 SHA 系列的哈希,然后可能再拼一段固定盐值; - 对序列化后的字符串做一次 Base64 编码,再在末尾追加上时间戳后二次哈希;
- 入参加上一个 Cookie 里的值(比如
api_uid、webp或某种设备标识),形成用户绑定关系。
怎么判断是哪种?看函数体的字符特征:搜代码里有md5、sha、hex、digest这类词,或看它是否调用了window上的一个全局对象。更直接的办法是在那行调用代码上右键选“Add script to ignore list”以外的另一种操作——在函数底部打到 return,执行到 return 时,用鼠标悬浮变量,快速确认它返回的是纯字符串结果,还是依赖了window.navigator.userAgent这类的环境信息。
这一步原则上不要追求看懂全部代码,只需要记录三条信息:这个函数接收什么入参、有没有拼外部变量(Cookie、时间戳、页面环境)、输出是什么编码的字符串。这三条信息足够支撑后面的本地复算。
3. 把解密逻辑搬到 Node 里执行:补环境与最小调用
3.1 提取加密函数:把 chunk 抠出来
定位完成之后,接下来就是把生成 anti-content 的 JS 代码提取到本地。做法是切到 Sources 面板,找到所在文件,在文件内容上右键选择 Save As,保存为一个.js文件。但这个文件往往有几千行,因为它是 webpack 打包后的一个 chunk,包含了大量跟签名无关的业务逻辑。
不要整文件塞进 Node 里跑,会报一堆window is not defined的错,而且很不好排查。正确做法是先找出关键模块的导出方式。在断点处执行:
Object.keys(d)返回结果里通常有a、b、c等几个导出项,其中d.a就是签名函数。再执行:
d.a.toString()这一步很关键,它会把加密函数源码直接以字符串形式打印到 Console 里。把这个函数体复制出来,它可能长这样:
function(t) { var e = function(t) { var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {} , r = Object.keys(t).sort() , n = {}; return r.forEach(function(r) { n[r] = t[r] }), JSON.stringify(n) }(t); return (0, c.a)(e + "&_pdd_f=1&scene=search") }观察这个函数,它先对入参做了一次 key 排序后重新序列化,再拼接了一个固定字符串&_pdd_f=1&scene=search,最后调用c.a做了最终哈希。因此至少要提取两个部分:外层这个排序拼接逻辑,以及c.a对应的哈希函数。
这里遇到的现象很常见:c.a在同一个 chunk 文件里,直接把它源码也打印出来。如果c.a内又调用了e.t()之类的二级函数,就继续顺着往下打印,一直打到原生方法为止。多数情况最终会落到 MD5、SHA1、SHA256 或者它们的魔改版本。魔改版本的表现是:原生哈希算法被改了初始向量 IV,或者对输入字符串做了字节级替换,所以网上搜出来的标准 MD5 代码对不上。
3.2 用 Node 跑通最小签名:补 window 环境
把函数提取出来后,在本地建一个独立目录,结构如下:
pdd-crawler/ ├── sign/ │ ├── algs.js # 提取出来的哈希算法 │ ├── sign.js # 组装 anti-content 的入口 │ └── test.js # 本地验证脚本sign.js的内容思路是导出一个纯函数,输入为请求参数对象,输出为 anti-content 字符串,形式类似这样:
// sign/sign.js // 本地复算 anti-content 的入口,抽离环境依赖 const { hash } = require('./algs'); function generateAntiContent(params) { // 注意:这里的 Object.keys().sort() 排序逻辑必须和浏览器一致 const sortedKeys = Object.keys(params).sort(); const sortedParams = {}; for (const key of sortedKeys) { sortedParams[key] = params[key]; } const plainText = JSON.stringify(sortedParams) + '&_pdd_f=1&scene=search'; return hash(plainText); } module.exports = { generateAntiContent };逻辑说明:generateAntiContent接收的params是请求参数的普通对象,函数内先按 key 排序,再序列化,然后拼接盐值字符串,最后调用hash得到最终签名。参数说明:排序用的是默认的字典序,这是很多 JS 里Object.keys().sort()的默认行为,千万别改成自定义排序,否则结果立刻不一致。
test.js里放抓包拿到的真实请求参数,算一次签名,再和真实的 anti-content 做对比:
node test.js返回一致,说明最小签名已经跑通。不一致的情况下,优先排查两件事:第一,hash函数里的 IV 和填充方式是不是原生哈希;第二,盐值字符串是否拼错了位置,有的算法是先加盐再哈希,有的是哈希后再拼盐做二次哈希,打印出代码里每个字符串拼接处逐一核对即可。
还有一种情况是函数依赖了window.navigator.userAgent、document.cookie这类环境变量,直接跑会报错。解决办法不是在 Node 里补全套浏览器环境,而是看依赖的变量到底参与不参与签名计算。调试方法:在函数内打印navigator.userAgent的值,手动把这个值作为参数传进去,替代对全局变量的读取。大部分 PDD 搜索接口的签名不依赖 UA,依赖的是 Cookie 和固定的盐值,所以这一步要实测。
提示:优先尝试
vm模块执行原始 chunk 代码,是比较省事的做法,但调试成本高;我一般推荐先做纯函数提取,跑通后再考虑用 vm 跑完整 chunk。两种路径选一种即可,不用都做。
3.3 Python 侧调 Node:execjs 方案与失败时的备选
签名逻辑在 Node 里跑通之后,Python 侧最常见做法是用execjs调用 Node 运行时,直接复用上面的 JavaScript 代码。整个过程建议写成一个独立的签名服务模块,而不是每次请求时都重新加载 JS 文件。
import execjs import os # 加载签名模块,模块内导出 generateAntiContent with open(os.path.join("sign", "sign_bundle.js"), "r", encoding="utf-8") as f: JS_CODE = f.read() ctx = execjs.compile(JS_CODE) def get_anti_content(params: dict) -> str: # 调用 Node 侧生成的函数 return ctx.call("generateAntiContent", params)逻辑说明:execjs.compile会把 JavaScript 源码编译进一个运行时上下文,之后每次调用都在同一个上下文里执行,省去反复读文件的开销。参数说明:params必须与浏览器端传入签名的对象保持一致,包括字段名、字段值类型,例如数字、字符串、布尔值的区别会影响序列化结果。get_anti_content返回的字符串直接放进请求体里的anti-content字段即可。
execjs 有三个高频坑需要注意。第一个是运行时选择,本机一定要装 Node.js,默认的 JavaScriptCore 和 PhantomJS 在跑现代 ES6 语法时容易报错,建议显式指定:
execjs.get().name # 确认输出是 Node.js (V8)第二个是超时问题。正常签名计算不超过 50ms,如果加了 RPC 调用或依赖了较大的 chunk,可能耗时较长,需要设置超时。第三个是上下文变量污染,JS 里如果用了Math.random或时间戳参与签名,那同一个请求每次生成的签名可能都不同,这其实是正常的,只是会影响你对比验证时的注意力。
备选方案是:如果提取出的哈希算法是标准 MD5/SHA 系列魔改,可以直接用 Python 的hashlib复刻,不经过 Node。比如魔改点只在拼接盐值上,那 Python 侧只需要算一次标准哈希即可。完整 chunk 提取方案跑不通时,我一般先试这条路,代码少、容易排查,但前提是你必须确认算法没有被魔改到字节级。
4. 全站抓取的代码思路:从签名到落库
4.1 分接口梳理 anti-content 覆盖范围
PDD 的 H5 端接口并不全依赖 anti-content,但搜索、商品详情、评价列表这几个核心接口基本都带这个参数。全站抓取的代码思路,第一步不是写爬虫,而是先把接口清单列出来,确认每个接口的入参规格和签名规则。
以搜索和商品链路为例,常用接口如下表:
| 功能模块 | 接口路径(示意) | 关键入参 | 是否需要 anti-content |
|---|---|---|---|
| 搜索 | /proxy/api/search | query、page、size、sort_type | 是 |
| 商品详情 | /proxy/api/goods | goods_id、pdduid | 是 |
| 评价列表 | /proxy/api/reviews | goods_id、page、size | 是 |
| 店铺信息 | /proxy/api/mall | mall_id | 部分情况需要 |
| 轮播商品 | /proxy/api/carousel | 无 | 否 |
表格里的接口路径代表这一类接口的命名规律,实际项目里路径会带上渠道参数。注意观察所需签名这一列:并不是所有接口都走同一套generateAntiContent,有些接口的参数排序规则可能不同,盐值字符串也可能不同。所以代码里应该把签名逻辑按接口分类,而不是全局共用一个函数。
代码目录结构建议这样设计:
pdd-crawler/ ├── sign/ # 签名模块,按接口分类 │ ├── search.js │ ├── goods.js │ └── reviews.js ├── crawlers/ # 抓取模块 │ ├── search_crawler.py │ ├── goods_crawler.py │ └── base.py ├── storage/ # 存储模块 │ └── models.py └── config.py抓取模块的基类可以封装统一的请求逻辑,把签名生成、Cookie 管理、重试策略都收敛在一个地方。以搜索为例:
import requests import time from sign import get_anti_content class SearchCrawler: def __init__(self, cookies: dict): self.session = requests.Session() self.session.cookies.update(cookies) self.session.headers.update({ "User-Agent": "...", "Referer": "https://mobile.yangkeduo.com/", }) def fetch_page(self, query: str, page: int, size: int): params = { "page": page, "size": size, "query": query, "sort_type": "default", } anti = get_anti_content(params) params["anti-content"] = anti resp = self.session.get("https://mobile.yangkeduo.com/proxy/api/search", params=params, timeout=10) data = resp.json() if data.get("errorCode") != 0: # 非 0 的 errorCode 必须单独处理,很多是风控信号 return None return data逻辑说明:每次请求前实时调用get_anti_content计算签名,再把签名放进查询参数里发起请求。参数说明:User-Agent和Referer一定要配置齐全,很多风控逻辑先校验请求头再校验签名,缺一个字段页面行为和接口行为都会变化。注意errorCode的判断,这是抓包时最容易忽略的地方——HTTP 状态码 200 不代表数据正常,真正的状态码在 JSON 体里。
4.2 请求调度:登录态、Cookie、UA 与限速
全站抓取不是一次性跑完所有接口就结束,而是要持续采集新增商品、评价和价格变动。调度上要解决三个问题:登录态怎么维护、Cookie 怎么轮换、每秒最多能发多少个请求。
登录态这块,H5 端接口的鉴权主要靠 Cookie 里的几个标识字段加pdduid查询参数。保持登录状态不能只靠复制 Cookie,要先确认签名生成时是否读取了 Cookie 内容。如果读取了,说明签名和登录态绑定,换账号就要重新提取该账号的签名逻辑;如果没有读取,可以只维护一份公共 Cookie。
UA 建议准备一个池子,但不要频繁切。风控系统会看 UA 的变化频率与请求频率之间的关系,同一个 UA 每天固定 1000 次请求,比每 10 次请求换一个 UA 要安全得多。
限速是很多爬虫翻车的主要原因。PDD 的搜索接口高频请求的阈值并不高,同 IP 下每秒超过 2 次搜索请求,持续几分钟后就会触发验证码。保险的设置如下:
# 请求间隔控制:搜索接口是重接口,必须慢 SEARCH_INTERVAL = 1.5 # 搜索请求间隔 1.5 秒 DETAIL_INTERVAL = 0.8 # 详情页间隔 0.8 秒 MAX_RETRY = 3 # 单个请求失败最多重试 3 次 def fetch_with_retry(func, *args, **kwargs): for attempt in range(MAX_RETRY): try: return func(*args, **kwargs) except requests.exceptions.Timeout: time.sleep(2 * (attempt + 1)) except Exception: time.sleep(1) return None逻辑说明:fetch_with_retry是一个通用包装器,所有请求都走这个入口,失败后按指数退避重试。参数说明:SEARCH_INTERVAL控制的是两次搜索请求之间的间隔,这个值不是随便定的,至少从 1.5 秒起步,跑一晚不出验证码再逐步降;DETAIL_INTERVAL可以略快,因为商品详情页接口风控阈值通常高于搜索接口。
这里强调一点:请求调度里最值得投资的不是代理池,而是每个请求的间隔稳定性。抖动大比整体频率高更容易触发风控,所以建议用固定间隔加少量随机扰动,例如1.5 + random.uniform(0, 0.5)。
4.3 SQLAlchemy 落库:别把数据存成 JSON 文件
全站抓取到的数据结构不复杂,但量大、更新频繁,用 JSON 文件存会越来越难维护。这里用 SQLAlchemy 定义 ORM 模型,把搜索商品、商品详情、评价分开建表,方便增量更新和去重。
from sqlalchemy import create_engine, Column, String, Integer, Float, DateTime, UniqueConstraint from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime Base = declarative_base() class SearchGoods(Base): """搜索商品表:一个商品多次出现在搜索结果中时,只保留最新一条""" __tablename__ = "search_goods" id = Column(Integer, primary_key=True, autoincrement=True) goods_id = Column(String(64), nullable=False) goods_name = Column(String(512), default="") price = Column(Float, default=0.0) sales = Column(Integer, default=0) query = Column(String(64), nullable=False) page = Column(Integer, default=0) created_at = Column(DateTime, default=datetime.now) __table_args__ = (UniqueConstraint("goods_id", "query", name="uq_goods_query"),) engine = create_engine("mysql+pymysql://user:pass@localhost:3306/pdd?charset=utf8mb4") Session = sessionmaker(bind=engine) Base.metadata.create_all(engine)逻辑说明:这张表保存每次搜索抓取到的商品信息,用goods_id + query做唯一约束,同一商品在同一搜索词下重复出现时只保留最新记录。参数说明:price使用Float,因为拼多多前端返回的价格很多是带小数点的字符串,Python 侧先float()再入库更稳;query字段用于区分不同搜索词下同一商品的价格差异,如果不留这个字段,后续分析关键词价格走势会很吃力。
入库时用merge代替add,可以省掉先查后插的麻烦:
from sqlalchemy.orm import sessionmaker def save_search_goods(session, items, query): for it in items: row = SearchGoods( goods_id=it["goods_id"], goods_name=it["goods_name"], price=float(it["price"]), sales=int(it["sales"]), query=query, page=it["page"], ) session.merge(row) # 存在则更新,不存在则插入 session.commit()参数说明:session.merge(row)是 SQLAlchemy 里最实用的一个方法,它按唯一约束判断记录是否存在,存在就更新,不存在就插入。注意必须在SearchGoods类里定义好UniqueConstraint,否则merge会退化为简单插入,重复数据会不断累积。
评价表和商品详情表的结构类似,不再展开。建表之后,建议再加一个简单采集日志表,记录每次任务跑了多少请求、成功几条、失败几条、耗时多久。日志表会让后面排查问题轻松很多,没有日志的爬虫在跑了一周之后基本就是黑匣子。
5. anti-content 解密与抓取避坑:现象、原因、处理
5.1 签名对不上:请求体顺序变了
现象:本地用抓包的参数算出 anti-content,第三方请求工具里直接替换进去,返回errorCode=4001。
原因:JS 里的签名函数对入参做了Object.keys().sort()排序后再序列化。你本地传参时如果先存 dict 再转 JSON,字段顺序可能和浏览器不一致。还有一种是入参里有字段没传全,比如漏了sort_type,签名结果当然对不上。
处理:不要自己拼字符串,把抓包时的完整参数原样复制到 Python dict 里,再调用签名函数生成。每次新增字段时,先用抓包样本做回归对比,别直接上线。签名对不上时,第一步永远不是怀疑算法,而是逐字段核对入参。
5.2 拿旧签名重放:过期与非法请求
现象:把几分钟前抓包里的 anti-content 拿出来直接当固定参数用,前几次可能成功,一段时间后返回request illegal。
原因:anti-content 的入参里可能包含当前时间戳,或者签名本身绑定了一段限时的服务端 token,过期之后签名失效。这是最容易踩的一个坑,会让人误以为签名算法不稳定。
处理:每个请求都现场算签名,不做缓存。假如你的调度器把一组 URL 放进队列里慢慢消费,务必在请求发出那一刻调用签名函数,不要在入队时就算好。拿旧签名重放做测试只能验证签名绑定关系,不能作为生产方案。
5.3 Node 里跑 JS 报错:环境分支走错
现象:同样的 JS 代码,在浏览器里执行正常,放进 Node 里跑就报Cannot read property 'xxx' of undefined,或者算出来的签名跟抓包不一致。
原因:JS 函数里可能有一个环境判断,比如typeof window !== "undefined"时走一条分支,Node 环境下走了另一条分支。两条分支计算方式可能完全不同,返回结果也不同。
处理:打印函数内部typeof window、typeof document、typeof navigator的值,比对浏览器与 Node 的差异。最简单的方案是提取纯函数部分绕过环境判断,不用补全套环境。我之前遇到过一种情况,加密函数里调用了new Uint8Array()来构造字节数组,Node 里一样有,问题却出在TextEncoder的编码差异上,最终是显式传入utf-8参数解决的。
5.4 请求一多就出滑块:被风控盯上了
现象:前几百个请求都正常,把线程数从 1 加到 4 之后,返回的内容变成一段 HTML 验证页,或者接口直接返回errorCode=4004。
原因:同一 IP 的请求频率超过了阈值,或者请求指纹与浏览器行为差距过大。滑块页面的出现说明已经进入风控流程,不是签名问题。
处理:先降线程数,回到单线程跑半天观察。然后检查 Cookie 里的api_uid和pdduid是否一致,有的风控逻辑会校验客户端标识。最有效的规避手段是慢请求和稳定间隔,优先于任何代理方案。验证码出现后没必要硬解,换一个时间段继续跑,或者降低每天的采集总量。
5.5 返回 200 但不是真数据:错误码在 JSON 里
现象:请求没有超时,HTTP 状态码是 200,解析出的 JSON 里errorCode却是 4004 或者 5000,页面数据空白。
原因:PDD 接口的正常与异常状态都通过 JSON 里的errorCode表达,404、500这类标准 HTTP 错误码反而不常见。如果代码里只判断resp.status_code == 200,会把风控响应当正常数据处理,导致落库一堆空值。
处理:每个接口的解析函数必须检查errorCode。建议封装一个统一的响应校验函数:
def check_response(data): code = data.get("errorCode") if code != 0: raise RuntimeError(f"errorCode={code}, msg={data.get('errorMsg', '')}") if not data.get("result"): raise RuntimeError("result 为空,可能是数据字段变化") return data["result"]参数说明:errorCode为 0 时表示正常,非 0 时抛异常进入重试流程。加一个result非空判断,防止接口字段改名后静默失败。这类校验写清楚,后续维护成本会低很多。
6. 进阶技巧:用一次成功请求验证你的签名是否真生效
跑通单个搜索接口后,很多人下一个动作就是直接开全站抓取,但这会浪费一次很好的验证机会——你并没有确认签名到底是真的参与服务端校验,还是恰好被忽略了。验证方式很简单:找一条真实抓包记录,把原始请求的anti-content替换掉,替换成你自己写的一个错误字符串,然后重新发请求。如果返回错误码,说明服务端确实校验签名,你的签名逻辑是有效的;如果返回正常数据,说明这个接口的签名只是前端摆设,后端没校验,后续可以省去签名步骤。
替换测试时需要注意,anti-content值里不要带空格、换行,路径参数和请求体参数都要仔细核对。对于搜索接口,签名值和请求参数绑定,替换后大概率报 4001;如果某个小接口替换后依然返回正常数据,记下来,这个接口后续抓取时可以不走签名流程,降低依赖。
验证通过后,接下来值得花时间的是签名性能优化。在实际抓取中,搜索、详情、评价三个接口的签名调用频率会很高,每次调 Node 再回传有毫秒级开销,线程多了之后开销会被放大。我的做法是给签名模块加一个简单缓存,缓存键是参数对象的序列化结果,缓存有效期控制在 60 秒以内。注意有效期不能太长,因为签名里可能带时间戳,过期签名会导致请求失败。合理配置下,缓存命中率能达到 80% 左右,整体请求速度能提升一半。
最后一条经验:全站抓取前,先把单接口的日采集量验证跑出来。比如搜索接口连续跑 12 小时,记录触发验证码的次数、错误码分布、数据完整率,用这份数据反过来调整限速参数。不要等到整个采集框架搭完再暴露风控问题,那时候定位问题成本就高多了。选定一个慢但稳定的节奏,再逐步提高线程数,每一步调整后都要观察至少 30 分钟。这样才能在拼多多复杂的风控体系下长期稳定采集。希望这些经验能帮你少走弯路。
本文还有配套的精品资源,点击获取