环境搭建
本次实践基于 Python 3.8+,需安装以下第三方库,执行命令:
pip install requests execjs fake-useragent pyquery
execjs:用于在 Python 中执行逆向后的 JS 代码,需提前安装 Node.js(保证 JS 运行环境);fake-useragent:生成随机 User-Agent,规避请求头特征检测;
使用 Python 进行 Web 爬虫时的 JS 逆向工程与多线程优化实践。通过抓包分析淘宝接口加密逻辑,利用 Node.js 环境还原加密函数,并结合 execjs 库在 Python 中调用。文章详细展示了单线程与多线程(ThreadPoolExecutor)的实现对比,分析了反爬策略如 Cookie 持久化、User-Agent 随机化及请求间隔控制。测试表明多线程方案在 I/O 密集型任务中效率提升显著,适用于大规模数据采集场景。
本次实践基于 Python 3.8+,需安装以下第三方库,执行命令:
pip install requests execjs fake-useragent pyquery
execjs:用于在 Python 中执行逆向后的 JS 代码,需提前安装 Node.js(保证 JS 运行环境);fake-useragent:生成随机 User-Agent,规避请求头特征检测;pyquery:轻量的 HTML 解析库,便捷提取页面数据;requests:发送 HTTP/HTTPS 请求,核心网络请求库。同时准备抓包工具(Charles 或 Fiddler)、浏览器 F12),用于抓包分析请求参数与 JS 加密逻辑。
某宝的商品列表、详情等接口均为异步 AJAX 请求,且请求参数中包含多个加密字段(如 _m_h5_tk、_m_h5_tk_enc、sign),直接构造请求会返回 403/500 错误,因此第一步需通过抓包分析加密逻辑,再完成 JS 逆向。
以某宝商品搜索接口为例,操作步骤如下:
https://h5api.m.taobao.com/h5/mtop.taobao.search.core/1.0/);_m_h5_tk、_m_h5_tk_enc:与用户登录态、时间戳相关的加密串;sign:对请求参数、时间戳、固定密钥的混合加密结果;t:时间戳,appKey:固定应用标识。加密参数的生成逻辑藏在某宝的前端 JS 代码中,通过开发者工具定位核心 JS 文件:
_m_h5_tk、sign),定位到参数生成的核心函数;sign 的生成规则为:sign = md5(appKey + t + token + data),其中 token 为 _m_h5_tk 分割后的字段,data 为请求体的 JSON 字符串。由于某宝的前端 JS 会做混淆压缩(变量名简写、代码嵌套),需对核心加密函数进行提取和还原,步骤如下:
通过 execjs 库让 Python 执行逆向后的 JS 代码,实现加密参数的动态生成,这是连接 JS 逆向与 Python 爬取的关键环节。
本部分先实现 JS 逆向的 Python 封装,生成合法的加密请求参数,再完成单线程的基础爬取,为后续多线程改造打下基础。
新建 taobao_encrypt.js 文件,存放还原后的加密代码,核心实现 sign、_m_h5_tk(简化版,实际需结合 Cookie 维护)的生成,代码如下:
// 引入 MD5 加密模块(Node.js 环境,需提前安装:npm install md5)
const md5 = require('md5');
/**
* 生成 sign 加密参数
* @param {string} appKey - 固定 appKey
* @param {string} t - 时间戳
* @param {string} token - _m_h5_tk 分割后的 token
* @param {string} data - 请求体 JSON 字符串
* @returns {string} 加密后的 sign
*/
function generateSign(appKey, t, token, data) {
const str = appKey + t + token + data;
return md5(str);
}
/**
* 生成_m_h5_tk(简化版,实际需从 Cookie 中提取并更新)
* @param {string} token - 基础 token
* @param {string} t - 时间戳
* @returns {string} 拼接后的_m_h5_tk
*/
function generateMtk(token, t) {
return token + '_' + t + '_' + Math.floor(Math.random() * 1000);
}
// 暴露方法,供 Python 调用
module.exports = { generateSign, generateMtk };
注:实际某宝的 _m_h5_tk 会随请求更新,需从响应头的 Cookie 中提取并维护,本文为简化实践,做基础实现,生产环境需完善 Cookie 持久化。
新建 taobao_crawler.py,实现 JS 代码调用、加密参数生成、基础请求封装,代码如下:
import execjs
import requests
import time
import json
import hashlib
from fake_useragent import UserAgent
from pyquery import PyQuery as pq
# 初始化 UserAgent,生成随机请求头
ua = UserAgent(verify_ssl=False)
# 加载 JS 加密文件
with open('taobao_encrypt.js', 'r', encoding='utf-8') as f:
js_code = f.read()
ctx = execjs.compile(js_code, cwd=r'C:\Program Files\nodejs')
# cwd 为 Node.js 安装路径,execjs 需找到 node 可执行文件
# 某宝固定配置
APP_KEY = '12574478' # 某宝公开 appKey,实际可从抓包获取
BASE_TOKEN = 'your_token' # 从 Cookie 中提取的基础 token,抓包获取
BASE_URL = 'https://h5api.m.taobao.com/h5/mtop.taobao.search.core/1.0/'
class TaobaoEncrypt:
"""加密工具类,生成某宝请求所需加密参数"""
@staticmethod
def get_timestamp():
"""生成 13 位时间戳(某宝接口要求)"""
return str(int(time.time() * 1000))
@staticmethod
def generate_params(data):
"""
生成所有加密参数
:param data: 请求体原始数据(字典)
:return: 加密后的参数字典
"""
t = TaobaoEncrypt.get_timestamp()
# 生成_m_h5_tk
m_tk = ctx.call('generateMtk', BASE_TOKEN, t)
# 分割_m_h5_tk 获取 token(规则:_m_h5_tk = token + _ + t + _ + 随机数)
token = m_tk.split('_')[0]
# 转换 data 为 JSON 字符串(无空格,某宝要求)
data_str = json.dumps(data, separators=(',', ':'))
# 生成 sign
sign = ctx.call('generateSign', APP_KEY, t, token, data_str)
return {
't': t,
'_m_h5_tk': m_tk,
'_m_h5_tk_enc': hashlib.md5(m_tk.encode()).hexdigest().upper(), # 简单实现,实际需按某宝规则加密
'sign': sign,
'appKey': APP_KEY,
'data': data_str
}
# 基础请求方法
def single_crawl(self, keyword, page=1):
"""
单线程爬取某宝商品搜索结果
:param keyword: 搜索关键词
:param page: 页码
:return: 商品列表数据
"""
# 构造原始请求体数据
data = {
'q': keyword,
'pageNo': page,
'pageSize': 20,
'platform': 'h5'
}
# 生成加密参数
encrypt_params = self.generate_params(data)
# 构造请求头
headers = {
'User-Agent': ua.random,
'Referer': 'https://s.m.taobao.com/',
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': f'_m_h5_tk={encrypt_params["_m_h5_tk"]};', # 携带加密 Cookie
'Host': 'h5api.m.taobao.com'
}
# 构造请求体
payload = {
'jsv': '2.6.1',
'appKey': encrypt_params['appKey'],
't': encrypt_params['t'],
'sign': encrypt_params['sign'],
'data': encrypt_params['data']
}
try:
# 发送 POST 请求(某宝核心接口均为 POST)
response = requests.post(BASE_URL, headers=headers, data=payload, timeout=10)
if response.status_code == 200:
result = response.json()
if result.get('ret') == ['SUCCESS::接口调用成功']:
# 解析商品数据
goods_list = result.get('data', {}).get('items', [])
print(f'第{page}页爬取成功,共{len(goods_list)}件商品')
return goods_list
else:
print(f'第{page}页爬取失败,返回信息:{result.get("ret")}')
return []
else:
print(f'请求失败,状态码:{response.status_code}')
return []
except Exception as e:
print(f'请求异常:{str(e)}')
return []
# 单线程测试
if __name__ == '__main__':
start_time = time.time()
# 爬取关键词「Python 教程」前 3 页
for page in range(1, 4):
crawler = TaobaoEncrypt()
crawler.single_crawl('Python 教程', page)
end_time = time.time()
print(f'单线程爬取完成,总耗时:{end_time - start_time:.2f}秒')
execjs.compile 加载 JS 文件,通过 ctx.call 调用 JS 中的方法,需指定 Node.js 路径(cwd 参数),避免 execjs 找不到运行环境;sign、_m_h5_tk 等核心参数,确保请求合法性;fake-useragent 生成随机 User-Agent,携带加密后的 Cookie,设置请求超时,避免请求阻塞;ret 字段为成功标识后,提取商品核心数据,简化了异常处理逻辑。Python 中的爬取属于网络 I/O 密集型任务,单线程爬取时,程序会在等待网络响应的过程中阻塞,造成资源浪费。多线程技术可让多个请求同时发起,充分利用网络带宽,大幅提升爬取效率。
本次实践采用 Python 内置的 concurrent.futures.ThreadPoolExecutor 实现多线程,该库封装了线程池的创建、任务提交、结果获取,使用简洁且线程管理更安全。
在上述 taobao_crawler.py 中新增多线程爬取方法,核心代码如下:
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
# 全局控制:线程数(根据反爬调整,建议 5-10)
THREAD_NUM = 8
# 全局控制:每页请求间隔(秒,避免请求过快被封)
REQUEST_INTERVAL = 0.5
def multi_thread_crawl(keyword, max_page):
"""
多线程爬取某宝商品搜索结果
:param keyword: 搜索关键词
:param max_page: 最大爬取页码
:return: 所有商品数据列表
"""
all_goods = []
# 创建线程池
with ThreadPoolExecutor(max_workers=THREAD_NUM) as executor:
# 提交任务:将每一页的爬取任务提交给线程池
future_to_page = {executor.submit(crawler_instance.single_crawl, keyword, page): page for page in range(1, max_page + 1)}
# 遍历完成的任务,获取结果
for future in as_completed(future_to_page):
page = future_to_page[future]
try:
# 获取单页爬取结果
goods = future.result()
if goods:
all_goods.extend(goods)
# 间隔请求,规避反爬
time.sleep(REQUEST_INTERVAL)
except Exception as e:
print(f'第{page}页多线程爬取异常:{str(e)}')
return all_goods
# 多线程测试
if __name__ == '__main__':
# 单线程测试(注释掉单线程代码,开启多线程)
# start_time = time.time()
# for page in range(1, 4):
# crawler = TaobaoEncrypt()
# crawler.single_crawl('Python 教程', page)
# end_time = time.time()
# print(f'单线程爬取完成,总耗时:{end_time - start_time:.2f}秒')
# 多线程测试:爬取「Python 教程」前 10 页
start_time = time.time()
crawler = TaobaoEncrypt()
total_goods = multi_thread_crawl('Python 教程', 10)
end_time = time.time()
print(f'多线程爬取完成,总耗时:{end_time - start_time:.2f}秒')
print(f'累计爬取商品:{len(total_goods)}件')
THREAD_NUM = 8,线程数并非越多越好,某宝对单 IP 的请求频率有限制,过多线程会导致请求被封,建议根据实际测试调整(5-10 为宜);as_completed 中添加 time.sleep(REQUEST_INTERVAL),对每个完成的任务做间隔,避免单 IP 短时间内发起大量请求;with ThreadPoolExecutor 自动管理线程池,无需手动关闭线程,避免资源泄漏;try-except 捕获单个线程的异常,确保一个页面爬取失败不会影响其他线程的执行。以爬取「Python 教程」前 10 页为例,测试环境为普通家用网络(百兆宽带)、Windows 10、Python 3.9,结果如下:
即使实现了 JS 逆向与多线程,若忽略反爬规避细节,仍可能出现 IP 被封、请求失败等问题。结合某宝的反爬机制,以下是几个关键的优化策略,可大幅提升爬取稳定性:
实际某宝的 _m_h5_tk 并非固定值,会在每次请求后从响应头的 Set-Cookie 中更新,因此需实现 Cookie 的持久化存储与动态更新:
requests.Session() 保持会话,自动维护 Cookie;response.cookies 中提取新的 _m_h5_tk,更新至加密工具类,确保后续请求参数的合法性。
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online