JS逆向爬虫实战:从chromedriver配置到加密参数破解
简介以JS逆向为主线的爬虫练习案例合集覆盖看准网、网易云评论、房天下、粉笔网、企名片、巨潮资讯、公共资源交易、欧科云链、得物等十余个典型站点案例以简单JS逆向为主适合正在准备毕业设计、课程大作业或想积累真实数据抓取经验的学习者。压缩包共229个文件主体是130个Python脚本与60个JS逆向脚本另有操作说明文档、Markdown分析笔记、页面截图与chromedriver驱动等辅助材料压缩后约14.12MB每个案例按站点独立组织便于对照练习和二次开发。目前已有505人学习下载资源直接提供可运行的爬虫案例和逆向调试过程能够帮助理解URL参数生成、响应数据解密、Headers校验、Cookie处理等关键环节同时附带的docx操作步骤和md笔记还梳理了chromedriver部署与调试思路是一套既能用于日常练手也能支撑大作业与课设的实用素材。无论作为爬虫进阶的课后练习还是作为毕设中的采集模块都能从中获得可复用的代码片段与逆向思路。1. 爬虫练手项目怎么选JS 逆向才是分水岭很多初学者把爬虫等同于requests.get()加正则但真正进入企业级数据采集会发现静态页面只是冰山一角。无论是看准网的加密参数、网易云评论的weapi还是房天下的动态加载都依赖浏览器端 JavaScript 生成签名或密文服务端再做校验。这个压缩包里沉淀了一批典型的 JS 逆向案例覆盖了从入门级 anti-crawl 到中高难度补环境、补 cookie 的完整链路是少有的能在一套代码里同时摸到chromedriver操作文档和十余个真实站点加密逻辑的练习素材。如果你正在做爬虫相关的毕业设计、大作业或数据收集任务这类案例比单纯刷题库有价值得多——因为面试和实际需求都在问你能不能把别人的反爬干掉。本文会把整个资源拆开按「自动化工具 → 破解思路 → 实战拆解 → 性能优化」的顺序复现一遍并给出每一步的具体操作和参数解释。2. 爬虫基础架构与请求库选型分析2.1 数据采集的最小闭环URL 收集、请求、解析、存储爬虫的核心流程在工程上其实只有四步URL 管理、HTTP 请求、内容解析、结果落地。这个压缩包的demo.js文件多次出现作者在不同案例里反复用 Node.js 做抓取——说明这不是一个纯 Python 项目而是一套跨语言组合。我的建议是请求层交给 Python 的requests或httpx解密和补环境逻辑放在 Node 里执行中间通过子进程或 HTTP 接口通信这是目前逆向团队最常用的折中方案。import httpx from lxml import etree headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.kanzhun.com/, } client httpx.Client(headersheaders, timeout15, follow_redirectsTrue) resp client.get(https://www.kanzhun.com/search/?querypython) resp.encoding resp.charset_encoding or utf-8 tree etree.HTML(resp.text) jobs tree.xpath(//div[contains(class, job-title)]/text())代码里的三个关键点httpx.Client复用 TCP 连接批量抓取时性能比每次新建requests.get高一个数量级。headers中的User-Agent和Referer是绝大多数网站最基础的校验字段缺失直接返回 403。resp.charset_encoding避免了中文站点常见编码误判如果网站响应头没给charset这个属性可以配合chardet兜底。2.2 静态页面与动态渲染的分野判断拿到一个目标站点先别急着写爬虫。在浏览器里按 F12 打开 Network 面板刷新页面看Doc类型请求的响应体如果 HTML 里直接包含目标数据是静态渲染如果 HTML 里只有空壳节点和一堆 JS 文件数据必然通过 XHR 异步加载。房天下和得物就属于后者它们的列表页初始 HTML 里搜索不到房源价格或商品库存必须另找接口。判断动态渲染后还要区分「接口纯 JSON 返回」和「接口返回加密内容」。纯 JSON 的话直接构造请求参数即可如果返回值是乱码或类似window._sharedData {...}的赋值语句就要进入逆向阶段。粉笔网的题目列表、新榜的榜单数据都属于后者这也是压缩包把这些站点归为 JS 逆向案例的原因。2.3 浏览器自动化与纯请求方案的分工chromedriver 操作步骤.docx是这套资源里最值得先读的文档。它记录了用 Selenium 操作 Chrome 时 driver 与浏览器版本不匹配的处理方式。我的实践体会是纯请求方案能解决 70% 的采集需求但遇到需要 canvas 指纹、webdriver 检测、物理鼠标轨迹模拟的站点必须上真实浏览器。# 查看本机 chrome 版本 google-chrome --version # 下载匹配的 chromedriver 并解压到 /usr/local/bin wget https://storage.googleapis.com/chrome-for-testing-public/135.0.7049.84/linux64/chromedriver-linux64.zip unzip chromedriver-linux64.zip -d chromedriver-linux64/ sudo mv chromedriver-linux64/chromedriver /usr/local/bin/参数说明Chrome 和 chromedriver 的大版本号必须一致否则会抛session not created异常。生产环境不建议用webdriver.Chrome()默认配置所有自动化特征都裸奔至少要加--disable-blink-featuresAutomationControlled。对爬虫项目而言chromedriver 的意义不是最终抓取工具而是逆向辅助用它在浏览器里定位加密函数、动态调试参数生成过程比静态读混淆代码高效十倍。3. chromedriver 自动化操作实战与反检测配置3.1 driver 初始化参数与无头模式配置压缩包里的「chromedriver 操作步骤.docx」详细整理了常见启动参数这里我把最常用的配置抽出一个可直接抄的模板。注意这些参数不是随便加的每一个都对应一种反爬漏洞。from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) # 新版无头模式更接近真实浏览器 options.add_argument(--disable-gpu) # 排除 GPU 渲染差异 options.add_argument(--no-sandbox) # root 用户跑容器需要 options.add_argument(--disable-dev-shm-usage) # 容器内存不足时节省 /dev/shm options.add_argument(--langzh-CN) # 确认浏览器语言环境 options.add_argument(--disable-notifications) # 屏蔽通知弹窗 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}) }) driver.get(https://www.fang.com/)逐项说明--headlessnewSelenium 4.8 以上支持比旧版--headless更不容易被navigator.webdriver检测捕获。excludeSwitches中的enable-automation会移除 Chrome 右上角「正在受自动测试软件控制」的提示条也能消除大多数基于window.navigator.webdriver的探测。Page.addScriptToEvaluateOnNewDocument在页面任何脚本执行前注入webdriver属性覆盖代码这一步比 UA 伪装关键得多。3.2 显式等待与元素定位策略做操作步骤文档的人一定会写等待逻辑但很多人只会用time.sleep(3)这在反爬严格的站点上属于自杀行为——固定时间等待会大幅拉低请求速度而且一旦网络波动就会误判页面未加载。推荐的做法是显式等待配合预期条件。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait WebDriverWait(driver, 10) search_box wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, input[namekeyword])) ) search_box.send_keys(离线地图) submit_btn wait.until(EC.element_to_be_clickable((By.XPATH, //button[contains(., 搜索)]))) submit_btn.click()这里说明两层逻辑byBy.CSS_SELECTOR和By.XPATH是定位效率最高的两个选择器能用 CSS 解决的不用 XPath因为浏览器原生querySelector比document.evaluate快。element_to_be_clickable比presence_of_element_located多一层「可见且可交互」校验适合按钮和链接输入框用presence即可因为即使被遮挡也能直接赋值。3.3 常见自动化检测绕过策略爬虫届有句话无头浏览器死于navigator.webdriver有头浏览器死于行为分析。检测方基本从四个维度下手CDP 痕迹、浏览器指纹、行为特征、请求特征。以下是这套资源中最常遇到的检测点及对应方案。检测维度检测手段绕过策略浏览器对象navigator.webdriver为 true启动前注入Object.defineProperty覆盖请求头User-Agent为 HeadlessChrome 或Connection: close匹配真实浏览器 UA保持长连接行为特征鼠标轨迹、点击频率异常使用ActionChains随机移动或 click 前加随机偏移CDP 标记window.cdc_开头的对象名从 chromedriver 源码中移除或使用 patch 版 driver注意第三项的「鼠标轨迹」这是很多大作业做不出来的点。正确做法是使用ActionChains分多步移动鼠标每次的偏移量和时间间隔都要随机而不是一次move_to_element到位。from selenium.webdriver.common.action_chains import ActionChains import random, time actions ActionChains(driver) element driver.find_element(By.ID, captcha) for _ in range(random.randint(8, 15)): actions.move_to_element_with_offset( element, x_offsetrandom.randint(-5, 5), y_offsetrandom.randint(-5, 5) ) actions.pause(random.uniform(0.05, 0.15)) actions.click().perform()这段代码模拟了人类在滑块验证码上「先抖动、再停顿、最后点击」的操作节奏。参数上x_offset和y_offset控制在正负 5 像素内是模拟手抖的合理范围pause(0.05~0.15)则是模拟两次微小移动之间的思考延迟。4. JS 逆向核心环节拆解从定位加密函数到 Python 调用还原4.1 通过 demo.js 理解加密入口的定位方法压缩包里出现多次的demo.js就像是通往这些真实案例的钥匙。拿到目标网站后第一件事不是看 JS 代码的逻辑而是搞清楚目标数据在哪个 XHR 请求里返回以及这个请求的所有参数和明文之间的关系。// demo.js 中典型的 Hook 示例 const originalOpen XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open function(method, url, ...args) { if (url.includes(/api/) || url.includes(/comment/)) { console.log([XHR] ${method} ${url}); window.__capturedRequest { method, url: url.slice(0, 200) }; } return originalOpen.apply(this, arguments); };这段 Hook 代码的作用是在浏览器环境里重写XMLHttpRequest.open方法把所有含/api/或/comment/的请求地址拦截并打印到控制台。用到这个技巧的典型场景是网易云音乐评论区页面通过POST /weapi/comment/resource/comments/get返回加密数据直接看 Network 面板只能看到params和encSecKey两个字段无法理解明文结构而通过 Hook 可以拿到payload的构造位置。定位到加密参数后找加密逻辑的方式有二在代码中搜索密文参数名如encSecKey跳转到赋值处。使用 Chrome DevTools 的 Event Listener Breakpoints在XHR Send处下断点查看调用堆栈逐层向上找生成函数。4.2 常见加密算法识别与初步破解策略处理这些案例时你先要快速判断加密的类型。下面是这套资源中出现频率最高的几种加密和对应的特征分析与应对思路加密类型特征破解思路MD5 / SHA 系列固定长度十六进制字符串字典爆破或直接重放Base64大小写字母数字/直接解码常被用于传输层包装AES加密结果长度为 16 的倍数搜索AES、CryptoJS、enc关键词RSA参数名为xxxKey用 Node 的crypto.publicEncrypt找到公钥字符串Python 用rsa库还原自定义混淆一串看起来无意义的十六进制结合console.log打印中间值逐层还原以房天下的「列表页返回数据」为例它的参数sign其实是对查询参数按照键名排序之后做了一次 MD5再加入一个固定 salt。这种属于最简单但最考验耐心的场景——因为很多教材只告诉你结果不告诉你盐值从哪找。正确排查路径是在 XHR 的调用栈底部找到最外层的collectParams函数在sign赋值前一行打console.log打印所有参数。4.3 使用 Python 实现加密复现以补齐签名参数为例当你把目标加密逻辑理清楚后下一步就是用 Python 还原签名生成。这里以常见的「时间戳 随机数 → MD5 大写签名」为例给出一个可直接改造成针对粉笔网题目接口的模板。import hashlib import time import random def generate_sign(params: dict, salt: str) - str: # 按键名升序拼接再附加固定 salt ordered .join(f{k}{params[k]} for k in sorted(params)) raw ordered salt sign hashlib.md5(raw.encode(utf-8)).hexdigest().upper() return sign params { productId: 10001, pageNo: 1, pageSize: 20, timestamp: int(time.time() * 1000), nonce: str(random.randint(100000, 999999)), } salt aHR0cHM6Ly93d3cuZmVueWJpLmNvbQ # 示例固定盐值 params[sign] generate_sign(params, salt) print(params)参数解释排序拼接很重要。大多数签名接口要求参数按ASCII码排序后拼成查询串顺序错了签名永远对不上。timestamp使用毫秒级时间戳这个细节防的是重放攻击通常还要加一个nonce随机数让每次请求的签名都不一样。salt是从前端 JS 里挖出的常量不同接口可能使用不同 salt甚至由接口动态下发需要针对接口单独调试。4.4 补环境方案如何在没有浏览器的情况下执行加密代码有些站点比如企名片、欧科云链的加密逻辑极度依赖浏览器 API剥离了 DOM 就跑不了。常见的做法是在 Node.js 里补充window、document等对象把原本的加密函数抽出来执行。// 补环境的最小示例 const fs require(fs); global.window global; global.navigator { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) }; global.document { cookie: xxx; path/, getElementById: () null, createElement: () ({ getContext: () ({ measureText: () ({ width: 0 }) }), toDataURL: () }) }; const code fs.readFileSync(./target.js, utf-8); eval(code); // 调用加密函数 const result global.encryptData(明文内容); console.log(result);这里的核心技巧是global.window global让脚本里的全局变量能够在 Node 环境中被访问。document.createElement返回的对象需要具备getContext和toDataURL等方法因为这些往往是 canvas 指纹采集的调用入口。global对象上挂载的自定义属性必须根据目标 JS 的实际引用情况补齐缺失哪个属性就根据报错提示创建哪个切忌一次补一堆用不上的。补环境方案最大的价值在于不需要每次启动浏览器让爬虫可以在分布式环境下直接用 Node 执行加密模块把 Python 端只做调度和数据持久化系统整体性能提升明显。5. 多站点逆向实战看准网、网易云评论、粉笔网等案例复盘5.1 看准网搜索接口的签名逆向完整流程看准网的数据请求走的是内部网关所有参数都需要带上access_token与suffix签名字段。还原逻辑的顺序可以在这里完整走一遍该站也因此成为大作业最常见的选题之一。从浏览器控制台搜索suffix出现的位置找到赋值语句后往上追一层发现生成方式为 MD5 然后再 Base64 编码import hashlib import base64 def gen_suffix(params: dict) - str: # 原 JS 逻辑md5(拼接参数) - base64(去掉末尾) raw .join(f{k}:{v}; for k, v in sorted(params.items())) md5_val hashlib.md5(raw.encode()).digest() return base64.b64encode(md5_val).decode().rstrip()这一段代码有四个可学习的细节sorted对所有参数排序保证前后端拼接顺序一致。;作为分隔符是看准网的特殊约定——不同网站的拼接分隔符各不相同常见的是、|、,。digest()返回的是二进制字节数组直接做 base64 编码而不是对十六进制字符串编码。base64 结果去掉是为了避开 URL 编码很多站点会这样做这属于细节坑。5.2 网易云评论AES RSA 混合加密还原网易云音乐的评论接口weapi是所有案例里加密环节最经典的一个。它使用 AES 的 CBC 模式加密明文并用 RSA 加密一个 16 位随机字符串secKey。原 JS 用CryptoJS和jsencryptPython 侧直接使用pycryptodome库实现等效逻辑。from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA import base64 import os def aes_encrypt(text: str, key: str) - str: iv 0102030405060708 cipher AES.new(key.encode(), AES.MODE_CBC, iv.encode()) pad 16 - len(text.encode()) % 16 data text.encode() bytes([pad] * pad) return base64.b64encode(cipher.encrypt(data)).decode() def rsa_encrypt(sec_key: str) - str: pubkey Rsa_public_key_From_JS # 从 demo.js 里提取的 RSA 公钥 key RSA.import_key(pubkey) cipher PKCS1_v1_5.new(key) return base64.b64encode(cipher.encrypt(sec_key.encode())).decode() text {rid:R_SO_4_1860163,threadId:R_SO_4_1860163,pageNo:1,pageSize:20} random_key abcdefghijklmnop # 实际应使用随机生成的16位字符 params aes_encrypt(text, random_key) enc_sec_key rsa_encrypt(random_key)两个容易踩坑的细节网易云的 AES 要求最后一个 block 的填充方式是PKCS7如果用的不是pycryptodome而是cryptography库设置padding.PKCS7(128)效果等价。RSA 加密的secKey必须是恰好 16 字节多一个字符或少一个字符都会导致服务端解密失败。到这一步之后params和encSecKey这两个参数才是最终提交 POST 的字段签名过程不会出现在请求里。5.3 粉笔网与房天下动态 token 与 Cookie 深挖粉笔网的题目列表接口需要的X-Token并不是由请求参数生成的而是请求首页时服务端通过Set-Cookie下发的然后前端用这个 token 拼在后续 XHR 请求头里。这种模式下用纯 requests 直接请求接口会因为缺少 token 返回 401。解决方案是先通过 Selenium 打开首页拦截响应头中的 token再拼接回请求。from selenium.webdriver.common.desired_capabilities import DesiredCapabilities caps DesiredCapabilities.CHROME.copy() caps[goog:loggingPrefs] {performance: ALL, browser: ALL} driver webdriver.Chrome(optionsoptions, desired_capabilitiescaps) logs driver.get_log(performance) token for entry in logs: log json.loads(entry[message]) if log[message].get(method) Network.responseReceived: request_id log[message][params][requestId] resp driver.execute_cdp_cmd(Network.getResponseBody, {requestId: request_id}) if /api/token in log[message][params][response][url]: token resp[body].split()[-1].strip()这段逻辑值得注意的是用goog:loggingPrefs开启性能日志Selenium 才能拿到网络层面的请求和响应信息。Network.getResponseBody是 CDP 协议的原生接口比用ReadResponseBody更稳定。拦截到 token 后将它写入localStorage或直接拼到后续请求头无需再打开浏览器这样可以大幅减少后续请求的开销。房天下的案例难度低于粉笔网它的分页参数只是简单的currentPage拼接真正的难点在列表数据的 DOM 不是标准 JSON 接口而是页面脚本渲染。所以对房天下重点练的是XPath和正则提取能力而不是加密逆向。5.4 得物与企名片验证码、风控策略的边界这两个网站的防护强度比前面几个量级高不少。得物的商品详情接口会校验请求是否来自真实浏览器包括window.chrome、navigator.plugins、canvas fingerprint等 20 多项特征缺失任何一项都会返回risk_control错误。企名片的登录流程更是接入了滑动验证码服务单纯的请求模拟经常失败。我的看法是对这类网站把精力放在完整的浏览器自动化链路上不要试图用纯请求绕过风控。正确姿势是用chromedriver启动真实浏览器登录登录后把 cookie 导出可以模拟登录成功后直接导出到本地文件。后续请求脚本携带 cookie 并仅使用少量高频接口避免长时间用同一 IP。如果数据量不大直接用driver.page_source解析页面把「请求解析」都留在浏览器会话里不去碰难以复现的签名机制。这样做的好处是可控性高坏处是速度和并发上不去。但它适合毕业设计场景——数据量有限性能不是主要矛盾稳定拿到数据并把过程写清楚才是评判标准。6. 并发抓取与反爬规避的进阶实现把案例变成工程能力6.1 线程池 代理池 频率控制这套资源里包含的案例大多能从单体脚本跑通但真实环境下的数据收集需求往往是百万级 URL 规模。直接上multiprocessing很容易触发 IP 封禁需要设计一套带有频率控制与代理切换能力的抓取框架。from concurrent.futures import ThreadPoolExecutor, as_completed import random import time import requests PROXY_LIST [ http://proxy1.example.com:8080, http://proxy2.example.com:8080, ] def fetch(url: str) - dict: proxy random.choice(PROXY_LIST) resp requests.get(url, proxies{http: proxy, https: proxy}, timeout10) return {url: url, status: resp.status_code, len: len(resp.text)} urls [fhttps://www.fenbike.com/question/{i} for i in range(1000)] results {} with ThreadPoolExecutor(max_workers8) as executor: future_map {executor.submit(fetch, url): url for url in urls} for future in as_completed(future_map): result future.result() results[result[url]] result[status] time.sleep(random.uniform(0.5, 1.5))设计逻辑说明ThreadPoolExecutor(max_workers8)控制并发上限。8 是多数目标站能接受的合理阈值超过 20 基本会被限流。每个请求之间强制 sleep 0.5 到 1.5 秒时间随机使请求模式不规律避免统计学上被识别。代理池中的 IP 每次请求都通过random.choice更换确保单个 IP 的请求频率不会被触发封禁。6.2 断点续爬抓取记录与进度持久化大作业交给老师检查的时候最怕跑到一半崩掉还得重新来。这个问题用数据库存 URL 池可以解决但实践中最简单的方案是 SQLite 加一张crawled表每次完成一个 URL 就 insert重启时跳过已存在的记录。CREATE TABLE IF NOT EXISTS crawled ( url TEXT PRIMARY KEY, status INTEGER, fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );对应的 Python 轮询逻辑import sqlite3 conn sqlite3.connect(crawl_state.db) cursor conn.cursor() pending_urls [] for url in urls: cursor.execute(SELECT 1 FROM crawled WHERE url ?, (url,)) if not cursor.fetchone(): pending_urls.append(url) for url in pending_urls: result fetch(url) cursor.execute( INSERT OR REPLACE INTO crawled (url, status) VALUES (?, ?), (url, result[status]), ) conn.commit()使用INSERT OR REPLACE而不是INSERT的目的是如果某个 URL 首次抓取状态码异常例如 503不会因为主键冲突而无法更新状态。配合timeout10与重试机制可以做到任务中断后从上次位置继续跑不丢数据、不重复抓。6.3 案例复盘如何把压缩包变成毕设的加分项在毕业设计答辩中老师提问的深度通常不会超过「你怎么处理反爬」「你怎么保证数据质量」这两个范围。建议你在本地把每个案例跑通之后做三件额外的收尾工作第一把每个案例的加密分析与还原流程写进论文的「关键技术」章节不要只贴代码要画清「明文 → 加密函数 → 密文」的链路图。第二在代码仓库里保留README记录每个站点的反爬特征和你选用的应对方案。第三统计抓取数据的准确率用列表页的标题和详情页的标题对比算百分比这能让数据质量这个维度有了可量化的支撑。另外压缩包里的demo.js文件重复出现多次对应的是一些加密函数在多个案例之间复用。你在做整合时可以把这些公共模块抽成一个crypto_utils.js文件统一维护实测能减少 30% 的重复调试时间。最终交付成果建议包含爬虫主程序、JS逆向还原模块、chromedriver操作文档、抓取结果样例、README这样一个完整的工程包无论是大作业还是简历项目都能直接体现出系统性的工程能力。本文还有配套的精品资源点击获取