闲鱼商品搜索爬虫:从签名算法到反爬机制的完整逆向与实现

在数据采集领域,**签名算法(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------ 你就已经跨越了"脚本执行者"与"系统工程师"之间的界线。


总结一句话:

爬虫不是为了"抓到数据",而是为了"理解系统如何守护数据"。

--- 以这篇内容 为基础写一段 文章摘要

相关推荐
跟着珅聪学java3 小时前
vue通过spring boot 下载文件教程
前端·spring boot·后端
云闲不收3 小时前
golang编译
开发语言·后端·golang
数据知道3 小时前
Go语言:加密与解密详解
开发语言·后端·golang·go语言
武子康3 小时前
大数据-117 - Flink JDBC Sink 详细解析:MySQL 实时写入、批处理优化与最佳实践 写出Kafka
大数据·后端·flink
用户21411832636024 小时前
Qwen3-VL 接口部署全攻略:从源码到 Docker,手把手教你玩转多模态调用
后端
databook4 小时前
Manim实现旋转扭曲特效
后端·python·动效
karry_k4 小时前
ThreadLocal原理以及内存泄漏
java·后端·面试
MrSun的博客4 小时前
数据源切换之道
后端
Keepreal4964 小时前
1小时快速上手SpringBoot,熟练掌握CRUD
spring boot·后端