在数据采集领域,**签名算法(sign)与 时间戳校验(timestamp)**是最常见的反爬门槛。 本文以闲鱼(Goofish)为例,完整讲解一个真实可用的 签名级别爬虫实现,从参数逆向到稳定运行,并逐步升级为可扩展的工程化架构。
一、前言:从"抓不到数据"开始
最初,我的目标很简单:
想抓取闲鱼搜索结果,批量获取 "Python 爬虫" 相关的商品标题、价格与卖家信息。
我照着视频教程写了个爬虫,但结果一行数据都没有。 浏览器能搜到数据,代码却返回空。
排查后发现问题出在时间戳验证。闲鱼的搜索接口使用签名机制,每次请求必须带上:
- token :来源于浏览器 Cookie 中的
_m_h5_tk
- t:时间戳
- sign:由 token、时间戳、appKey、请求参数计算出的 MD5 值
签名算法看似简单,但其中的 时间戳偏差问题 是关键。
二、时间戳反推:16 小时的谜团
抓包时我发现,浏览器发出的请求中:
ini
t = 1759801941772
对应的实际时间是 2025-10-06 17:52:21 , 而我运行代码时系统时间是 2025-10-07 09:53。
时间差整整 16 小时。
当我强行把 time.time()
生成的时间戳往前拨 16 小时后,数据立即返回。 这说明签名算法与真实系统时区不同,闲鱼接口使用的时间戳不是本地时间,而更接近 UTC 时区。
于是我将时间戳修正:
python
current_timestamp = int(time.time() * 1000)
sixteen_hours_ago = current_timestamp - (16 * 60 * 60 * 1000)
j = str(sixteen_hours_ago)
成功拿到数据。
三、签名算法复现
浏览器请求中的签名计算方式如下:
ini
sign = MD5(token + "&" + timestamp + "&" + appKey + "&" + data)
其中:
token
:Cookie 中_m_h5_tk
的前半段timestamp
:时间戳(毫秒)appKey
:固定为34839810
data
:请求体 JSON 字符串
Python 实现非常直接:
python
string = token + "&" + j + "&" + h + "&" + c_data
sign = hashlib.md5(string.encode('utf-8')).hexdigest()
通过这一行,你完成了与前端 JS 一致的签名生成逻辑。
四、完整爬虫实现
整合签名生成、请求与数据落地,形成完整采集脚本:
python
import csv, time, hashlib, requests
def get_sign(page_number):
token = '9a7fd05c49b25d5075fab51a234be307'
current_timestamp = int(time.time() * 1000)
sixteen_hours_ago = current_timestamp - (16 * 60 * 60 * 1000)
j = str(sixteen_hours_ago)
h = '34839810'
c_data = '{"pageNumber":%d,"keyword":"python爬虫","rowsPerPage":30}' % page_number
string = token + "&" + j + "&" + h + "&" + c_data
sign = hashlib.md5(string.encode('utf-8')).hexdigest()
return j, c_data, sign
headers = {
"User-Agent": "Mozilla/5.0 ...",
"Referer": "https://www.goofish.com/",
"Cookie": "t=xxx; _m_h5_tk=9a7fd05c49b25d5075fab51a234be307_1759808103035; ..."
}
f = open('商品搜索爬虫批量.csv', 'w', newline='', encoding='utf-8')
writer = csv.DictWriter(f, fieldnames=['标题','区域','昵称','标签','产品简介','价格','标题详情'])
writer.writeheader()
for page in range(1, 8):
print(f'正在采集第{page}页...')
j, c_data, sign = get_sign(page)
params = {"jsv": "2.7.2", "appKey": "34839810", "t": j, "sign": sign, "v": "1.0"}
data = {'data': c_data}
res = requests.post("https://h5api.m.goofish.com/h5/mtop.taobao.idlemtopsearch.pc.search/1.0/",
headers=headers, params=params, data=data)
json_data = res.json()
for item in json_data['data']['resultList']:
main = item['data']['item']['main']
dit = {
'标题': main['exContent']['detailParams']['title'],
'区域': main['exContent']['area'],
'昵称': main['exContent']['userNickName'],
'标签': main['clickParam']['args']['tagname'],
'产品简介': main['exContent']['title'],
'价格': main['clickParam']['args']['price'],
'标题详情': main['exContent']['title']
}
print(dit)
writer.writerow(dit)
运行结果: 脚本能成功批量获取闲鱼搜索结果,自动写入 商品搜索爬虫批量.csv
。
五、反爬机制与规避策略
闲鱼接口有严格的防御逻辑,以下几点需要重点注意:
风控点 | 描述 | 应对方案 |
---|---|---|
Cookie 绑定 | _m_h5_tk 与 token 关联 |
定期更新 Cookie,自动解析 token |
时间窗口 | 验签允许 ±1 小时偏差 | 建议提取 _m_h5_tk 内时间戳 |
请求频率 | 高频请求封禁 IP | 加入随机延时:time.sleep(random.uniform(1.2,3.6)) |
Referer 校验 | 必须来源 goofish.com |
固定 Referer 头保持一致性 |
六、完整的代码
python
import csv
import time
from pprint import pprint
import requests
import hashlib
def get_sign(page_number):
"""
生成请求签名和相关参数
该函数根据页面编号生成用于闲鱼搜索接口的签名和时间戳参数。
通过特定算法生成签名,确保请求能够通过接口验证。
Args:
page_number (int): 需要请求的搜索结果页码
Returns:
tuple: 包含三个元素的元组
- str: 时间戳字符串
- str: 请求数据的JSON字符串
- str: 生成的MD5签名
"""
token = '9a7fd05c49b25d5075fab51a234be307'
'''
# 原本我也是按照视频里的代码运行但是失败了没有数据过来,于是我就不断比对视频里的代码 和
# 我实际上的 数据 以及浏览器请求的参数 最终发现 浏览器请求出去的时间戳和我当前的事件和日期 做出来的时间戳 完全不一致 问过AI 发现两个时间戳 的 差值高达16个小时
抓包时间与当前时间的差异:
1759801941772 对应的时间是 2025-10-06 17:52:21.772
当前系统时间是 2025-10-07 09:53(左右)
这说明你是在大约 16 小时前进行的抓包操作
于是 我就将当前时间戳向后倒了 16个小时果然 数据直接过来了
'''
current_timestamp = int(time.time() * 1000)
print(f"当前时间戳: {current_timestamp}")
sixteen_hours_ago = current_timestamp - (16 * 60 * 60 * 1000)
# j = str(int(time.time() * 1000))
j = str(sixteen_hours_ago)
h = '34839810'
c_data = '{"pageNumber":%d,"keyword":"python爬虫","fromFilter":false,"rowsPerPage":30,"sortValue":"","sortField":"","customDistance":"","gps":"","propValueStr":{},"customGps":"","searchReqFromPage":"pcSearch","extraFilterValue":"{}","userPositionJson":"{}"}' % page_number
string = token + "&" + j + "&" + h + "&" + c_data
# print(string)
sign = hashlib.md5(string.encode('utf-8')).hexdigest()
return j, c_data, sign
# 打开CSV文件准备写入数据
f = open('商品搜索爬虫批量.csv', mode='w', newline='', encoding='utf-8')
writer = csv.writer(f)
# 创建DictWriter对象,指定列名
csv_writer = csv.DictWriter(f, fieldnames=[
'标题',
'区域',
'昵称',
'标签',
'产品简介',
'价格',
'标题详情',
])
# 写入表头
csv_writer.writeheader()
# 设置请求头信息,包括User-Agent、Referer和Cookie等
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"Referer": "https://www.goofish.com/",
"Cookie": "t=333d89bdd8d3c7f1680d61c314977359; tracknick=tb575736359; cna=5dFRIU3sMHMBASQJijx6xuFX; isg=BA4O1Z6E6rPx1V7kM_eYllnfX-TQj9KJBFr77zhXA5HMm671oBaHmbdQ18f3hsqh; cookie2=1bd479bb51a7304737dd39912bcce0eb; _samesite_flag_=true; sgcookie=E100lSj5ts%2Bd9eb0HAVpdFmW%2BsVh9FYgXvUZuwcDxh0BtMKbwlSXJGs5OlNCOdjXKrrUB4oRAn7SpNlLypY%2BSmUclm%2Fn0PN6lsfI%2FDaqY7Dmno6l%2F5%2BZX8Jlqq1URukGipG2; csg=2de4d3fd; _tb_token_=3f65e75e137d5; unb=2209968140617; sdkSilent=1759879902293; xlly_s=1; mtop_partitioned_detect=1; _m_h5_tk=9a7fd05c49b25d5075fab51a234be307_1759808103035; _m_h5_tk_enc=c4118ef3c44e5a8a979739e9dba2c653; tfstk=gBw-nFcf0ZbkVK8y2yfcKGNY-0IcssqzZzr6K20kOrUYfl8oO8DuJBUuYycCzY2LkkaO4DbzK3P4Sl9uE_kHpYkEdNbGIOmz4vkB0iYP2UJjbD6HVLgIUxO_ruBOIOqzV3m5jo6gKL3BIDuIdbGIcKgqvpOQRXgjlqoEAUiBFiEjuqiBRpOSlniixeT7RvsYcqoEVvG7RxsxYqgIdvGkzg32V0pLNHUa4cQlNCRaH0h-Jp0vpVwDIb01eq9ppqzIwQqSkp9QHWdeq235_n0EE4rq2yWwUYGLO8DbFZ6IC5zYpfMOtgkTDSViwSQWpq2ouf2SBH67DY3-1-h6vdz_DkNiM-j1QYHSPWDzxhQuD8Uu4-EHAIM-EShTeA6wP2V0XJnLLwWYWkZaOjepyglzIRHPLF0txQsADBRENmRxupw3Fgv2Bm3GVbOeTXmqDVjADBRENmoxSg9WTBln0"
}
# 循环爬取多页搜索结果数据
for pageNumber in range(1, 8):
print(f'正在采集第{pageNumber}页的数据内容')
url = "https://h5api.m.goofish.com/h5/mtop.taobao.idlemtopsearch.pc.search/1.0/"
j, c_data, sign = get_sign(pageNumber)
# 构造请求参数
params = {
"jsv": "2.7.2",
"appKey": "34839810",
"t": j,
"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"
}
# 构造请求数据
data = {
'data': c_data
}
# 发送POST请求获取搜索结果
response = requests.post(url, headers=headers, params=params, data=data)
json_data = response.json()
# pprint(json_data)
# exit()
resultList = json_data['data']['resultList']
# pprint(resultList)
# 遍历搜索结果,提取并保存商品信息
for item in resultList:
try:
main = item['data']['item']['main']
# pprint(main)
fishTags_keys = main['exContent']['fishTags'].keys()
if 'r2' in fishTags_keys:
tagname = ''.join(
[i['utParams']['args']['content'] for i in main['exContent']['fishTags']['r2']['tagList']])
else:
tagname = '无'
dit = {
'标题': main['exContent']['detailParams']['title'],
'区域': main['exContent']['area'],
'昵称': main['exContent']['userNickName'],
'标签': main['clickParam']['args']['tagname'],
'产品简介': tagname,
'价格': main['clickParam']['args']['price'],
'标题详情': main['exContent']['title'],
}
print(dit)
csv_writer.writerow(dit)
# break
except Exception as e:
# 捕获其他未预期的异常并打印详细信息,但不中断程序执行
print(f"未预期的错误: {type(e).__name__}: {e}")
pass
七、结语:技术的本质是洞察
这次实战最大的收获不是拿到数据,而是理解了平台反爬体系的逻辑。 反爬的本质,不是"反你",而是防止滥用。 而爬虫的价值,不在于突破限制,而在于理解机制、提取规律、生成洞察。
当你能看懂签名算法、识别时区错位、动态适配 Cookie------ 你就已经跨越了"脚本执行者"与"系统工程师"之间的界线。
总结一句话:
爬虫不是为了"抓到数据",而是为了"理解系统如何守护数据"。
--- 以这篇内容 为基础写一段 文章摘要