前言 闲鱼(Goofish)作为阿里旗下的二手交易平台,汇聚了大量个人卖家和商品数据。对于二手商品比价、市场行情分析来说,闲鱼的数据极具价值。然而,闲鱼的搜索接口使用 MD5 签名验证,且签名参数与时间戳强绑定,给自动化采集带来了不小的挑战。
另一方面,闲鱼商品链接会定期失效(商品下架或重新发布),需要定期刷新链接保持活跃度。面对大量链接的手动刷新需求,多线程浏览器自动化成了高效的解决方案。
本文基于闲鱼爬虫项目的真实源码,分两大模块讲解:商品搜索爬虫(含 MD5 签名逆向)和多线程链接刷新(含移动端模拟)。
技术背景 闲鱼搜索接口分析 闲鱼商品搜索通过以下 API 实现:
1 POST https://h5api.m.goofish.com/h5/mtop.taobao.idlemtopsearch.pc.search/1.0/
该接口需要 sign 签名参数,签名算法为 MD5 ,计算公式为:
1 sign = MD5(token + "&" + timestamp + "&" + appKey + "&" + data)
涉及的库
库
用途
requests
发送 HTTP 请求
hashlib
MD5 签名计算
csv
数据持久化
time
生成时间戳、请求间隔
threading
多线程浏览器自动化
DrissionPage
浏览器自动化控制
实现思路 项目分为两大模块:
1 2 3 4 5 6 7 8 9 10 模块一:商品搜索爬虫 ├── 单个搜索(商品搜索单个爬虫.py) ├── 批量搜索(商品搜索爬虫批量.py)—— 含MD5签名 └── 商品详情(商品详情爬虫.py) 模块二:多线程链接刷新 ├── 单线程版(闲鱼刷新1.py) ├── 多线程版(多线程闲鱼链接刷新.py) ├── 不保存版(多链接多线程不保存闲鱼链接刷新.py) └── 移动端模拟版(模拟移动端.py)
核心代码解析 一、商品搜索爬虫 1. 单页搜索:基础版 单个搜索版本直接使用固定的签名参数,适合单次请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import csvimport requestsf = open ('单个商品搜索数据.csv' , mode='w' , newline='' , encoding='utf-8' ) csv_writer = csv.DictWriter(f, fieldnames=[ '价格' , '标签' , '产品简介' , '标题' , '昵称' , '区域' , '标题详情' , ]) csv_writer.writeheader() 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; ..." } url = "https://h5api.m.goofish.com/h5/mtop.taobao.idlemtopsearch.pc.search/1.0/" params = { "jsv" : "2.7.2" , "appKey" : "34839810" , "t" : "1759795344200" , "sign" : "8e4f63f339cc4eaea8dc3a9a778a94a5" , "v" : "1.0" , "type" : "originaljson" , "accountSite" : "xianyu" , "dataType" : "json" , "api" : "mtop.taobao.idlemtopsearch.pc.search" , } data = { 'data' : '{"pageNumber":1,"keyword":"python爬虫","fromFilter":false,"rowsPerPage":30,...}' } response = requests.post(url, headers=headers, params=params, data=data) json_data = response.json()
2. 批量搜索:MD5 签名动态生成 批量搜索的关键突破在于动态生成签名 。源码中实现了完整的签名算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import hashlibimport timedef get_sign (page_number ): """ 生成请求签名和相关参数 签名算法:MD5(token + "&" + timestamp + "&" + appKey + "&" + data) """ token = '9a7fd05c49b25d5075fab51a234be307' current_timestamp = int (time.time() * 1000 ) j = str (current_timestamp) 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 sign = hashlib.md5(string.encode('utf-8' )).hexdigest() return j, c_data, sign
签名逆向过程 :源码注释中记录了一个真实的调试经历——开发者发现抓包时间戳与当前系统时间相差16小时(抓包时与运行时的时差),导致签名失效。将时间戳调整为当前时间后,请求成功。这说明 sign 与时间戳强绑定,必须使用实时时间戳。
循环爬取多页:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 for pageNumber in range (1 , 8 ): print (f'正在采集第{pageNumber} 页的数据内容' ) 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" , "api" : "mtop.taobao.idlemtopsearch.pc.search" , } data = {'data' : c_data} response = requests.post(url, headers=headers, params=params, data=data) json_data = response.json() resultList = json_data['data' ]['resultList' ]
3. 商品数据提取 从返回的嵌套 JSON 中提取商品信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 for item in resultList: try : main = item['data' ]['item' ]['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['clickParam' ]['args' ]['price' ], '标签' : main['clickParam' ]['args' ]['tagname' ], '产品简介' : tagname, '标题' : main['exContent' ]['detailParams' ]['title' ], '昵称' : main['exContent' ]['userNickName' ], '区域' : main['exContent' ]['area' ], '标题详情' : main['exContent' ]['title' ], } csv_writer.writerow(dit) except KeyError as e: print (f"商品缺少字段{e} ,跳过" ) except Exception as e: print (f"未预期的错误: {type (e).__name__} : {e} " )
异常处理 :闲鱼返回的数据结构不完全统一,有些商品可能缺少 fishTags 等字段。使用 try-except 捕获 KeyError 可以跳过异常数据,保证程序不中断。
4. 商品详情爬虫 获取单个商品详情通过另一个 API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 url = "https://h5api.m.goofish.com/h5/mtop.taobao.idle.pc.detail/1.0/" data_form = { 'data' : '{"itemId": "963567908521"}' } params = { "jsv" : "2.7.2" , "appKey" : "34839810" , "t" : "1759793614723" , "sign" : "2f84c8f1628ea3253f8343def6720c7c" , "api" : "mtop.taobao.idle.pc.detail" , "accountSite" : "xianyu" , "dataType" : "json" , } response = requests.post(url, headers=headers, params=params, data=data_form) resjson = response.json()
二、多线程链接刷新 1. 单线程版 单线程版本使用 DrissionPage 控制浏览器,逐个访问链接并定时刷新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import timeimport randomfrom DrissionPage import ChromiumPagepage = ChromiumPage() urls = [ "https://www.goofish.com/item?id=978882160967&spm=widle.12011849.0.0&ut_sk=" , "https://www.goofish.com/item?id=976990295214&spm=widle.12011849.0.0&ut_sk=" , "https://www.goofish.com/item?id=976991963358&spm=widle.12011849.0.0&ut_sk=" ] max_refreshes_per_url = 100 min_interval = 3 max_interval = 10 for url_index, url in enumerate (urls): page.get(url) time.sleep(5 ) refresh_count = 0 while refresh_count < max_refreshes_per_url: refresh_count += 1 page.refresh() wait_time = random.randint(min_interval, max_interval) time.sleep(wait_time) print (f"URL {url_index + 1 } - 第 {refresh_count} 次刷新完成,等待 {wait_time} 秒" ) page.quit()
2. 多线程版 多线程版本为每个 URL 创建独立的浏览器实例,并发刷新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import threadingbrowser_instances = {} browser_lock = threading.Lock() def refresh_url (url, url_index ): """为单个URL创建浏览器实例并刷新""" page = ChromiumPage() with browser_lock: browser_instances[url_index] = page try : page.get(url) time.sleep(5 ) refresh_count = 0 while refresh_count < max_refreshes_per_url: refresh_count += 1 page.refresh() wait_time = random.randint(min_interval, max_interval) time.sleep(wait_time) print (f"[线程 {url_index + 1 } ] 第 {refresh_count} 次刷新完成" ) return True except Exception as e: print (f"[线程 {url_index + 1 } ] 发生错误: {e} " ) return False if __name__ == "__main__" : threads = [] for i, url in enumerate (urls): thread = threading.Thread(target=refresh_url, args=(url, i)) threads.append(thread) thread.start() time.sleep(2 ) for thread in threads: thread.join() with browser_lock: for index, browser in browser_instances.items(): browser.quit()
关键设计:
每个线程独立浏览器实例 :避免多线程操作同一浏览器导致冲突
browser_lock :保护 browser_instances 字典的线程安全
time.sleep(2) :线程启动间隔2秒,避免同时启动多个浏览器
3. 移动端模拟版 移动端版本通过修改 User-Agent 和窗口大小,模拟手机访问:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from DrissionPage import ChromiumPage, ChromiumOptionsdef create_mobile_chromium_options (): """创建移动端浏览器配置""" co = ChromiumOptions() co.set_user_agent('Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1' ) co.set_argument('--window-size=390,844' ) co.set_argument('--device-scale-factor=3' ) co.set_argument('--use-mobile-user-agent' ) return co co = create_mobile_chromium_options() page = ChromiumPage(co)
移动端模拟的意义在于:某些闲鱼链接(如 m.tb.cn 短链)只在移动端可访问,PC端访问会跳转或失效。
运行效果 商品搜索爬虫输出:
1 2 3 4 5 6 正在采集第1页的数据内容 当前时间戳: 1759801941772 {'价格': '299', '标签': '包邮', '产品简介': 'Python爬虫开发实战', '标题': 'Python爬虫书籍', '昵称': '二手书店', '区域': '广东深圳', '标题详情': '...'} {'价格': '1500', '标签': '九成新', '产品简介': 'Python入门到精通', '标题': 'Python编程教材', '昵称': '技术宅', '区域': '北京海淀', '标题详情': '...'} ... 正在采集第2页的数据内容
多线程链接刷新输出:
1 2 3 4 5 6 7 8 9 10 11 [线程 1] 开始处理URL: https://www.goofish.com/item?id=978882160967... [线程 1] 页面标题: 闲鱼商品 [线程 2] 开始处理URL: https://www.goofish.com/item?id=976990295214... [线程 1] 第 1 次刷新完成,等待 9 秒 [线程 2] 页面标题: 闲鱼商品 [线程 1] 第 2 次刷新完成,等待 6 秒 ... 所有URL处理完成,正在关闭浏览器... [浏览器 1] 已关闭 [浏览器 2] 已关闭 所有浏览器已关闭
总结与优化方向 本文基于闲鱼爬虫项目的真实源码,完整讲解了两大模块:商品搜索(MD5 签名逆向 + 数据提取)和多线程链接刷新(浏览器自动化 + 移动端模拟)。
核心知识点回顾:
MD5 签名逆向 :sign = MD5(token + "&" + timestamp + "&" + appKey + "&" + data),关键在于 token 的提取和时间戳的实时性
多线程浏览器自动化 :每个线程创建独立浏览器实例,通过 threading.Lock 管理共享资源
移动端模拟 :通过修改 UA 和窗口大小模拟手机访问
优化方向:
Token 自动提取 :当前 token 硬编码在代码中,可从 Cookie 的 _m_h5_tk 字段自动提取
代理 IP 池 :多线程刷新时容易被识别,搭配代理 IP 降低风险
刷新策略优化 :根据商品活跃度动态调整刷新频率,活跃商品多刷,冷门商品少刷
异常恢复 :浏览器崩溃后自动重启,保证长时间运行的稳定性
数据持久化 :搜索结果存入数据库,支持历史价格对比和趋势分析
闲鱼爬虫的核心难点在于 MD5 签名的逆向和时间戳的处理。理解了签名算法后,就能灵活应对阿里系平台(淘宝、天猫、闲鱼)的类似签名机制。