ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

拼多多爬虫实战:anti-content参数逆向与全站数据采集框架

拼多多爬虫实战:anti-content参数逆向与全站数据采集框架 简介这是针对拼多多平台的一款爬虫源码压缩包聚焦于anti-content参数的解密逻辑与全站抓取思路适合有一定爬虫基础、想要研究JS逆向或电商数据采集的开发者。资源共45个文件包含16个Python脚本、7个JavaScript解密文件以及若干配置文件与说明文档整体体积仅183KB代码精简、模块划分清晰。已有1425人学习/下载。压缩包内提供了完整的参数解密实现、多个抓取入口脚本、Scrapy与Sanic配置样例以及README等辅助文档可帮助读者理解拼多多页面动态请求的签名算法掌握从请求构造、参数加密到数据落地的整体流程。同时针对反爬虫机制也给出了相应的应对思路适合用于学习与二次开发。1. 拼多多爬虫的 anti-content一个参数挡住九成采集需求的真实现状做爬虫的同行应该都有同感拼多多网页端的采集难度不在接口数量而在一个叫anti-content的签名参数。你在浏览器里点开搜索页、商品页、评论页Network 面板里每个请求都带着这个参数一旦缺失或过期服务端直接返回403或一段伪造的正常 HTML——页面看着没问题数据却全是空的。很多想抓 pdd 数据的团队项目就卡在这个黑匣子上。这份方案的核心就是把anti-content的生成逻辑从 JS 里抠出来用 Python 在本地复现再围绕它搭建一套能跑全站的抓取框架。内容包括三块JS 逆向定位与算法还原、请求链路的模拟、以及基于 SQLAlchemy 的数据存储。适合已经会 Python 基础爬虫、但没碰过 JS 逆向的人也适合想系统了解拼多多反爬体系、准备投入数据采集方向的技术决策者。下面按我的落地顺序讲每一步都能照着跑。2. 定位 anti-content 生成源头从搜索请求到 JS 断点的逆向路径2.1 先搞清楚 anti-content 在请求里的位置打开浏览器开发者工具切到 Network 面板在拼多多搜索框输入任意关键词过滤XHR请求能看到一个类似/proxy/api/...的接口。点开它的 Payload 或 Query String Parameters里面有几个关键字段anti-content一长串 Base64 编码的字符串通常几百到上千字符timestampUnix 毫秒级时间戳ver签名版本号sign部分接口还有单独的短签名。anti-content 的特点有两个。第一它不是固定值同一关键词第二次请求会生成完全不同的内容第二它和请求体里的filt、pdduid、搜索词等字段绑定你手动改一个关键词再拿旧参数重放会被直接拒绝。这里给新手一个判断基准anti-content 是动态签名本质上是前端把请求参数做了一次序列化后加上时间戳和盐值再经过哈希或加密生成的。所以抓包时不要只盯着它的值要盯它的生成函数。2.2 用搜索定位法找到加密函数所在文件不推荐一上来就全局搜anti-content因为拼多多 Web 端的 JS 文件做了混淆字段名经常被打散。我一般用两步定位第一步在 Sources 面板里对 JS 文件做格式化Pretty Print然后按关键词搜索。不要搜anti-content先搜timestamp、ver、sign这类常见伴生字段。找到同时出现这些字段的函数基本就是签名入口。第二步在函数入口打下断点重新触发一次搜索请求。当代码停在断点上时看 Call Stack 调用栈从最内层往外逐个看找到生成_antiContent或者r.generate的那一层函数。这个过程看起来是玄学其实有规律可循签名函数通常被封装在一个自执行的模块里外层包着webpackJsonp的回调。你可以在 Console 里输入webpackJsonp查看模块列表重点找包含md5、sha256、base64、hmac字样的模块——哈希和编码函数是签名的基础工具。2.3 扣代码而不是重写算法很多教程喜欢让你用 Python 重写一遍加密算法我强烈不建议这么做。拼多多的 anti-content 由多层算法组合而成包含自定义字符映射表、类 Base64 的变体编码、以及类似 HMAC 的哈希过程。你重写一遍某个字符表写错一个映射调试就得花掉两三天。常见做法是直接把相关 JS 函数整体抠出来用 Node.js 加载执行。具体操作是这样的// extract.js // 将浏览器里定位到的加密模块保存为独立文件后通过 vm 模块加载 const fs require(fs); const vm require(vm); // 这里替换为从浏览器控制台复制出的、包含加密函数的完整代码块 const code fs.readFileSync(./pdd_crypto.js, utf-8); const sandbox { console, setTimeout, Buffer, // 拼多多算法里常用到的全局变量 navigator: { userAgent: Mozilla/5.0 ... }, window: {}, document: { createElement: () ({ style: {} }) } }; vm.createContext(sandbox); vm.runInContext(code, sandbox); // 在浏览器里确认过的全局入口函数 const antiContent sandbox.generateAntiContent({ url: /proxy/api/search, data: { keyword: 手机, page: 1 } }); console.log(antiContent);这段代码的逻辑是把浏览器环境里缺失的 DOM API 用空对象补齐然后用vm.runInContext在隔离上下文里运行加密模块。补齐navigator和document是必须的——如果加密函数里读取了这些全局变量缺一个就会抛ReferenceError。跑通这段代码后你会得到和浏览器里几乎一致的 anti-content 值。验证方法很简单把脚本输出的值粘贴到浏览器请求的 Payload 里重放如果返回正常数据说明代码扣成功了。2.4 算法结构解析为什么是哈希和编码的组合以我逆向过的版本为例anti-content 的生成大致是这样一个流水线把请求参数按 key 的 ASCII 码升序排序拼成一个字符串对这个字符串做一次自定义的 Base64 变体编码字符映射表被打乱了顺序把编码结果加上时间戳和版本号再做一次 HmacSHA256最后把哈希结果再进行一次编码作为最终的 anti-content。前两步决定了同一个关键词每次生成的值都不同因为时间戳参与了第三步的哈希过程。后两步决定了服务端可以校验参数的时效性——时间戳超过一定时间范围直接拒绝。这个结构说明一个问题不需要知道算法内部为什么不直接用标准 Base64只需要知道它是「排序 → 编码 → 哈希 → 再编码」的嵌套结构就能在本地完整复现。真正的坑不在算法本身而在字符映射表——所以扣完整 JS 文件是最高效的路径。3. 拿到算法之后怎么抓全站请求链路、风控规避与数据落库3.1 全站抓取需要覆盖哪几类接口拼多多网页端的数据类型可以分成四类每一类的接口签名方式略有差异数据类型典型接口签名特征搜索结果搜索接口anti-content 与关键词、分页绑定商品详情商品详情接口anti-content 之外还有额外动态参数评论列表评论接口需要商品 ID 和翻页游标店铺信息店铺接口部分需要登录 Cookie全站抓取的框架设计应该围绕这四类接口展开而不是只针对搜索接口。原因很直接拼多多 Web 端的搜索接口有较强的反爬限制而商品详情和评论接口相对宽松从后者入手更容易跑通全流程。我建议按这样的顺序推进评论接口 → 商品详情 → 搜索 → 店铺。评论接口的入参最少只需要商品 ID 和翻页参数最适合用来验证你扣下来的签名代码是否通用。3.2 请求构造与签名生成的最小框架下面是一个可以直接运行的脚本骨架它把签名生成和请求发送拆成了两层import requests import time import json from urllib.parse import urlencode, quote class PddClient: def __init__(self, script_runner): # script_runner 是封装了 Node 签名脚本的调用器 self.script_runner script_runner self.session requests.Session() self.headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: application/json, text/plain, */*, Referer: https://mobile.yangkeduo.com/, Origin: https://mobile.yangkeduo.com } def fetch(self, url, params, refererNone): # 生成签名前的原始参数字典 payload { timestamp: int(time.time() * 1000), ver: 1, params: json.dumps(params, separators(,, :), ensure_asciiFalse) } # 调用 Node 脚本生成 anti-content anti_content self.script_runner.generate(payload) # 最终发送时把签名参数拼进请求体 payload[anti-content] anti_content self.headers[Referer] referer or https://mobile.yangkeduo.com/ resp self.session.post(url, headersself.headers, datapayload) return resp.json()这段代码把签名和发送解耦script_runner.generate从 Node 脚本获取签名值而请求的构造在 Python 侧完成。注意两个细节params的 JSON 序列化必须用紧凑格式因为 JS 里的签名函数是基于紧凑 JSON 做哈希的多一个空格签名就不同timestamp要在签名前生成并和发送请求时保持同一个值。从这段代码能看出整个抓取框架的分层思路签名层独立成服务请求层只负责组装参数和发送。后续不管是加代理还是加多线程都只需要改请求层。3.3 让 Node 脚本与 Python 高效协作Python 调用 Node 脚本的方式有几种我按实际项目中的稳定性排序说明。第一种是每请求一次就subprocess调用一次 Node。代码简单但性能很差每次调用要拉起一个 Node 进程500 毫秒起步多线程下会卡死。第二种是用flask把签名函数包装成一个本地 HTTP 服务Python 侧通过requests调用。这种方式性能好Node 进程常驻响应在 1 毫秒级别。缺点是本地端口需要额外管理。第三种是用execjs库直接执行 JS。不用管子进程坏处是它依赖系统 Node 环境在某些 Linux 服务器的精简环境下容易出问题。我推荐第二种因为全站抓取必然要上多线程或分布式签名服务独立成进程能避免频繁创建子进程的损耗。具体封装是这样# sign_server.py # 在 Node 端启动一个极简 HTTP 服务接收参数、返回签名 from flask import Flask, request, jsonify from subprocess import run, PIPE app Flask(__name__) app.route(/sign, methods[POST]) def sign(): data request.get_json() # 这里调用 Node 脚本把 data 传给 generate 函数 result run([node, gen_sign.js, json.dumps(data)], capture_outputTrue, textTrue) return jsonify({anti_content: result.stdout.strip()}) if __name__ __main__: app.run(host127.0.0.1, port8765)实际项目里这个服务的输入输出可以做得更细比如把参数分组传入让 Node 端自己拼 JSON。多线程抓取时所有线程共享这一个签名服务签名速度不再是瓶颈。3.4 会话维持与 Cookie 策略拼多多的接口大部分不需要登录但不带 Cookie 的请求很容易触发风控。从网络请求包来看有几个关键的 Cookie 是匿名请求必需的api_uid首次访问页面时由接口下发后续请求都要带上_nano_fp设备指纹生成后基本固定webp图片格式偏好影响不大但要保持一致。我的策略是先请求一次首页把Set-Cookie里的关键字段收集起来然后注入requests.Session。还需要注意拼多多对同一 Cookie 的请求频率有统计切换 IP 但没有对应 Cookie 时反而可能触发异常。def init_session(): session requests.Session() # 先访问首页获取初始 Cookie resp session.get(https://mobile.yangkeduo.com/, headers{ User-Agent: Mozilla/5.0 ... }) # 手动补上可能缺失的字段 session.cookies.set(_nano_fp, 生成的设备指纹字符串) return session设备指纹字符串不是所有环境都需要有些网络环境对_nano_fp的校验较严格。如果你发现不补指纹也能正常请求就先不要补减少变量。这类参数属于「被风控了再加」的典型——一开始不要全上出了问题再逐步加。4. 用 SQLAlchemy 把抓下来的商品数据沉淀成可查询结构4.1 为什么不用 Pandas 直接存很多做数据的人习惯把抓取结果直接存成 CSV 或 Excel。数据量小的时候没问题但全站抓取的商品数据通常包含商品 ID、标题、价格区间、评论数、店铺信息等十几个字段而且存在同一商品多次抓取、需要更新价格变化的场景。用 Pandas 处理这种增量更新场景会非常痛苦。SQLAlchemy 的价值在于它是一个 ORM 层让你用定义 Python 类的方式建表不用手写 SQL又能轻松处理主键冲突和字段更新。这也是为什么很多爬虫项目最终都会迁到数据库存储——因为抓取本身只是第一步数据的二次查询和分析才是价值所在。4.2 定义商品表和评论表from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, Text, UniqueConstraint from sqlalchemy.ext.declarative import declarative_base from datetime import datetime Base declarative_base() class Product(Base): __tablename__ pdd_products id Column(Integer, primary_keyTrue, autoincrementTrue) goods_id Column(String(64), uniqueTrue, indexTrue) # 拼多多商品ID goods_name Column(String(512)) min_price Column(Integer) # 单位分 max_price Column(Integer) sales_tip Column(String(255)) # 前端展示的销量文本 store_name Column(String(255)) # 店铺名称 category_id Column(String(32)) created_at Column(DateTime, defaultdatetime.now) updated_at Column(DateTime, defaultdatetime.now, onupdatedatetime.now) class Comment(Base): __tablename__ pdd_comments id Column(Integer, primary_keyTrue, autoincrementTrue) goods_id Column(String(64), indexTrue) comment_id Column(String(64), uniqueTrue) # 评论ID content Column(Text) rating Column(Integer) # 评分通常 1-5 comment_time Column(DateTime) user_name Column(String(255))注意商品表的设计goods_id加了唯一约束和索引因为同一个商品可能从搜索和详情两个入口抓到不要数据库中存两条。价格字段用整数分存储避免浮点数精度问题。时间字段设了default和onupdate这样每次更新记录都会自动刷新更新时间。4.3 数据入库时的去重策略抓取数据入库时最常见的坑是重复写入。SQLAlchemy 原生没有INSERT OR IGNORE这种 MySQL 专属语法但可以这样处理from sqlalchemy.dialects.mysql import insert def upsert_product(session, data: dict): # 使用 MySQL 的 on_duplicate_key_update 语法 stmt insert(Product).values(**data) stmt stmt.on_duplicate_key_update( goods_namedata[goods_name], min_pricedata[min_price], max_pricedata[max_price], updated_atdatetime.now() ) session.execute(stmt) session.commit()这段代码的核心是on_duplicate_key_update当goods_id已存在时更新价格和名称不存在时插入新记录。这样既能保留首次抓取的时间又能更新最新的价格信息是商品类数据比较合理的存储方式。4.4 评论表的分页存储与翻页游标拼多多的评论接口是游标翻页不是页码翻页。响应里会返回一个next_cursor字段作为下一次请求的参数。存储时要注意评论 ID 的唯一性因为同一评论可能在多页中重复出现。def save_comments(session, goods_id, comment_list): for item in comment_list: comment_data { goods_id: goods_id, comment_id: str(item[comment_id]), content: item[content], rating: item.get(rating, 0), comment_time: datetime.fromtimestamp(item[time]), user_name: item.get(nickname, ) } # 已存在的评论直接跳过 exists session.query(Comment).filter_by(comment_idcomment_data[comment_id]).first() if not exists: session.add(Comment(**comment_data)) session.commit()评论抓取的一个经验是先判断数据库里是否已有该评论 ID再决定插入还是跳过。不要用merge或无条件添加评论数据不会变化重复插入只会让数据库膨胀。5. 全站抓取踩坑实录多端风控、字段缺失与反爬升级的 5 个真相5.1 现象同一个签名在 PC 端能用在移动端被拒绝原因拼多多 Web 端至少有两套前端代码桌面端和移动端mobile.yangkeduo.com的签名算法不完全一致。你从桌面端断点抠下来的算法在移动端请求时服务端校验的参数组合不同。解决统一入口。全站抓取尽量只走一个端推荐移动端因为移动端的请求头更简单反爬策略相对宽松。不要为了省事在同一个脚本里混用两个端的签名逻辑。5.2 现象请求频率不高但突然大面积返回 403原因IP 被临时风控。拼多多的风控不是纯频率触发而是综合了 IP、设备指纹、请求规律的维度。你每 2 秒请求一次但每次都带同一个_nano_fp也会被识别为同一个设备在异常请求。解决给每个抓取线程单独生成一套 Cookie 和_nano_fp模拟「不同用户在不同设备上访问」的状态。IP 变化时Cookie 和设备指纹也要跟着一起换不能只换 IP。5.3 现象扣下来的 JS 在 Node 里跑签名结果和浏览器对不上原因大概率是缺失某个环境变量。拼多多的加密算法里可能会读取navigator.userAgent、document.referrer或者window.devicePixelRatio你补齐的navigator是空对象导致算法分支走了不同路径。解决不要手动拼接传给 Node 的参数。正确做法是在浏览器 Console 里执行JSON.stringify(window)把完整的窗口对象导出来作为 sandbox 的初始化数据传给 vm 模块。这样能最大程度规避玄学问题。5.4 现象字段频繁缺失同一个商品今天能抓到销量、明天抓不到原因拼多多接口返回的字段是不稳定的。同一个接口在不同版本的前端代码里字段名会变商品有活动、有价格区间时部分字段存在为空的情况。解决解析响应时不要用data[sales]这种硬编码方式改用data.get(sales, )兜底并且对所有字段名做一次字典映射。字段缺失时记录日志不要中断整个抓取流程。5.5 现象anti-content 算法升级旧代码突然全量失效原因拼多多会不定期更新前端 JS加密模块的文件名和算法结构都会变。最直接的信号是首页 JS 文件的 hash 变了或者请求里的ver参数变了。解决把签名 Node 服务和抓取逻辑彻底分离。算法升级时只需要更新 Node 端模块Python 端完全不用动。同时建议在签名服务里加一个监控定期检查签名结果是否合规——比如每 10 分钟用当前关键词生成一次签名然后实际请求一次接口如果返回非正常数据就触发告警。提示这条是长期维护的核心经验。爬虫项目的寿命取决于你能多快发现算法升级而不是你能多快破解新算法。6. 从单机脚本到可控采集验证入口、限速策略与合规边界6.1 全站抓取的正确验证方式先把单个接口跑通再把代码扩展成批量。我自己的验证流程是搜索接口跑通后先固定抓取前 5 个商品的详情与评论全链路数据都能正常落库再上分页和批量。不要一上来就全量跑。拼多多的数据量巨大搜索一个关键词能翻出几百页加上评论和店铺接口单机全量跑不完就会触发风控反而影响整个方案的稳定性。6.2 限速策略比风控阈值低一个量级成功的长期采集靠的不是突破反爬而是让流量看起来像真人。我用过最稳妥的参数是单个 IP 每分钟不超过 30 个请求每请求之间加 2 到 4 秒随机延时每个商品详情抓取后固定暂停 5 秒。import time import random def rate_limited_fetch(client, url, params): time.sleep(random.uniform(2.0, 4.0)) # 随机延时 resp client.fetch(url, params) if resp.get(error) or resp.get(status) 403: time.sleep(60) # 触发风控时退避更久 return resp这段代码的要点是风控退避时间远大于正常延时。如果遇到 403等一分钟再继续而不是换 IP 立刻重试——立刻重试会让风控系统认为你在主动对抗反而拉黑概率更高。6.3 合规边界的自我检查最后说一个我自己的教训。爬虫项目做好技术实现之外必须考虑使用边界。对于拼多多这类电商平台公开商品信息、价格和评论数据在行业内是可以分析的但需要注意只采集公开可见的数据不碰用户隐私字段控制请求频率不做影响平台正常运行的访问抓下来的数据仅用于内部研究分析不要做转售或恶意用途。做爬虫的人最该有的能力不是破解而是判断「这条数据该不该拿、怎么拿才不越界」。希望这一套思路能帮你在 pdd 数据采集方向上少走弯路把精力花在数据分析和业务落地上而不是反复和签名算法死磕。本文还有配套的精品资源点击获取
返回列表