一个 MD5,搞定闲鱼的反爬
我以前以为反爬是很神秘的东西——什么 IP 指纹、行为画像、深度学习模型,听上去都很高大上。直到我自己啃了一遍闲鱼 mtop 接口,才明白最常见的反爬,本质上就是一个 MD5。今天我把自己最早写的版本拆给你看,看完你会发现,所谓"算法解析",很多时候就是把浏览器做的一个小动作用 Python 重写一遍。
先看完整脚本
import requests, time, hashlib, json
import jsonpath, csv
search = "篮球鞋"
send_url = "https://h5api.m.goofish.com/h5/mtop.taobao.idlemtopsearch.pc.search/1.0/"
send_h = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
"referer": "https://www.goofish.com/",
"cookie": "<YOUR_XIANYU_COOKIE_HERE>", # 必须含 _m_h5_tk
}
payload = {
"pageNumber": 1, "keyword": search, "fromFilter": False,
"rowsPerPage": 30, "sortValue": "", "sortField": "",
"customDistance": "", "gps": "", "propValueStr": {},
"customGps": "", "searchReqFromPage": "pcSearch",
"extraFilterValue": "{}", "userPositionJson": "{}",
}
c_data = json.dumps(payload)
def get_sign(cookie_str, c_data):
# 1. 从 Cookie 里抠出 _m_h5_tk,取最后一段的 token
pairs = cookie_str.split("; ")
token_list = [item.split("=", 1)[1].split("_")[0]
for item in pairs if item.startswith("_m_h5_tk=")]
if not token_list:
raise ValueError("Cookie 内未找到 _m_h5_tk 字段!")
d_token = token_list[–1]
# 2. 生成 13 位毫秒时间戳
t = str(int(time.time() * 1000))
h = "34839810" # appKey
# 3. 拼原始串 + MD5
raw = f"{d_token}&{t}&{h}&{c_data}"
md5 = hashlib.md5(raw.encode("utf-8"))
return t, md5.hexdigest()
t, sign = get_sign(send_h["cookie"], c_data)
# 4. 把 sign 塞进 URL query,再 POST
xy_params = {
"jsv": "2.7.2", "appKey": "34839810",
"t": t, "sign": sign,
"v": "1.0", "type": "originaljson",
"accountSite": "xianyu", "dataType": "json",
"timeout": "20000",
"api": "mtop.taobao.idlemtopsearch.pc.search",
"sessionOption": "AutoLoginOnly",
"spm_cnt": "a21ybx.search.0.0",
"spm_pre": "a21ybx.search.searchInput.0",
}
datas = {"data": c_data}
res = requests.post(send_url, headers=send_h, params=xy_params, data=datas)
resdata = res.json()
# 5. jsonpath 跨层级抓数据
exContent = jsonpath.jsonpath(resdata, "$..exContent")
all_data = []
for i in exContent:
try:
all_data.append([
i["detailParams"]["title"][:15],
i["area"],
i["detailParams"]["soldPrice"],
])
except: pass
with open("闲鱼数据_篮球鞋.csv", "w", newline="", encoding="utf-8") as f:
cf = csv.writer(f)
cf.writerow(["标题", "地址", "价格"])
cf.writerows(all_data)
整个脚本的核心不是 requests,是那个 get_sign()。
mtop 是什么?一句话讲清
淘系(淘宝/天猫/闲鱼/支付宝…)的前端接口有一层统一的鉴权封装,叫 mtop。它的全称是 mtop.taobao.xxx,所有 H5 页面拿数据都走它。
mtop 的反爬不是 IP 指纹,不是人机验证,就是一个签名 + 一个时间戳:
sign = md5(token & t & appKey & data)
- token:从 Cookie 里抠出来的 _m_h5_tk;
- t:当前毫秒时间戳;
- appKey:写死在客户端里的常量字符串,闲鱼搜索是 34839810;
- data:本次请求的 payload 序列化后的 JSON 字符串。
四个东西一拼,MD5 加密,得到 32 位小写 hex,就是 sign。
为什么是 _m_h5_tk,不是别的 Cookie
token_list = [item.split("=", 1)[1].split("_")[0]
for item in pairs if item.startswith("_m_h5_tk=")]
Cookie 里这一长串 _m_h5_tk=f367c39421be01aa446b45d284ba96b0_1785247373395 里下划线前面那一段(f367c39421be01aa446b45d284ba96b0)才是 token。
为什么是这一段?因为服务端只认 token 这 32 位字符,后面的时间戳过期信息由客户端自己用,签名时不参与。
注意 token_list[-1]:cookie 字符串里 _m_h5_tk 经常出现两次(一个是明文、一个是加密版 _m_h5_tk_enc),永远取最后一个。这是闲鱼这个接口的"小坑",很多新人在这里栽过。
时间戳必须是 13 位毫秒
t = str(int(time.time() * 1000))
time.time() 默认是秒级浮点(如 1726204432.847),乘 1000 后是毫秒级整数。mtop 严格要求 13 位毫秒戳,少一位或多一位都会被服务端拒绝。
要不要担心时钟漂移?不需要。mtop 服务端的容忍窗口是 ±5 分钟,正常笔记本时钟不会差这么多。但如果你的脚本跑在树莓派或老机器上,先 ntpdate 同步一下时钟,否则大概率一直 FAIL_SYS_SESSION_EXPIRED。
sign 必须和 data 严格一致
raw = f"{d_token}&{t}&{h}&{c_data}"
这是签名公式。关键点:c_data 必须和 POST 请求里 data={"data": c_data} 的内容一模一样——包括字段顺序、包括空格、包括 {} 这种空字典的表示方式。
如果中间你为了"美化"调用了 json.dumps(payload, indent=2, ensure_ascii=False),服务端验签立刻失败。我当年第一次跑就因为加了 indent=2 被拒,调到怀疑人生才意识到是 JSON 格式不一致。
# 正确:c_data = json.dumps(payload)
# 错误:c_data = json.dumps(payload, indent=2) # 多了换行和空格
# 错误:c_data = json.dumps(payload, sort_keys=True) # 字段顺序变了
参数要塞对地方
xy_params = { "t": t, "sign": sign, "appKey": "34839810", ... }
datas = { "data": c_data }
res = requests.post(send_url, headers=send_h, params=xy_params, data=datas)
注意三件事:
- t、sign、appKey、jsv 等鉴权参数走 URL query(params=),不进 body;
- data 走 POST body(data=);
- 服务端会从 URL 里读 sign,从 body 里读 data,两边都要对得上。
混了任何一个地方,mtop 返回的都是同一句:FAIL_SYS_SIGNATURE_ERROR。
jsonpath 跨层级抓数据
exContent = jsonpath.jsonpath(resdata, "$..exContent")
$..exContent 是 jsonpath 的递归下降语法,意思是"从根节点开始,不限层级,凡是 key 叫 exContent 的全要"。mtop 返回的 JSON 结构非常深(用户数据/商品数据/模板数据嵌套十几层),用普通字典访问会很累,jsonpath 是为这种"深嵌套 JSON"而生的工具。
jsonpath.jsonpath() 返回的是一个列表,需要遍历才能拿到每个商品对象。
那段又长又丑的 Cookie,请你立刻删掉
和上一篇 B 站下载一样,这个脚本里的 cookie 也是真实凭据,里面含 _m_h5_tk、_tb_token_、unb、tracknick 等关键字段:
- _m_h5_tk:签名的原料,泄露等于别人能以你的身份调 mtop;
- _tb_token_:登录态绑定值,泄露等于账号白送;
- tracknick:你的昵称,泄露不算大碍但暴露身份。
正确做法和上一篇一致:挪进 .env,从 os.environ['XIANYU_COOKIE'] 读,.env 进 .gitignore。这是底线,不是建议。
mt 签名算法的本质
写到这儿你应该看出来了,mtop 这套反爬并不"反":
- 算法公开(前端 JS 源码就在浏览器 Network 里);
- 公式简单(一个 MD5);
- 数据透明(所有原料都来自你自己的 Cookie 和请求 body)。
它"反"的从来不是技术破解者,反的是用裸 requests.get 抄页面的低水平爬虫。任何一个愿意花 20 分钟看 Network 的人,都能复现它。
当 mtop 改动时,怎么应对
mtop 不是一成不变的。你大概率会遇到这些变化:
- appKey 变更:淘宝每几年会换一次 appKey,每次换都要重新抓前端 JS。
- token 提取规则:以前是 _m_h5_tk 下划线前面那段,后来出现过"倒数第二段"的情况。
- 新增参数:比如某次升级要求 sign 里再加 ua 或 v。
- token 加密:出现过 _m_h5_tk_enc(AES 加密版),需要前端 JS 解密才能拿原始 token。
应对策略:
- 不要硬编码常量:appKey、JS 版本号都做成配置项;
- 关注 mtop 官方升级公告:每次大促前后是高危期;
- 保留一份能跑的"基线版本":每次升级前后比对,定位变化点。
写在最后
mtop 签名解析是爬虫世界里非常典型的一类问题——看起来神秘,本质朴素。把这套流程搞懂,你会得到一个意外收获:所有走淘系 mtop 的 H5 页面,你都能爬。淘宝商品详情、天猫店铺、支付宝账单、闲鱼商品,骨架都一样,字段名改一改、appKey 换一换就行。
下一步建议做三件事:
- 把 sign 算法抽成 lib/sign.py,配合 cookie_pool.json 做多账号轮询;
- 用 retrying 或 tenacity 加自动重试,配合 random.uniform(2, 5) 随机等待;
- 把抓到的数据落到 SQLite,跑一个 pyecharts 看板,分析"哪些关键词的成交量涨得最快"。
爬虫这件事,重要的不是你今天下到多少数据,而是你能不能把它变成持续可用的信号。md5 只是起点,工程化才是终点。
免责声明(请务必读完)
- 闲鱼 / 淘宝 / 天猫的 mtop 接口属于阿里内部资源,调用它意味着你在和阿里的风控系统直接对话。任何形式的未授权批量抓取、商业转售、绕过人机验证都违反用户协议,也可能触发账号封禁甚至法律责任。
- Cookie 是个人登录态凭据,严禁出现在博客、GitHub、群聊截图、技术问答里。本文代码块中的 <YOUR_XIANYU_COOKIE_HERE> 不是装饰,是底线。
- 个人学习、小量下载、不传播、不商用、不影响平台正常运行——这是我能给的最宽边界。超过这条线,请走平台的开放接口或签合作协议。
- mtop 签名算法随时可能升级,本文示例仅供学习"思路",实际使用请以闲鱼当前接口为准,自负其责。
- 出了问题,与本文无关。脚本运行造成的账号封禁、设备损坏、数据丢失、法律纠纷,均由使用者自行承担。
技术是工具,不是免责符。把它用在合法、正当、有边界的场景里——这是我能给你的最后一点职业建议。




