
淘宝MD5爬虫乍一听像黑话其实就是把淘宝系接口里那套基于MD5的参数签名规则吃透再用 Python Requests 把商品数据、评论数据从 JSON 里稳定抓下来的过程。这个项目能解决一个很现实的问题很多新手打开浏览器开发者工具明明看到了接口返回的 JSON复制到代码里一跑却报签名错误卡在第一步就放弃了。这篇文章会从抓包、签名计算、JSON 价格解密到最终代码落地一层层拆开来讲适合刚入门爬虫、想搞懂电商平台接口签名机制的朋友参考。我会把踩过的坑一起写出来让你少走弯路。1. 项目全貌与核心思路拆解1.1 这项目到底在爬什么先说清楚一个概念淘宝MD5爬虫不是去爬 HTML 页面源码而是模拟 App 端或 H5 端的接口调用直接拿结构化 JSON 数据。这样做的效率比解析 HTML 高一个量级而且字段清晰、翻页方便是电商数据采集的主流思路。项目里涉及的数据目标通常包括这几类商品列表标题、主图、价格、销量、店铺名商品详情SKU、参数、库存、优惠信息评论数据评价内容、评分、追评、买家标签直播场景弹幕内容、在线人数、商品讲解片段。如果你对这些接口挨个抓一遍会发现一个共同点几乎每个请求都带着一个sign参数而且返回 JSON 里部分敏感字段并不是明文。这就是标题里“MD5”和“解密”这两个词的来历。签名不过关请求直接拒绝字段解不开拿到 JSON 也没法用。所以这个项目的本质不是“写一段 requests 代码”而是把“签名 解密 风控对抗”这条链路打通。你把这一套跑通之后换到其他电商平台思路是可以直接复用的。1.2 为什么绕不开 MD5 和 JSON 价格解密平台为什么要做 MD5 签名核心目的是防篡改、防伪造。服务端规定一种参数拼接规则客户端把请求参数加盐之后算出一个 32 位小写 MD5 值服务端用同样的规则重新计算如果对不上就直接拒绝。这样只要攻击者不知道盐和拼接顺序就很难伪造一个合法请求。JSON 里的价格字段加密目的也很直白防比价、防批量采集。你从页面源码里看到的价格往往不是真实成交价接口返回的price字段可能是经过编码的字符串需要找到前端的解密函数才能还原。这一层在电商爬虫里非常常见淘宝、京东、拼多多的部分接口都有类似机制。理解了这两个原因你就知道这个项目不能靠“运气”来做必须走完整的逆向分析流程先定位接口再还原签名算法再处理数据解密最后才能写稳定的爬虫代码。1.3 整体技术链路设计我在做这类项目时一般把整个链路拆成七个环节按顺序推进基本不会乱抓包定位在浏览器或抓包工具里找到目标接口参数分析梳理哪些参数参与签名、哪些是动态变化的签名还原通过 JS 代码定位签名函数还原 MD5 拼接规则请求模拟用 Python 按规则计算 sign 并发起请求字段解密针对加密的 JSON 字段定位并模拟解密逻辑解析入库用 XPath 或 JSONPath 提取字段落库保存稳定运行处理 Cookie 过期、限流、重试等异常。这套链路里最容易翻车的是第三步因为签名规则藏在压缩混淆的 JS 代码里定位它需要耐心。但大多数时候它就是一个普通函数找到函数名和参数拼接顺序就完成了八成工作。2. 抓包与接口参数分析2.1 工具链准备做这类逆向项目核心工具还是这几个实测下来够用Chrome DevTools 或 Charles/Fiddler抓包看接口Python 3.10写爬虫主逻辑requests、lxml、pycryptodomeHTTP 请求、解析、加解密Node.js跑 JS 解密代码、补环境Postman 或 Apifox单独调试接口参数。这里提一句前端依赖的安装。如果你需要用到 Node.js 分析 JS 代码安装依赖时经常会碰到网络慢的问题直接切换镜像源就行npm config set registry https://registry.npmmirror.com pnpm config set registry https://registry.npmmirror.com淘宝源和 npm 官方源同步速度很快日常开发和逆向调试都够用了。2.2 定位核心接口抓包的第一步不是乱点而是带着目的去操作。我的做法是这样打开目标页面按 F12 进入 Network 面板勾选 XHR 类型清空日志在页面里执行一次搜索或翻页操作在接口列表里找返回内容包含目标数据的请求观察 URL 和参数优先看那些名字里带list、detail、comment的接口。找到目标接口后先把完整的请求参数复制下来保存成 JSON 文件。这一步别偷懒后面写签名函数和请求模拟都要对照它来。参数里面通常会看到data、sign、timestamp、token等字段其中sign就是我们要重点处理的。2.3 关键参数梳理我整理过一个常见参数表不同端的字段名会有差异但大体逃不出这几个参数名示例值含义是否参与签名data一段 JSON 字符串业务参数主体通常参与timestamp173721600000013 位毫秒时间戳通常参与sign32 位小写字符串签名结果不参与token一段加密字符串会话令牌偶尔参与cookie登录态信息身份凭证不参与这里最容易踩坑的是data字段。很多人以为它就是一个普通 JSON直接塞进请求里就行但实际上它通常是序列化后的字符串在参与签名时还要保持原始字符串的顺序和格式多一个空格或少一个空格都会导致签名不一致。2.4 请求生命周期还原我把一次正常请求的全过程还原一下你就知道签名在哪个环节起作用了用户点击页面 → JS 收集业务参数 → 把参数按约定排序拼接 → 拼接盐值和时间戳 → 计算 MD5 得到 sign → 组装请求头发出请求 → 服务端按同样规则验签 → 验签通过后返回 JSON → 前端解密渲染页面。这里的关键在于“排序拼接”这一步。如果参数顺序错了客户端和服务端算出来的 MD5 值不一样请求就会被拒。所以逆向时的核心任务就是把这一步的顺序和盐值弄明白其他都好说。3. MD5 签名机制拆解与计算3.1 MD5 到底是什么MD5 是一种哈希算法输入任意长度字符串输出固定 32 位十六进制值。它有明显的雪崩效应哪怕原始字符串只改一个字符算出来的摘要也会完全不同。这使得它很适合做参数防篡改的校验值。不过要提醒一句大家习惯叫“MD5 加密”严格说这是“MD5 摘要”或“MD5 签名”它不是可逆加密。著名的 CVE-2004-2761 漏洞就是因为 X.509 证书里使用 MD5 做签名后来被构造碰撞攻击导致行业逐步淘汰 MD5 用于证书签名。但为什么现在很多业务接口还在用 MD5 做签名因为计算快、输出长度短而且只要引入时间戳和随机盐短时间内碰撞成本远大于收益对于分钟级有效的接口来说完全够用。理解这一点很重要。你在逆向签名算法时不需要纠结“MD5 不安全”这件事那是证书和密码学领域的问题业务签名场景里 MD5 依然是主流方案。3.2 常见拼接规则在淘宝系接口里我遇到过的签名拼接方式主要有三种第一种参数按 ASCII 码排序拼成keyvaluekeyvalue再在末尾拼接盐值a1b2c3secretxxxx第二种不拼接等号和连接符直接按顺序拼接键值a1b2c3secret第三种data 字段直接拼接 timestamp、token最后拼盐{json字符串}1737216000000tokensecret这三种方式在别的平台上也很常见。你要做的是在 JS 代码里找到sign md5(...)这类调用然后看(...)里到底拼了什么。3.3 用 Python 还原签名函数一旦确定了拼接规则用 Python 还原签名函数非常快。我举个通用的例子import hashlib import time def md5_sign(params: dict, secret: str) - str: # 1. 按键名升序排序 sorted_keys sorted(params.keys()) # 2. 拼接成 keyvaluekeyvalue 形式 raw_string .join(f{k}{params[k]} for k in sorted_keys) # 3. 尾部追加盐值 raw_string fsecret{secret} # 4. 计算 32 位小写 MD5 return hashlib.md5(raw_string.encode(utf-8)).hexdigest() # 示例 params { timestamp: str(int(time.time() * 1000)), data: {pageSize:20,pageNo:1}, token: abc123, } sign md5_sign(params, my_secret_key) print(sign)注意几个细节参数值必须转成字符串再拼接JSON 字符串要保持原始序列化格式盐值可能是固定字符串也可能是动态获取的需要从 JS 代码里确认。3.4 签名计算中的三个常见坑第一个坑是大写小写。有的接口要求 32 位小写有的要求大写。写代码时先看一眼之前抓包拿到的 sign 格式直接用hexdigest()默认是小写如果你想转大写加.upper()。第二个坑是时间戳的取值。有些接口签名用的是 10 位秒级时间戳有些是 13 位毫秒级参与拼接的格式必须和抓包时完全一致。我用脚本重放请求时经常因为时间戳格式不对导致签名校验失败。第三个坑是 URL 编码。data 字段里如果有特殊字符比如中文、引号、百分号参与签名前有些平台会做 URL 编码有些不会。这个只能靠反复试验或读 JS 源码确认猜是猜不出来的。4. JSON 价格字段解密思路4.1 加密字段长什么样接口返回的 JSON 里价格字段大概率不是直接给你一个19.90而是一段看不出规律的字符串。我见过类似这样的返回{ data: { itemId: 123456789, title: 测试商品标题, price: 9ZjK3dQwE1xV, quantity: 100 } }这里的price就是加密后的价格。要想拿到真实价格必须还原前端的解密逻辑。不要试图通过正则去猜加密字符串长度和明文价格没有固定关系猜不出来的。4.2 解密定位三步走定位解密函数我一般分三步第一步在浏览器里打断点或者直接在全局搜索price字段名找到渲染价格的代码位置。第二步看渲染前对这个字段做了什么处理通常会调用一个解密函数比如decodePrice(price)或decryptItem(price)。第三步把解密函数从混淆代码里抠出来在 Node.js 里补一个最小运行环境输入加密字符串验证结果。如果函数特别长不要试图一行行理解先完整复制跑通后再根据输出反推算法。很多时候前端只是做了简单的 Base64 变种解码、字符偏移或异或运算复杂度不高。4.3 用 Python 模拟解密拿一个我调试过的例子来说它的加密逻辑是先将明文字符串按字符偏移一定位数再进行 Base64 编码最后做了字符反转。解密反过来就行import base64 def decode_price(encrypted_str: str) - str: # 第一步反转字符串 reversed_str encrypted_str[::-1] # 第二步Base64 解码 decoded_bytes base64.b64decode(reversed_str) decoded_str decoded_bytes.decode(utf-8) # 第三步按偏移量还原字符 offset 3 original .join(chr(ord(c) - offset) for c in decoded_str) return original print(decode_price(3DdXd0ZkQwE1xV))当然这只是个演示例子不同接口的算法完全不同。但思路是一致的先在 JS 里找到解密逻辑再翻译成 Python。如果你补环境不顺利也可以直接在 Node.js 里把原函数跑通然后通过子进程调 Node 脚本这样最省事也不用担心翻译出错。4.4 字段含义速查表解析 JSON 时字段名在不同接口里命名规则不太一样但含义基本固定。我列了几个高频字段字段名含义常见类型itemId商品 ID字符串title商品标题字符串price / view_price商品价格字符串quantity / stock库存数字sellerNick卖家昵称字符串rateCount评价数量数字注意price和view_price可能同时存在前者是真实价后者是展示价解密逻辑也可能不相同要分开处理。5. 完整爬虫代码实现5.1 代码结构规划我建议把代码拆成几个独立模块别都堆在一个文件里。下面这个结构是我常用的清晰也方便扩展taobao_md5_spider/ ├── config.py # 配置文件请求头、Cookie、盐值、目标URL ├── sign.py # MD5 签名函数 ├── spider.py # 请求封装Session、重试、限速 ├── parser.py # 解析逻辑JSON 字段提取、价格解密 └── main.py # 主入口翻页、调度、入库这样后续加功能时只需要改对应模块不会牵一发动全身。5.2 核心代码实现config.py 里放基础常量HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/, Accept: application/json, text/plain, */*, } COOKIE 你的Cookie SECRET 从JS代码里还原的盐值 API_URL https://api.example.com/mtop.some.list/sign.py 负责签名计算import hashlib import time def build_sign(params: dict, secret: str) - str: sorted_keys sorted(params.keys()) raw .join(f{k}{params[k]} for k in sorted_keys) raw fsecret{secret} return hashlib.md5(raw.encode(utf-8)).hexdigest() def build_params(data: dict, token: str): timestamp str(int(time.time() * 1000)) params { data: data, timestamp: timestamp, token: token, } params[sign] build_sign(params, SECRET) return paramsspider.py 里做请求封装和重试import time import random import requests from config import HEADERS, COOKIE, API_URL session requests.Session() session.headers.update(HEADERS) session.cookies.update({cookie: COOKIE}) def fetch_with_retry(params, max_retries3): for attempt in range(max_retries): try: resp session.get(API_URL, paramsparams, timeout10) if resp.status_code 200: return resp.json() except requests.RequestException: pass time.sleep(random.uniform(1, 3)) return None这里有两个细节一是用Session保持连接避免频繁创建连接带来的性能损耗二是重试时间用随机值而不是固定时间能降低被风控误判的概率。5.3 XPath 解析与字段提取如果你拿到的数据不是 JSON 而是 HTML或者 JSON 里嵌套了 HTML 片段就要用 XPath 来提取。热词里提到的text()函数这里正好用得上。text()是用来取节点文本的最常见的用法是//div[classtitle]/text()表示取这个节点下的直接文本。注意它不会取子节点里的文本如果你需要节点下所有文本要用.//text()。举个例子from lxml import html html_text div classtitle测试商品 span限购/span/div doc html.fromstring(html_text) # 只取直接文本结果是 [测试商品 ] direct_text doc.xpath(//div[classtitle]/text()) # 取所有后代文本结果是 [测试商品 , 限购] all_text doc.xpath(//div[classtitle]//text())很多时候你想提取标题用normalize-space()把空白也处理掉更省事title doc.xpath(normalize-space(//div[classtitle]//text()))这种方法写出来的 XPath 既稳又简洁比写一堆循环去拼接文本好用得多。5.4 主流程与限速控制main.py 里控制翻页和限速import time import random from sign import build_params from spider import fetch_with_retry from parser import parse_item, decode_price def main(): token 你的token for page in range(1, 11): data {pageNo: page, pageSize: 20} params build_params(data, token) result fetch_with_retry(params) if not result: continue items result.get(data, {}).get(list, []) for item in items: parsed parse_item(item) parsed[price] decode_price(item.get(price, )) print(parsed) # 每次翻页随机休息 0.5~2 秒 time.sleep(random.uniform(0.5, 2)) if __name__ __main__: main()跑通后你会发现核心难点还是那两步签名对不对、价格能不能解出来。这两个问题解决了后面的翻页和入库都是体力活。6. 高频问题排查与反爬对抗实录6.1 常见错误速查表我把自己在这个项目里踩过的坑整理成了表格遇到问题直接对照排查现象可能原因解决方向返回“签名错误”拼接顺序、大小写、编码不一致回放 JS 源码逐字核对拼接规则返回滑块验证请求频率过高或 UA 特征明显降低频率完善请求头考虑验证码处理提示登录过期Cookie 或 token 失效重新获取登录态做自动续期返回数据为空参数缺字段或被风控拦截用抓包数据对照比对完整参数请求超时IP 被限流放慢速度必要时换代理最气人的是第一种。签名错误本身不难处理但要找到错在哪里很磨人。我经常做的一件事是用抓包工具导出一条原始请求把它改写成 Python 字典然后逐字段与自己的代码输出做 diff减少盲目猜测。6.2 Cookie 续期与登录态管理淘宝系接口的 Cookie 有有效期平时大家说的“ck 续期”就是指登录态过期前的自动刷新。如果你爬的是需要登录才能看到的数据Cookie 失效之后所有请求都会变成“登录过期”或“滑块验证”。我的做法是单独写一个检查函数每隔一段时间检测一次关键 Cookie 字段是否存在以及是否接近过期时间import time def is_cookie_expired(cookie_str: str, expire_hours: int 24) - bool: # 这里只做逻辑演示实际需要根据具体的 Cookie 结构来解析 if expires not in cookie_str: return True return False续期的具体方案不能一概而论有些场景可以通过固定接口刷新 token有些则需要重新走一遍登录流程。自动化登录本身又是一个大话题我建议前期先用半自动方式手动登录后把新 Cookie 填到配置文件里跑起来能撑几个小时就够了。6.3 频率控制与分布式爬虫思路很多新手一上来就疯狂并发结果几秒钟就被封。频率控制不是可选项是必选项。我在代码里一般都会加随机延时而不是固定延时因为真实用户的操作节奏是有波动的。如果你确实需要大规模采集那就得考虑分布式架构。基本思路是把任务列表放到 Redis 里多个爬虫节点从 Redis 拉取任务抓取结果再写回 Redis 或数据库。这样做的好处是单节点被封时其他节点还能继续跑但前提是你已经能处理单节点的风控问题否则分布式只会让你被封得更快。6.4 合规边界要怎么把握这一点我放在最后说但它是最重要的。做爬虫项目尤其涉及电商平台时有两条底线很明确一是只用于学习研究不对外提供商业化的数据服务二是不采集个人隐私信息不抓取需要登录才能看到的用户私人数据。请求频率也要控制在对线上服务没有实质影响的范围。像淘宝这种大平台单机小流量地跑影响微乎其微但如果把握不好节奏被风控盯上封号只是时间问题。技术研究归技术研究别越界。7. 站在防守方Controller 层如何防爬7.1 为什么爬虫开发者要懂防爬热词里有一条是“java controller层 如何防护 防止爬虫”这正好说明很多人不只是写爬虫也在做反爬。其实这两个方向是同一个问题的两面你只有知道服务端会怎么防才知道你自己的请求为什么会失败。如果把爬虫当成攻击方那么服务端 Controller 层就是第一道防线。很多服务端新手只会查参数是否为空、用户是否登录完全没有防爬意识。结果就是爬虫开发者随便伪造几个请求就能拿到全量数据。下面分享几个适合在 Controller 层落地的防护手段。7.2 核心防爬手段服务端防爬最常用的手段有这么几类第一签名校验。在 Controller 入口先验证请求里的 sign 参数计算规则和客户端一致。签名不通过直接返回错误。第二时间戳窗口。签名里带上 timestamp服务端判断时间差超过 5 分钟就算过期。这样即使有人抓到了完整请求过几分钟也就失效了。第三接口限流。按 IP、按用户维度做频率统计比如一分钟最多 30 次。超过就直接拒绝或返回验证码。第四UA 校验。识别User-Agent是否来自真实浏览器排除常见的 Python requests、curl 等默认标识。这些手段单独用效果有限组合在一起就能挡住大部分低水平爬虫。真正高级的爬虫会去模拟浏览器指纹那就是另一层对抗了。7.3 Java 拦截器示例在 Spring Boot 项目里最简单的防爬实现是写一个拦截器。下面是一个验签和限流结合的示例Component public class AntiSpiderInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String sign request.getParameter(sign); String timestamp request.getParameter(timestamp); String secret 你的服务端盐值; if (sign null || timestamp null) { response.setStatus(403); return false; } // 时间戳窗口校验超过 5 分钟拒绝 long ts Long.parseLong(timestamp); if (Math.abs(System.currentTimeMillis() - ts) 5 * 60 * 1000) { response.setStatus(403); return false; } // 服务端重算签名 String sortedParams buildSortedParams(request.getParameterMap()); String expectedSign md5(sortedParams secret secret); if (!expectedSign.equals(sign)) { response.setStatus(403); return false; } // 简单限流基于 IP 的计数器一分钟最多 30 次 String ip request.getRemoteAddr(); if (!allowAccess(ip)) { response.setStatus(429); return false; } return true; } }这里的buildSortedParams和md5方法需要根据项目具体实现思路就是把所有参与签名的参数按字典序拼接再算 MD5。和客户端签名保持一致才能真正挡住伪造请求。7.4 防爬力度的平衡点服务端防爬有个常见误区以为防得越死越好。实际上如果对正常用户也误伤会直接导致业务投诉。更好的做法是分级处理第一级所有请求都做签名校验挡住乱写的脚本第二级对高频请求做限流返回 429 或提醒验证码第三级对明显异常的行为做封禁而不是对所有用户一刀切。这样既保证了数据安全又不会让正常用户觉得产品很难用。做防爬和做爬虫一样本质上都是对请求特征的深入理解这一块积累的经验反过来对理解接口设计也很有帮助。8. 最后聊几点个人体会最后说点实在话都是我自己踩出来的经验。第一不要低估签名逆向的复杂度。我第一次做类似项目时以为拿到接口参数就能直接算结果整整折腾了两天最后发现是漏了一个 data 字段内部的空格处理。当时我记得很清楚抓包工具里 data 的值经过 URL 编码后多了一个%20而我手写 JSON 时用的是普通空格两边 MD5 永远对不上。从那以后我每次写签名函数前都会先把原始请求导出来跟代码生成的请求做一次逐字符对比这才是最快的定位方式。第二对于价格解密优先在 Node 里跑原版 JS 函数。翻译成 Python 虽然显得优雅但混淆代码里有太多坑一个字符偏移看漏了就能让你怀疑人生。跑通原版函数之后再决定要不要翻译成 Python 或直接用子进程调用 Node。第三这类项目做出来之后记得让它慢一点。慢不是低效而是生存策略。控制请求间隔、控制并发数、及时处理 Cookie 过期比任何花哨的代理方案都重要。我自己现在跑数据单机最大并发都不会超过 5宁可多跑一会儿也不愿意看到账号被封。这个项目后续还可以扩展的方向很多比如加定时任务做持续监控、把数据接入前端做可视化大屏、用 Redis 队列把单机爬虫改造成分布式采集。但不管怎么扩展核心还是那套东西把请求签名搞明白、把数据解密搞定、把异常处理做好。这三件事做到位就已经超过大多数停留在爬 HTML 阶段的同行了。