1. 项目概述当爬虫遇到JS加密最近在数据采集圈子里一个话题的热度居高不下如何搞定那些前端做了复杂JS加密的网站尤其是像“黑猫投诉”这类汇聚了大量消费维权信息的平台数据价值高但反爬措施也相当严密。很多新手朋友兴冲冲地拿着requests库就冲了上去结果发现返回的要么是空白页要么是一堆看不懂的乱码核心数据一个都拿不到。问题就出在那些动态生成的、每次请求都变化的加密参数上。这个项目我们就来啃一啃这块“硬骨头”。目标很明确逆向分析黑猫投诉平台以某次查询为例的网页请求找到关键加密参数的生成逻辑并用Python完整复现这一过程最终成功获取到结构化的投诉数据。这不仅仅是写几行代码更是一次对前端JavaScript代码的“侦探”工作涉及到对网络请求的抓包分析、对混淆JS代码的调试与理解以及用Python模拟浏览器执行环境。如果你之前主要爬取静态页面或简单的Ajax接口那么这次实战将是你爬虫技能的一次重要进阶。2. 核心思路与逆向分析准备面对JS加密无脑硬冲是行不通的。我们的核心思路是“模拟”即让我们的Python爬虫程序能够像真实浏览器一样生成出服务端认可的有效请求参数。整个过程可以拆解为四个关键步骤抓包定位、寻找入口、逆向推导、代码复现。2.1 工具选型你的“手术刀”和“显微镜”工欲善其事必先利其器。逆向JS加密以下几样工具必不可少浏览器开发者工具Chrome DevTools这是主战场。重点关注Network网络面板和Sources源代码面板。Network面板用于捕获和分析所有HTTP请求Sources面板用于查看、调试页面加载的JavaScript文件。抓包工具虽然浏览器自带的Network面板很强但专业的抓包工具如Fiddler或Charles在设置断点、修改请求、批量查看等方面更有优势。它们能帮你更清晰地看到请求/响应的完整过程。Node.js 环境有些加密逻辑可能依赖浏览器环境下的特有对象如window、document或者代码被严重混淆。我们可以在本地用Node.js配合jsdom等库来模拟浏览器环境单独执行和调试关键的JS函数片段这比在浏览器控制台里调试大段混淆代码要方便得多。Python 生态库requests/httpx用于发送最终的HTTP请求。execjs一个非常关键的库它允许你在Python中调用JavaScript代码。我们可以把逆向出来的JS加密函数放到一个.js文件里然后用execjs去执行它并获取结果。PyExecJSexecjs的一个后端通常需要安装一个JS运行时如Node.js来配合工作。注意逆向工程的法律和道德边界必须遵守。我们的所有分析应仅限于公开可访问的页面用于个人学习与技术研究严禁对网站进行恶意攻击、高频访问造成压力或窃取非公开、用户隐私数据。务必在目标网站的robots.txt协议允许范围内进行操作。2.2 第一步网络抓包与参数定位打开黑猫投诉平台的搜索页面我们随意输入一个关键词比如“快递”点击搜索。此时立刻打开浏览器的开发者工具切换到Network面板并勾选“Preserve log”保留日志。很快你会看到列表刷新出现了一个新的XHR或Fetch请求其响应里包含了我们想要的投诉列表数据。点击这个请求查看它的详细信息Headers请求头关注Cookie、User-Agent、Referer等这些可能需要模拟。Payload请求负载或Query String Parameters查询参数这里就是重点了你会发现除了我们输入的关键词还有一堆长长的、看似随机的字符串参数例如可能叫做sign、token、_t、encryptKey之类的。它们的值通常是一串由字母数字组成的密文。关键操作尝试直接复制这个请求的cURL命令用Python的requests库简单重放一次。十有八九你会得到一个错误响应比如“签名无效”或“请求参数错误”。这证实了这些参数是动态生成且经过校验的。我们的任务就是找到生成这些参数尤其是那个最关键的签名参数的JavaScript代码。3. 深入JS丛林寻找加密函数入口知道要找什么之后下一步就是找到它在哪里被创造出来。这通常是最耗时的部分。3.1 搜索与断点调试在开发者工具的Network面板中找到那个携带加密参数的请求右键点击它选择“Search in all files”在所有文件中搜索。然后尝试搜索那些加密参数的名字比如sign。如果运气好你可能会直接在一个JavaScript文件里找到包含sign赋值语句的代码。但更多时候由于代码被压缩和混淆你搜到的可能是一堆难以理解的变量名。这时我们需要更聪明的方法。XHR/Fetch 断点在Sources面板找到“XHR/Fetch Breakpoints”选项添加一个新的断点URL包含你请求接口的关键字部分如/api/complaint/list。这样当浏览器发起这个特定请求时代码执行会自动暂停我们可以查看此时的调用栈。事件监听器断点在Sources面板的“Event Listener Breakpoints”中可以勾选“Script”、“XHR”等类别。当有相关事件发生时也会断住。跟栈无论通过哪种方式断住后关注右侧的“Call Stack”调用栈。这里显示了当前暂停点的函数调用链。从栈顶往下看寻找那些属于网站自身域名下的JS文件而不是chrom-extension或node_modules逐层点击观察代码。目标就是找到那个最终设置请求参数比如sign的函数。3.2 分析一个典型的加密流程经过一番查找你可能会定位到一个函数它接收一些原始参数如关键词、页码、时间戳然后进行一系列操作输出加密后的字符串。一个非常常见的流程是参数排序与拼接将所有待传参数按字典序排序然后拼接成key1value1key2value2...格式的字符串。添加盐值Salt在拼接后的字符串末尾加上一个固定的、不对外公开的字符串盐值。这个盐值可能硬编码在JS里也可能来自某个全局变量。哈希运算对拼接后的字符串进行哈希计算通常是MD5或SHA-256。在JS中可能会用到CryptoJS这个库或者浏览器原生的SubtleCryptoAPI。二次处理或编码将哈希得到的十六进制字符串可能再进行一次Base64编码或者截取其中一部分最终形成我们看到的sign。实操心得在混淆的代码中识别出CryptoJS.MD5(...).toString()或window.crypto.subtle.digest(SHA-256, ...)这样的语句就是找到了加密的核心。你需要仔细理清它的输入即拼接前的原始参数有哪些和输出。4. 实战Python复现JS加密逻辑假设我们已经成功逆向找到了生成sign参数的JS函数它被混淆后可能叫function s(t) { ... }。现在我们需要在Python环境中复现它。4.1 提取并净化JS代码不要试图在Python中直接翻译整个复杂的、可能依赖浏览器环境的JS函数。更稳妥的做法是将这个函数以及它直接依赖的辅助函数、变量从Sources面板中完整地复制出来保存到一个本地的.js文件中例如encrypt.js。关键步骤在调试器中将鼠标悬停在变量上或使用控制台打印确认你提取的函数所需的全局变量或外部依赖。常见的依赖如CryptoJS如果代码中直接使用了CryptoJS你需要确保在Node.js环境中能引用到它通过npm install crypto-js或者更简单点直接找到CryptoJS库中对应算法如MD5的源码实现一并复制到你的encrypt.js里。网上有开源的、纯JS写的CryptoJS单文件版本可以直接引入。将你找到的加密函数例如s进行“封装”。在encrypt.js文件末尾添加几行代码使其能在Node环境下被调用。例如// encrypt.js 末尾添加 // 假设我们找到的加密主函数是 function getSign(params) {...} function getSign(params) { // ... 这里是逆向出来的加密逻辑 ... return encryptedSign; } // 为了让Python能调用我们将其暴露出来 if (typeof module ! undefined module.exports) { module.exports { getSign: getSign }; }4.2 使用 execjs 桥接 Python 与 JS现在我们可以在Python中利用execjs来调用这个本地JS文件中的函数。import execjs import time import random # 1. 读取我们封装好的JS代码 with open(encrypt.js, r, encodingutf-8) as f: js_code f.read() # 2. 创建execjs上下文 ctx execjs.compile(js_code) # 3. 准备原始参数模拟前端构造的参数 def generate_params(keyword, page1): # 这些参数名和格式需要根据实际抓包分析得来 base_params { keyword: keyword, page: page, pageSize: 20, t: int(time.time() * 1000), # 常见的时间戳参数 nonce: random.randint(100000, 999999) # 常见的随机数参数 } # 注意可能有些参数是固定值也需要加入 base_params[platform] web # ... 其他必要参数 return base_params # 4. 调用JS函数生成签名 def get_encrypted_sign(params): # 将参数字典转换为JS函数需要的格式通常是JSON字符串 # 具体格式取决于你封装的JS函数如何接收参数 sign ctx.call(getSign, params) return sign # 5. 组合最终请求参数 keyword 快递 raw_params generate_params(keyword) signature get_encrypted_sign(raw_params) # 将签名加入请求参数 final_params raw_params.copy() final_params[sign] signature # 参数名也可能是‘signature’等根据实际修改 print(最终请求参数, final_params)4.3 处理常见的环境依赖问题你的JS代码可能在浏览器里运行正常但在Node或execjs环境下报错常见问题有window、document未定义如果加密函数里用到了window.btoaBase64编码或document.xxx在Node环境下这些是不存在的。解决方案是提供补丁。对于btoa可以用Node.js内置的Buffer模拟在JS文件开头添加if (typeof window undefined) { global.window { btoa: (str) Buffer.from(str).toString(base64) } }。或者直接寻找不依赖浏览器环境的替代实现。CryptoJS未定义确保你将CryptoJS的源码或你需要的部分如MD5完整地复制到了encrypt.js的开头或者通过Node的require引入如果使用完整的Node环境而非execjs的简单编译。复杂的代码混淆与动态执行有些网站会使用eval、Function构造函数或WebAssembly来执行核心加密逻辑增加逆向难度。面对eval可以在浏览器调试时在eval调用前打条件断点将其要执行的字符串代码复制出来。WebAssembly则更为复杂可能需要分析.wasm文件。踩坑记录在一次实战中我发现sign的生成依赖了一个名为_getSecret()的函数这个函数返回的盐值每次页面加载都会变。它并不是硬编码而是通过一个初始的API请求从服务端获取的。这意味着我们的爬虫脚本在构造签名前必须先模拟一次“初始化”请求拿到这个动态的盐值。这提醒我们逆向时不能只看一个函数要关注整个数据流的生命周期。5. 构建完整的爬虫程序加密问题解决后构建一个健壮的爬虫就相对常规了但仍有细节需要注意。5.1 请求头与会话管理服务端除了校验参数通常还会校验请求头。至少需要设置headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://tousu.sina.com.cn/, # 根据实际修改 Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9, Content-Type: application/x-www-form-urlencoded; charsetUTF-8, # 根据实际请求设置 }使用requests.Session()或httpx.Client()来维持会话自动处理Cookie这对于需要登录或有多步交互的场景很重要。5.2 数据解析与存储成功请求后你会拿到一个JSON响应。仔细检查其结构通常data字段下的列表就是投诉数据。每条数据可能包含投诉编号、标题、内容、投诉对象、投诉时间、状态、商家回复等字段。使用Python的json库解析然后用pandas进行数据清洗和整理或者直接存入数据库如SQLite、MySQL或文件如CSV、JSON。import requests import json import pandas as pd def fetch_complaints(keyword, page): params construct_params_with_sign(keyword, page) # 这是之前封装好的函数 session requests.Session() session.headers.update(headers) try: resp session.get(https://tousu.sina.com.cn/api/complaint/list, paramsparams, timeout10) resp.raise_for_status() # 检查HTTP错误 data_json resp.json() if data_json.get(code) 200: # 根据实际响应结构判断成功 items data_json.get(data, {}).get(list, []) return items else: print(f请求失败状态码{data_json.get(code)}, 信息{data_json.get(msg)}) return [] except requests.exceptions.RequestException as e: print(f网络请求异常{e}) return [] except json.JSONDecodeError as e: print(fJSON解析异常{e}) return [] # 爬取多页 all_complaints [] for page in range(1, 6): # 爬取前5页 print(f正在爬取第{page}页...) items fetch_complaints(快递, page) if not items: # 如果某一页没数据可能已到底 break all_complaints.extend(items) time.sleep(1 random.random()) # 礼貌性延迟避免请求过快 # 转换为DataFrame df pd.DataFrame(all_complaints) print(f共爬取到{len(df)}条投诉数据。) # 保存到CSV df.to_csv(黑猫投诉_快递.csv, indexFalse, encodingutf-8-sig)5.3 错误处理与重试机制网络爬虫必须健壮。要添加完善的异常处理try...except和重试逻辑。可以使用tenacity库或自己实现一个简单的重试装饰器在遇到网络超时、连接错误或服务端返回5xx错误时自动重试几次。from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10)) def safe_fetch(url, params): # 包装请求函数最多重试3次等待时间指数增长 return requests.get(url, paramsparams, timeout15)6. 常见问题排查与进阶技巧即使按照流程走你也可能会遇到各种问题。这里记录一些典型的排查思路。6.1 问题排查清单问题现象可能原因排查思路签名无效/参数错误1. 加密函数提取不完整漏掉了某个依赖的变量或函数。2. 参数拼接顺序与服务器校验顺序不一致。3. 盐值salt错误或动态变化未捕获。4. 时间戳格式或精度不对如服务器用秒你用了毫秒。1. 在浏览器中用相同的输入分别运行你的JS代码和网站原代码对比输出是否完全一致。2. 检查参数排序规则是否去除了某些空值参数3. 清空Cookie重新打开页面观察初始请求看是否有获取动态密钥的接口。4. 将浏览器生成正确签名时的所有参数包括隐藏的完整记录下来与你脚本生成的参数逐一对比。返回空数据或验证码1. 请求频率过高触发反爬。2. Cookie或会话失效。3.User-Agent等请求头被识别为爬虫。4. IP地址被限制。1. 大幅增加请求间隔加入随机延迟。2. 检查是否需要先访问首页获取初始Cookie。3. 使用更常见的User-Agent或使用fake_useragent库随机生成。4. 考虑使用代理IP池。对于重要数据可能需要模拟更完整的行为流。execjs执行JS报错1. JS代码中存在浏览器环境特有的对象或API。2. JS代码语法错误如复制时漏了括号。3.CryptoJS等库未正确引入。1. 在Node.js环境中单独测试你的encrypt.js文件看是否能正常运行。2. 使用node -c encrypt.js检查语法。3. 在JS文件开头通过console.log输出中间结果定位错误位置。数据乱码或格式不对响应编码问题。检查响应头的Content-Type使用resp.encoding或resp.content.decode(正确的编码)来设置。6.2 应对更复杂的反爬策略一些网站会采用更高级的防御措施代码动态加载与混淆升级核心加密JS可能不是一次性加载而是通过异步请求分块加载并动态拼接执行。这时需要仔细追踪网络请求找到所有相关的JS片段。混淆器也会更新需要重新分析。WebAssembly加密核心算法用WebAssembly实现逆向难度极大。通常的应对思路是不尝试逆向WASM本身而是通过浏览器调试工具在WASM模块的输入输出接口处设置断点记录下输入和对应的输出然后在Python中通过“打表”或模拟调用外部WASM运行时的方式来处理。或者寻找是否有更上游的、生成WASM所需参数的JS逻辑。行为指纹识别除了参数网站可能通过JavaScript检测鼠标移动、点击节奏、屏幕分辨率、字体列表等生成浏览器指纹。对抗这类检测需要更复杂的模拟如使用Puppeteer、Selenium或Playwright这类自动化测试工具来真实驱动一个浏览器。但这会牺牲大量性能。取舍之道是先用本文的逆向思路尝试破解参数加密如果不行再考虑上无头浏览器。个人体会爬虫与反爬是一场持续的博弈。对于JS加密核心在于耐心和细致的观察。不要被混淆的变量名吓倒多使用调试器的“监视Watch”功能跟踪变量变化多用控制台输出中间值。将大问题分解为小问题先找到加密入口再理清输入输出最后提取关键代码。成功破解一次之后你会积累下宝贵的模式识别经验再遇到类似网站分析速度会快很多。最后始终牢记合规与尊重控制请求速率只获取公开且允许的数据这是技术人应有的素养。