
1. 项目概述当爬虫遇上JS加密我们该如何破局最近在写一个数据采集项目时我又一次被网站的JS加密参数给“教育”了。明明浏览器里点几下就能看到的数据用Python的requests库发请求返回的要么是空数据要么直接就是403。这种场景相信每一个写过爬虫的朋友都深有体会。问题的核心往往就出在那些由前端JavaScript动态生成并加密的请求参数上比如常见的sign、token、_t、encryptData等。这些参数就像是网站颁发给“合法”浏览器的动态门票没有这张票你的爬虫连门都进不去。这其实就是典型的“反爬虫”技术之一。网站通过在前端执行复杂的JavaScript代码对请求的必要参数如时间戳、页码、查询条件进行加密或混淆使得直接模拟HTTP请求变得困难。其目的很明确增加自动化数据抓取的成本将流量留给真实的用户浏览器。对于爬虫开发者而言这从简单的“发送请求-解析HTML”模式升级到了需要理解前端逻辑、甚至模拟浏览器执行环境的“逆向工程”挑战。那么面对JS加密这座大山我们真的只能望而却步或者完全依赖笨重的浏览器自动化工具如Selenium吗当然不是。经过多个项目的实战我总结出了一套核心思路与阶梯式的解决方案。从最直接的“扣代码”到更工程化的RPC调用每种方法都有其适用场景和优劣。今天我就来系统性地拆解一下破解JS加密参数的核心思路并附上可落地的Python代码实现。无论你是正在被某个具体加密参数卡住还是想系统性地提升应对反爬的能力这篇文章都能给你提供清晰的路径和实用的工具。注意本文所有技术讨论及代码示例均旨在用于技术学习、安全测试在授权范围内及提升开发者对Web应用安全的理解。在实际应用中请务必遵守目标网站的robots.txt协议尊重版权与数据所有权控制请求频率避免对目标服务器造成过大压力。2. 核心思路拆解从“黑盒”到“白盒”的逆向思维面对一个带有JS加密参数的请求我们的目标很明确用Python代码生成出与浏览器行为完全一致的加密参数。这个过程本质上是一个逆向工程。我们可以将思路分为几个层次由浅入深逐步攻坚。2.1 思路一直接复制与重放——最简单但局限最大这是最朴素的想法。在浏览器开发者工具F12的Network面板中找到那个携带加密参数的请求直接复制其完整的cURL命令或请求头、请求体然后在Python代码中原样重放。实现方式在Chrome的Network面板右键点击目标请求选择“Copy” - “Copy as cURL”。将复制的内容粘贴到支持cURL转代码的网站或工具如curlconverter.com中转换为Pythonrequests代码。直接运行这段代码。优点简单粗暴对于一次性抓取或参数有效期极长的场景可能有效。致命缺点参数过期绝大多数加密参数都与时间戳(timestamp)、随机数(nonce)绑定具有极强的时效性短则几秒长则几分钟。复制的参数很快失效。缺乏通用性每个请求都需要手动复制无法实现自动化。无法应对变化网站更新加密逻辑后方法立即失效。结论此方法仅适用于临时、手动的数据获取或用于获取第一次请求的“样本”为后续逆向分析提供素材不能作为自动化爬虫的解决方案。2.2 思路二逆向分析与代码移植——最核心的硬核解法这是破解JS加密最经典、最根本的方法。核心思想是深入分析前端JavaScript代码找到生成加密参数的函数理解其输入、输出和算法逻辑然后将该逻辑用Python重新实现一遍。关键步骤定位加密函数在开发者工具的Network面板中找到携带加密参数的请求查看其Initiator调用栈逐步回溯找到最终生成该参数的JavaScript函数。也可以搜索参数名如sign或加密特征字符串如MD5、SHA、encrypt来定位关键代码。分析算法逻辑阅读并理解该函数。它可能涉及参数拼接将多个请求参数按特定顺序和格式如key1value1key2value2拼接成一个字符串。加盐在拼接的字符串前后加上一个固定的或动态的“盐值”(salt)。哈希运算使用MD5、SHA-1、SHA-256等算法计算哈希值。对称加密使用AES、DES等算法进行加密可能需要处理CBC、ECB等模式以及PKCS7填充。非对称加密使用RSA算法加密通常需要处理公钥。Base64/Hex编码对加密或哈希后的二进制结果进行编码使其成为可传输的字符串。Python代码复现使用Python的相应库如hashlib,hmac,Crypto,rsa,base64将上述逻辑精确地重写出来。优点一旦成功效率极高请求速度快资源消耗低与浏览器行为完全一致。缺点技术门槛高需要一定的JavaScript和密码学知识。遇到代码混淆Obfuscation严重的网站分析过程会异常痛苦。适用场景加密逻辑相对清晰、未做高强度混淆的网站。这是衡量一个爬虫工程师技术水平的关键能力。2.3 思路三内置引擎执行——平衡效率与复杂度的选择当JS代码混淆严重难以人工逆向或者加密逻辑依赖浏览器特定的环境变量如window、document对象时我们可以考虑将JavaScript代码“搬”到Python环境中来执行。核心工具PyExecJS、js2py、Node.js子进程。PyExecJS一个通用桥梁可以调用本地安装的JavaScript运行时如Node.js来执行JS代码。js2py一个纯Python的JavaScript解释器无需外部环境。实现方式从网页源代码或JS文件中提取出包含加密函数的完整代码段。在Python中使用上述工具创建一个JS执行环境。将提取的JS代码载入该环境。像在浏览器中一样调用特定的JS函数并传入参数获取加密结果。优点避开了复杂的代码逆向只要能提取出正确的JS代码块就能直接使用其功能。对于依赖浏览器环境的加密有一定应对能力。缺点性能开销启动JS引擎和执行代码比纯Python计算慢。环境差异一些极度依赖浏览器特定对象如复杂的DOM操作的代码可能无法在无头环境中完美运行。代码提取如何从混淆、压缩的代码中准确提取出功能完整的片段本身也是一个挑战。适用场景加密逻辑复杂但代码可提取且对性能要求不是极端苛刻的场景。2.4 思路四无头浏览器自动化——终极武器也是最后的选择当所有以上方法都失效时例如加密参数的计算严重依赖完整的页面渲染、用户交互事件链或图形验证码我们只能祭出终极武器模拟一个真实的浏览器。核心工具Selenium、Playwright、Puppeteer通过pyppeteer调用。 这些工具可以自动化控制Chrome、Firefox等真实浏览器执行点击、输入、滚动等操作并在此过程中自然触发所有JS代码最终获取渲染后的页面数据或网络请求。优点通杀几乎所有前端反爬策略包括最复杂的加密。因为你的爬虫行为与真人操作浏览器无异。致命缺点资源消耗巨大每个爬虫实例都是一个完整的浏览器进程占用大量内存和CPU。速度极慢需要等待页面加载、渲染、JS执行比直接发HTTP请求慢几个数量级。极不稳定容易被网站通过浏览器指纹识别为自动化工具而封禁。难以大规模部署同时运行几十个浏览器实例对硬件是巨大考验。适用场景作为最后的手段用于抓取那些反爬极其严格、数据价值极高、且请求量不大的页面。通常需要配合IP代理池和浏览器指纹伪装来使用。选择策略在实际项目中我们应该遵循一个从简到繁的决策链能逆向分析思路二就不用引擎执行思路三能引擎执行就不用无头浏览器思路四。思路一仅用于侦查。3. 实战演练逆向分析与Python复现以模拟场景为例理论说再多不如一行代码。我们以一个虚构但非常典型的场景为例来演示最核心的“思路二”的完整实战流程。假设我们要抓取一个网站的商品列表其请求URL为https://api.example.com/products需要携带一个名为sign的加密参数。3.1 第一步侦查与抓包使用Chrome浏览器打开目标网站按F12打开开发者工具切换到Network网络面板勾选Preserve log。然后进行触发列表加载的操作如点击搜索、翻页。很快我们在Network面板中发现了一个XHR/Fetch请求其地址正是https://api.example.com/products。点击该请求查看Headers和Payload。请求头 (Headers)可能包含User-Agent、Cookie、Content-Type等常规信息。请求负载 (Payload)通常是Form Data或Query String Parameters。我们发现除了page1、keyword手机等业务参数外还有一个关键的sign: a1b2c3d4e5f67890...。这个sign就是我们的目标。我们需要找到它是如何生成的。3.2 第二步逆向定位加密函数在Network面板中点击该请求查看Initiator标签页。这里显示了是哪个JS文件发起了这个请求。点击调用栈中的文件名可以直接跳转到Sources源代码面板的对应位置。我们跳转过去发现JS代码被压缩混淆了变量名都是a、b、c、t、e等。这很常见。此时有几种策略搜索关键词在Sources面板中按CtrlShiftF进行全局搜索搜索sign、encrypt、MD5、SHA等。XHR断点在Sources面板的XHR/fetch Breakpoints中添加一个包含sign的URL断点。当浏览器发起包含sign的请求时代码会自动暂停此时调用栈会清晰地指向生成sign的函数附近。Hook函数在Console控制台中注入代码劫持标准的XMLHttpRequest.send或fetch方法在请求发出前打印出参数从而定位加密位置。假设我们通过搜索sign在混淆的代码中找到了类似下面的一行这是经过我们“反混淆”理解后的伪代码实际混淆代码难看得多function generateSign(params) { var keyList []; for (var k in params) { if (params.hasOwnProperty(k) k ! sign) { keyList.push(k); } } keyList.sort(); var str ; for (var i 0; i keyList.length; i) { var k keyList[i]; str k params[k] ; } str str.slice(0, -1); // 去掉最后一个 str str secretMY_SECRET_KEY; // 加盐 return md5(str); // 假设使用MD5 }这段代码的逻辑很清晰遍历传入的参数对象params收集除sign本身外的所有键名。对键名进行字母排序这是常见做法确保顺序一致。将排序后的键值对按keyvalue格式拼接成字符串。在字符串末尾拼接一个固定的盐值secretMY_SECRET_KEY。对最终字符串计算MD5哈希值作为sign。同时我们可能发现params中还被自动添加了一个timestamp字段值是当前时间的13位毫秒时间戳。3.3 第三步Python代码复现现在我们完全理解了加密逻辑。接下来就是用Python实现它。首先安装必要的库requests用于发送请求hashlib用于MD5计算。pip install requests然后编写我们的爬虫代码import requests import time import hashlib from urllib.parse import urlencode def generate_sign(params, secret_keyMY_SECRET_KEY): 根据逆向分析的逻辑生成 sign 参数 :param params: 字典所有的请求参数不包含sign本身 :param secret_key: 从JS中分析出的盐值密钥 :return: 计算得到的sign字符串 # 1. 移除可能存在的sign项并过滤掉值为None的项 filtered_params {k: v for k, v in params.items() if v is not None and k ! sign} # 2. 对参数键进行排序 sorted_keys sorted(filtered_params.keys()) # 3. 按 keyvalue 格式拼接 sign_str_parts [] for key in sorted_keys: # 确保值为字符串这是为了与JS的字符串拼接行为一致 sign_str_parts.append(f{key}{filtered_params[key]}) # 4. 用 连接各部分并拼接盐值 sign_str .join(sign_str_parts) sign_str fsecret{secret_key} # 5. 计算MD532位小写 m hashlib.md5() m.update(sign_str.encode(utf-8)) return m.hexdigest() def main(): # 目标API地址 url https://api.example.com/products # 构造请求参数 params { page: 1, pageSize: 20, keyword: 手机, timestamp: int(time.time() * 1000), # 13位毫秒时间戳 # 注意sign 先不填由函数生成 } # 生成sign secret_key MY_SECRET_KEY # 此处密钥需要从JS代码中准确提取 sign_value generate_sign(params, secret_key) # 将sign添加到请求参数中 params[sign] sign_value # 设置请求头模拟浏览器 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, Referer: https://www.example.com/, # 根据实际情况填写 Content-Type: application/x-www-form-urlencoded } # 发送请求 # 注意根据API要求参数可能放在查询字符串GET或请求体POST中 response requests.get(url, paramsparams, headersheaders) # 如果是POST通常这样写 response requests.post(url, dataparams, headersheaders) if response.status_code 200: data response.json() print(请求成功获取数据条数, len(data.get(list, []))) # 处理数据... else: print(f请求失败状态码{response.status_code}) print(response.text) if __name__ __main__: main()代码要点解析参数过滤与排序generate_sign函数严格复现了JS逻辑过滤非sign参数、按键名排序、拼接字符串。特别注意JS中对象的遍历顺序在某些情况下可能不可靠但排序是通用做法。字符串编码hashlib.md5()需要输入字节串所以我们用.encode(utf-8)将拼接好的Unicode字符串转为UTF-8字节串。编码必须与前端完全一致否则MD5结果不同。时间戳int(time.time() * 1000)用于生成13位毫秒时间戳这是Web前端Date.now()的常见等价形式。请求头添加合理的User-Agent和Referer能降低被直接识别为爬虫的风险。参数位置明确API是使用GET参数在URL中还是POST参数在请求体中使用requests对应的方法。实操心得在拼接参数字符串时要特别注意空值null/undefined、布尔值true/false在JS和Python中字符串表示的差异。一个稳妥的做法是在Python中先将所有参数值通过str()转换为字符串并处理None为空字符串以完全模拟JS的隐式类型转换。此外URL编码问题也需留意requests的params参数会自动进行URL编码但如果加密逻辑是在编码前进行的你就需要手动控制编码顺序。4. 进阶挑战与应对策略当加密变得复杂上面的例子是一个标准的“参数排序MD5加盐”模式相对简单。现实中你会遇到更复杂的挑战。4.1 应对高强度代码混淆网站可能使用Webpack、Vue、React等框架打包并经过UglifyJS、Terser等工具混淆变量和函数名被替换逻辑被分割和重组。应对策略使用Source Map如果网站部署了.map文件在Sources面板中可以直接看到还原后的源代码。检查Network加载的JS文件看是否有.map文件。美化代码Sources面板左下角有{}Pretty-print按钮可以格式化混淆的代码使其具备可读的缩进和换行。动态调试在疑似加密函数入口处打上断点单步执行(F11)观察每一步的变量值变化。这是理解混淆后逻辑的最有效方法。搜索特征常量加密算法中常包含一些固定常量如MD5的初始化向量、AES的S盒。在混淆的代码中搜索这些十六进制常数可能快速定位到加密函数。4.2 应对非对称加密RSA有些网站使用RSA公钥对参数进行加密。你会在JS中找到一段公钥通常是PEM格式的字符串以-----BEGIN PUBLIC KEY-----开头。Python实现要点使用rsa或Crypto.PublicKey.RSA库。加载从JS中提取的公钥字符串。使用公钥对特定字符串如时间戳拼接随机数进行加密。RSA加密结果通常是二进制需要做Base64编码后传输。import base64 from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 def rsa_encrypt(data, public_key_str): 使用RSA公钥加密数据 # 加载公钥 public_key RSA.import_key(public_key_str) cipher PKCS1_v1_5.new(public_key) # 加密并base64编码 encrypted_bytes cipher.encrypt(data.encode(utf-8)) return base64.b64encode(encrypted_bytes).decode(utf-8)4.3 应对动态密钥与算法更高级的反爬会动态变化加密密钥甚至算法。密钥可能来自另一个API接口的响应算法标识可能藏在Cookie或HTML的某个script标签里。应对策略完整模拟登录/会话流程先请求首页或登录页从响应中提取密钥或算法标识存入会话。建立状态管理你的爬虫需要维护一个“会话”(Session)自动处理Cookie并能够根据之前请求的结果来更新本次请求的加密逻辑。定期更新如果密钥有过期时间需要实现定时刷新密钥的逻辑。5. 工具链与调试技巧提升逆向效率工欲善其事必先利其器。一套顺手的工具能极大提升逆向JS的效率。5.1 浏览器开发者工具进阶用法Overrides本地覆盖在Sources面板的Overrides标签中可以将网站的JS文件映射到本地文件夹。你可以在本地修改JS代码例如插入debugger;语句或console.log刷新页面后浏览器会加载你修改后的版本方便调试。Console实用命令copy(...) 将对象复制到剪贴板。console.table(array) 以表格形式打印数组或对象查看参数结构非常清晰。monitor(function) 当指定函数被调用时在控制台输出其参数。XHR/Fetch Breakpoints如前所述这是定位发起网络请求的代码的神器。5.2 第三方逆向辅助工具Fiddler/Charles网络抓包工具可以拦截和修改HTTPS请求/响应用于分析API调用链和修改返回数据辅助调试。AST解析工具对于极度混淆的代码可以尝试使用Babel、Esprima等JavaScript解析器生成抽象语法树(AST)然后通过编写脚本对AST进行反混淆或分析但这属于高阶技能。Node.js环境将关键的加密函数代码提取出来在Node.js环境中单独运行和测试比在浏览器控制台调试更高效。5.3 Python调试与比对详细日志在Python的generate_sign函数中打印出每一步生成的中间字符串与在浏览器控制台中调试得到的中间结果进行逐字比对这是排查加密不一致问题的最直接方法。单元测试为你的加密函数编写单元测试。输入固定的参数确保输出的加密结果与浏览器在相同输入下产生的结果完全一致。一个字符、一个大小写的差异都会导致失败。6. 常见问题排查与实战避坑指南即使逻辑清晰在复现过程中也极易踩坑。下面是我总结的常见问题清单和排查思路。6.1 问题生成的sign与浏览器不一致这是最普遍的问题。请按以下顺序排查参数范围是否一致检查浏览器发送的请求中到底包含了哪些参数你的Python代码是否包含了所有参数特别注意隐藏的input表单值、全局变量、或者由其他JS函数自动添加的参数如_t,nonce。方法在JS加密函数入口打上断点查看传入的params对象完整内容。与你的Python代码接收的参数字典进行对比。参数顺序是否一致检查JS中的排序规则是什么是Object.keys()后的自然顺序还是用了自定义排序Python的sorted()默认是字母顺序升序是否与JS一致方法在JS中在排序后和拼接前用console.log(JSON.stringify(sortedKeys))和console.log(signStr)打印出关键中间变量。在Python中也做同样打印进行逐行比对。参数值的类型与格式是否一致检查JS中数字123和字符串123拼接结果不同。时间戳是字符串还是数字布尔值true是作为字符串true还是数字1处理方法在JS和Python中确保拼接前所有值都转换为完全相同的字符串表示形式。对于时间戳要确认是10位秒级还是13位毫秒级。拼接格式是否一致检查键值对之间是用连接还是用空字符key和value之间是用还是:末尾是否有多余的连接符方法仔细比对JS生成的原始待加密字符串和Python生成的字符串。可以使用在线MD5工具分别计算两者的MD5看是否一致从而快速定位是加密前字符串的问题还是加密算法的问题。加密算法与编码是否一致检查真的是MD5吗还是SHA-1、SHA-256MD5结果是16进制32位小写还是大写还是Base64编码后的结果方法在JS中将最终待加密的字符串复制出来。在Python中用hashlib.md5(‘完全相同的字符串’.encode(‘utf-8’)).hexdigest()计算看结果是否与JS函数输出一致。如果不一致尝试.upper()转为大写或尝试其他哈希算法。6.2 问题请求成功但返回数据为空或错误sign校验通过了但拿不到数据。检查其他请求头除了sign服务器可能还验证User-Agent、Referer、Origin、特定的X-Requested-With头或Cookie。确保你的Python请求头与浏览器发出的尽可能一致。检查Cookie和会话如果网站需要登录那么sign的生成可能依赖于登录后的sessionId或token这些信息通常保存在Cookie中。你需要先用Python模拟登录获取并维护一个有效的会话(requests.Session)。检查请求方法GET/POST和参数位置确认API是GET参数在URL查询字符串还是POST参数在请求体。如果是POST请求体格式是application/x-www-form-urlencoded还是application/json这必须与浏览器完全一致。频率限制即使签名正确过快的请求频率也会触发服务器的风控返回空数据或错误码。需要在请求间添加随机延时(time.sleep(random.uniform(1, 3)))。6.3 避坑技巧实录从简单到复杂不要一开始就处理最复杂的页面。先尝试抓取不需要登录、参数最少的简单接口验证你的逆向和复现流程是通的。固化测试用例在浏览器中抓取一组固定的请求参数和对应的sign。用这组参数作为你Python代码的单元测试确保每次修改代码后计算结果都与这组“黄金样本”一致。善用“重放”功能在开发者工具Network面板右键点击请求选择“Copy as cURL”然后粘贴到命令行执行。如果能成功说明问题出在你的Python代码模拟上如果不成功说明这个请求本身已过期或依赖特定上下文你需要重新分析这个上下文是什么。留意WebSocket和SSE有些现代网站的数据流通过WebSocket或Server-Sent Events传输而不是普通的XHR/Fetch。你需要使用websocket客户端库来连接和处理。保持耐心与敬畏JS逆向是一个需要耐心和细心的活。一个字符的差错就会导致失败。同时要尊重网站的防护措施合理控制抓取速度。破解JS加密参数是爬虫工程师的必修课也是一场与网站开发者的智力博弈。它没有一成不变的银弹需要你综合运用网络知识、编程能力和调试技巧。从清晰的逆向分析入手逐步构建起可靠的参数生成逻辑是最高效、最稳定的方法。当这条路走不通时再考虑引擎执行或无头浏览器这类“重武器”。希望本文梳理的思路和提供的代码实例能为你下次面对加密参数时提供一份清晰的作战地图和趁手的工具。记住每一次成功的逆向不仅是为了拿到数据更是对自身技术能力的一次扎实提升。