1. 项目概述为什么我们要研究Shopee的签名机制做电商数据采集的朋友对Shopee这个东南亚巨头平台一定不陌生。无论是选品分析、价格监控还是竞品研究都需要稳定、高效地获取其站点上的商品、店铺和搜索数据。然而和许多现代大型电商平台一样Shopee为了保护其服务器资源和数据安全部署了相当复杂的反爬虫机制其中最关键的一环就是其请求签名机制。直接使用简单的requests库发送请求十有八九会吃到“403 Forbidden”或者返回一堆乱码。这个项目就是一次彻底的“外科手术式”逆向工程目标不是简单地绕过一两个参数而是深入理解Shopee以网页端为例如何为每个请求生成唯一的、有时效性的“身份证”——签名并基于此构建一套可以长期稳定运行的自动化采集方案。这不仅仅是写几行代码更是一场与平台风控工程师的智力博弈理解他们的设计思路才能找到最优雅、最持久的应对策略。2. 核心思路拆解从黑盒到白盒的逆向路径面对一个未知的签名算法最忌讳的就是一头扎进代码里胡乱尝试。一个清晰的逆向路径能事半功倍。我的核心思路可以概括为“由外而内动静结合”。2.1 逆向工程的基本方法论逆向工程不是瞎猜它遵循一套科学的方法论。对于Web端的签名尤其是JavaScript实现的我们通常采用“动态分析为主静态分析为辅”的策略。动态分析指的是在浏览器真实环境中运行目标网页通过开发者工具监控网络请求、调用堆栈、内存变量变化从而观察签名生成的完整流程。静态分析则是在拿到关键的JavaScript文件后对其进行代码格式化、反混淆、逻辑梳理理解其算法原理。两者结合动态分析帮我们快速定位关键函数和参数静态分析帮我们彻底理解其内部逻辑为后续的代码复现打下坚实基础。2.2 Shopee签名机制的特点预判在开始动手前根据经验和对Shopee这类大型平台的分析我们可以对其签名机制做一些合理预判前端生成签名极大概率是在浏览器端JavaScript生成的因为需要用到浏览器的环境信息如User-Agent、屏幕分辨率等或本地计算能力。参数绑定签名不会只对一个固定字符串加密它必然与本次请求的某些特征强绑定例如请求的URL路径、查询参数Query String、请求体Body、甚至时间戳。时效性为了防止签名被复用通常会引入时间戳timestamp或随机数nonce作为签名因子之一使得每个签名在短时间内如几秒或几分钟内唯一有效。算法复杂度可能使用常见的哈希算法如MD5、SHA-256也可能使用HMAC或者更复杂的自定义算法。大型平台为了增加逆向难度常会对JavaScript代码进行混淆和压缩。环境检测签名过程可能隐式或显式地依赖浏览器环境比如检查window、document对象是否存在或执行一些只有浏览器中才有效的JavaScript代码。基于这些预判我们的逆向工作就有了明确的搜索方向。3. 实操环境准备与关键请求定位工欲善其事必先利其器。逆向工作对工具链的要求比较高。3.1 工具选型与配置抓包工具Charles或Fiddler是必备的。它们能拦截和查看所有HTTP/HTTPS流量。我更喜欢Charles因为它的界面更清晰重发请求Compose和断点调试Breakpoints功能非常强大。配置时记得在电脑和手机如果需要抓APP包上安装并信任Charles的根证书。浏览器开发者工具Chrome DevTools 或 Edge DevTools 是主战场。重点关注Network网络面板和Sources源代码面板。在Network面板中要勾选“Preserve log”保留日志并禁用缓存Disable cache。反混淆与调试工具浏览器自带的调试器通常够用。但对于高度混淆的代码可以借助一些浏览器插件或在线工具进行初步的格式化但最关键的还是靠人脑去理解逻辑。“Pretty print”美化代码功能是Sources面板里的神器一定要会用。编程环境Python 是首选配合requests、execjs用于执行JavaScript、node.js本地环境等库。准备好一个代码编辑器如VS Code。3.2 定位签名参数与入口打开Shopee的网页例如搜索页面https://shopee.sg/search?keywordshoes。在Charles或浏览器Network面板中仔细查看请求头Headers。很快你会发现关键的接口请求通常是XHR类型其请求头里会包含一些特殊的字段这正是我们搜索片段中提到的sap-ri和x-sap。不同的版本或地区字段名可能略有差异但思路一致。例如你可能会看到x-api-sourcex-requested-with以及最重要的x-sap-ri和x-sap-signature或类似名称。这些字段的值通常是一长串看似随机的字母数字组合它们就是签名的产物。我们的目标就是找出生成这些值的JavaScript函数。操作技巧在Network面板中找到携带这些签名头的请求右键点击选择“Copy - Copy as cURL”。然后粘贴到一个文本编辑器中你可以清晰地看到所有头信息。这有助于你理解请求的全貌。4. 动态调试追踪签名函数的执行过程找到了签名参数下一步就是找到生成它们的代码。4.1 启用浏览器调试器进行堆栈追踪在Network面板中点击那个携带签名头的请求。切换到Initiator标签页。这里显示了是哪个脚本文件发起了这个网络请求。点击旁边那个行号它会直接跳转到Sources面板对应的代码位置。这通常是我们寻找签名函数的第一个入口。更通用的方法是使用“XHR/Fetch Breakpoints”。在Sources面板的XHR Breakpoints区域点击“”号添加一个断点条件可以填写请求URL的一部分比如“/api/”。这样当任何包含“/api/”的请求发起时浏览器就会自动暂停Break此时调用堆栈Call Stack里就会显示出导致这个请求的所有函数调用链。4.2 关键函数定位与参数分析当断点触发JavaScript执行暂停后观察Call Stack面板。你会看到一长串函数调用。从下往上或从上往下看寻找那些看起来与网络请求、参数处理、加密相关的函数名。例如可能会看到sign、encrypt、getHeaders、axios.interceptors.request.use如果Shopee使用axios等。点击Call Stack中的不同函数在右侧代码区查看其源码。即使代码被混淆你也可以通过观察函数内部的变量赋值、参数传递来推断其作用。重点关注在疑似签名生成的函数附近使用“Step Over” (F10)、“Step Into” (F11)等按钮单步执行。同时将鼠标悬停在变量上或在Console面板中直接输入变量名查看其当前值。你的目标是找到最终赋值给x-sap-ri和x-sap-signature的那个变量或表达式。记录关键信息一旦找到生成签名的核心函数记下函数名混淆后的也行。该函数所在的JavaScript文件路径或名称。函数的输入参数是什么通常是URL、请求方法、请求数据、时间戳等。函数的输出是什么就是签名值。实操心得混淆后的代码变量名可能是a、b、c、t、e、n等单字母非常难读。这时候不要慌多利用断点和单步执行观察这些变量在关键节点如进入函数时、经过某个运算后的值变化结合上下文比如它后面被拼接成URL或放入请求头来反推它的含义。例如一个变量在经过一系列操作后被放入了x-sap-ri请求头那它很可能就是ri参数。5. 静态分析与算法还原通过动态调试我们定位到了核心函数。接下来就需要把这个函数的逻辑完整地“搬”到我们的Python环境中。5.1 提取并格式化关键JavaScript代码在Sources面板找到包含签名函数的那个.js文件。它可能是一个很大的vendor.xxxx.js或app.xxxx.js。选中关键函数及其周边相关的依赖函数比如它调用的其他加密函数、工具函数右键点击选择“Save as…”保存到本地。用代码编辑器打开如果代码是压缩成一行的先用浏览器的“Pretty Print”功能格式化或者使用在线JS格式化工具让代码变得可读。5.2 分析算法逻辑与依赖格式化后的代码虽然变量名依然混乱但结构清晰了。你需要像侦探一样梳理逻辑找出入口函数确认哪个函数是我们动态调试时找到的签名生成函数。理清输入输出明确这个函数接收哪些参数返回什么。拆解计算步骤一步步分析函数内部做了什么。常见的操作包括字符串拼接将URL路径、查询参数、时间戳、固定字符串等按特定顺序拼接成一个待签名的原始字符串。参数排序可能对URL的查询参数Query Params按照字典序排序后再拼接。哈希运算调用CryptoJS.MD5、CryptoJS.HmacSHA256或浏览器原生的SubtleCryptoAPI。Base64编码将哈希结果进行Base64编码。引入随机数生成一个随机字符串作为ri(request id)。识别外部依赖注意函数内部是否使用了浏览器特有的对象如window、navigator、document或全局变量。这些在Node.js或execjs环境中可能不存在需要我们在复现时模拟或替换。5.3 使用Python复现签名算法这是最具挑战性也最有成就感的一步。目标是写一个Python函数给定相同的输入能输出与浏览器完全一致的签名。import hashlib import hmac import base64 import time import json from urllib.parse import urlparse, parse_qs, urlencode class ShopeeSigner: def __init__(self): # 这里可能需要初始化一些从JS中提取的固定密钥或盐值 self.secret_key 从JS中分析出的一个固定值可能藏在代码里 self.app_version 某个版本号 self.platform web def generate_ri(self): 生成随机的request id模仿JS中的实现 import uuid # 有时可能是时间戳随机数的一种组合需要根据JS代码确定 return str(uuid.uuid4()).replace(-, ) def generate_signature(self, method, url, bodyNone, timestampNone): 核心签名函数 :param method: HTTP方法如 GET, POST :param url: 完整的请求URL :param body: POST请求的JSON体字典格式 :param timestamp: 时间戳秒级如果为None则使用当前时间 :return: 包含 ri 和 signature 的字典 if timestamp is None: timestamp int(time.time()) # 1. 解析URL获取路径和查询参数 parsed_url urlparse(url) path parsed_url.path # 例如 /api/v4/item/get query_params parse_qs(parsed_url.query) # 2. 处理查询参数可能需要排序、过滤空值、特定格式编码 # 根据JS逻辑这里可能需要按key排序后拼接成 key1value1key2value2 的形式 sorted_query_str if query_params: # 注意parse_qs返回的值是列表需要根据实际情况处理如取第一个值 sorted_items sorted(query_params.items(), keylambda x: x[0]) sorted_query_str .join([f{k}{v[0]} for k, v in sorted_items if v[0]]) # 3. 处理请求体 body_str if body and method.upper() POST: # 根据JS逻辑可能是JSON字符串并且可能需要按key排序后再字符串化 body_str json.dumps(body, separators(,, :), sort_keysTrue) # 4. 拼接待签名字符串 (这是最关键的一步顺序和格式必须与JS完全一致) # 以下拼接方式仅为示例真实逻辑需逆向得出 string_to_sign f{method}\n{path}\n{timestamp}\n{sorted_query_str}\n{body_str}\n{self.app_version} # 或者可能是 string_to_sign f{path}?{sorted_query_str}|{timestamp}|{body_str}|{self.secret_key} # 5. 计算签名 (示例为HMAC-SHA256) # 密钥可能是固定的也可能是动态生成的 sign_key self.secret_key.encode(utf-8) message string_to_sign.encode(utf-8) hmac_digest hmac.new(sign_key, message, digestmodhashlib.sha256).digest() signature base64.b64encode(hmac_digest).decode(utf-8) # 6. 生成ri ri self.generate_ri() return { x-sap-ri: ri, x-sap-signature: signature, x-timestamp: str(timestamp) # 有时时间戳也需要放在头里 } # 使用示例 signer ShopeeSigner() headers signer.generate_signature( methodGET, urlhttps://shopee.sg/api/v4/search/search_items?byrelevancykeywordshoeslimit50newest0orderdescpage_typesearchscenarioPAGE_GLOBAL_SEARCHversion2 ) print(headers) # 将返回的headers添加到你的requests请求中核心难点与技巧拼接string_to_sign的规则千变万化是逆向的精髓。你必须通过反复的断点调试在JS代码执行到计算签名前的那一刻把参与拼接的所有变量的值都记录下来。然后在Python里用完全相同的顺序、格式包括换行符\n、分隔符|等进行拼接。一个空格或顺序的错误都会导致签名无效。最可靠的方法是对比验证用你的Python函数算一个签名同时用浏览器发起一次真实请求对比两个签名是否完全一致。如果不同就检查拼接逻辑的每一个细节。6. 构建稳定采集方案逆向出签名算法只是第一步要构建稳定的采集系统还需要考虑更多工程化问题。6.1 请求头与会话管理Shopee的签名不是孤立的它通常需要和其他请求头配合使用。一个完整的请求头可能包括User-Agent: 模拟一个真实的浏览器。Referer: 设置为当前页面的来源URL。Cookie: 维持会话状态。对于公开数据采集可能不需要登录态Cookie但一些基础Cookie如地区、语言可能需要。Accept-Language,Accept-Encoding等标准头。我们逆向得到的x-sap-ri,x-sap-signature,x-timestamp。建议使用requests.Session()来管理会话它可以自动处理Cookie并允许你为所有请求设置公共的头部。import requests session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Accept: application/json, text/plain, */*, Accept-Language: en-US,en;q0.9, Referer: https://shopee.sg/, }) signer ShopeeSigner() def search_shopee(keyword, limit50): url fhttps://shopee.sg/api/v4/search/search_items?byrelevancykeyword{keyword}limit{limit}newest0 # 生成签名头 sign_headers signer.generate_signature(methodGET, urlurl) # 更新本次请求的头部 headers {**session.headers, **sign_headers} try: resp session.get(url, headersheaders, timeout10) resp.raise_for_status() # 检查HTTP错误 return resp.json() except requests.exceptions.RequestException as e: print(f请求失败: {e}) return None6.2 频率控制与IP代理策略即使签名正确过快的请求频率也会触发风控导致IP被暂时封锁。设置合理的延迟在请求之间使用time.sleep(random.uniform(1, 3))增加随机间隔模拟人类操作。使用代理IP池对于大规模采集这是必须的。可以使用付费代理服务并实现IP自动切换逻辑。确保代理IP的质量高匿名、低延迟、高可用。错误重试与退避当请求失败返回403、429等状态码时不应立即重试。实现一个带有指数退避机制的重试逻辑并更换代理IP。import time import random from typing import Optional class RobustFetcher: def __init__(self, proxy_poolNone): self.session requests.Session() self.signer ShopeeSigner() self.proxy_pool proxy_pool # 假设这是一个返回代理字典的函数/列表 self.proxy_index 0 def get_next_proxy(self) - Optional[dict]: if not self.proxy_pool: return None proxy self.proxy_pool[self.proxy_index % len(self.proxy_pool)] self.proxy_index 1 return {http: proxy, https: proxy} def fetch_with_retry(self, url, methodGET, max_retries3): retries 0 while retries max_retries: proxy self.get_next_proxy() sign_headers self.signer.generate_signature(method, url) headers {**self.session.headers, **sign_headers} try: resp self.session.request(method, url, headersheaders, proxiesproxy, timeout15) if resp.status_code 200: return resp.json() elif resp.status_code in [403, 429]: print(f请求被阻止 (状态码: {resp.status_code}) 更换代理并重试...) time.sleep(2 ** retries random.random()) # 指数退避 else: resp.raise_for_status() except Exception as e: print(f请求异常: {e}) retries 1 time.sleep(random.uniform(2, 5)) # 重试前等待 print(f重试{max_retries}次后仍失败: {url}) return None6.3 数据解析与存储成功获取到数据通常是JSON格式后需要根据业务需求进行解析和清洗。结构化数据将JSON中的商品列表、详情等信息提取出来转换成字典或列表。数据清洗处理价格可能包含货币符号、库存状态、图片URL等。持久化存储根据数据量选择存储方式。小规模测试可以用JSON或CSV文件。生产环境建议使用数据库如SQLite轻量、PostgreSQL或MongoDB灵活。增量更新设计机制识别新上架或已下架的商品避免重复采集全部数据。7. 常见问题排查与维护策略逆向方案不是一劳永逸的平台会更新我们的方案也需要维护。7.1 签名失效的快速排查流程当某天发现采集脚本突然全部失败返回403时按以下步骤排查手动浏览器验证首先用浏览器打开目标页面打开开发者工具查看一次成功请求的签名头。确认接口本身是否依然可用。对比签名参数用你的Python脚本为同一个URL生成签名与浏览器请求中的签名值进行对比。如果不一致说明签名算法可能已变更。重新动态调试重复第4节的步骤定位新的签名函数。重点关注之前保存的JS文件是否有更新通过文件哈希或内容对比。检查依赖环境确认是否引入了新的浏览器环境检测如Canvas指纹、WebGL指纹。简单的请求头模拟可能不再足够需要考虑使用更高级的模拟方式如playwright或puppeteer无头浏览器。7.2 应对算法升级与风控强化平台升级反爬策略是常态要有心理和技术准备。代码混淆升级可能从简单的变量名混淆升级为控制流扁平化、字符串加密等。这大大增加了静态分析的难度。此时更需要依赖动态调试关注函数的输入输出必要时可以尝试“黑盒调用”即用execjs直接执行整个混淆后的JS函数片段而不是完全用Python重写。增加环境指纹除了请求头平台可能通过JavaScript收集更详细的浏览器指纹字体、插件、硬件信息等。对抗此策略需要使用能高度模拟真实浏览器的工具如playwright它可以生成完整的浏览器上下文包括所有指纹信息。人机验证介入在频繁访问后可能会弹出滑块验证码如Akamai, hCaptcha。这是最棘手的情况。解决方案包括降低频率这是最有效且合规的方法。验证码识别服务接入第三方打码平台成本较高。自动化测试工具尝试用playwright等自动化操作通过简单验证成功率不稳定且可能违反平台条款。7.3 长期维护建议模块化设计将签名生成、请求发送、错误处理、数据解析等模块分离。这样当签名算法变更时你只需要修改ShopeeSigner这个类。监控与告警为采集脚本设置健康检查。如果连续多次请求失败或返回非预期数据通过邮件、钉钉、Telegram机器人等方式发送告警。数据备份与版本控制对逆向出的关键JS代码片段和对应的Python实现代码进行版本管理如Git。记录每次算法变更的时间和特征便于未来回溯。遵守规则与伦理明确你的数据采集目的控制请求频率避免对目标网站服务器造成过大压力。只采集公开数据不尝试破解登录、不涉及用户隐私。