
简介这份资源是面向爬虫逆向与接口分析学习者的维普期刊高级检索接口逆向工程实践包适合具备一定Python与HTTP基础、希望理解瑞数防护机制与接口调用逻辑的开发者。包内共18个文件以11个Python脚本为核心配合HTML页面样本、JS注入脚本、CRX浏览器插件、JSON配置与Markdown说明文档整体约76KB结构紧凑便于按模块拆解研究。内容围绕请求方式、参数构造、页面加载与请求注入等环节展开可帮助读者梳理接口逆向的完整链路理解从抓包分析到脚本复现的排错思路。目前已有493人学习下载适合作为接口逆向与数据检索方向的参考案例用于优化检索策略、提升对复杂接口的调试与复现能力。1. 维普高级检索接口为什么值得单独拆一遍维普期刊的高级检索页表面看是个普通表单实际点下「检索」之后浏览器发出的请求里塞了一串动态生成的参数其中最关键的就是rs开头的几个字段。这套机制来自瑞数信息的动态防护方案业内常说的「瑞数6」或「瑞数6代补环境」指的就是它较新一版的 JS 虚拟机加环境校验逻辑。rs_weipu_reverse这个命名本质上是把维普高级检索接口的逆向过程单独拎出来做一份可复现的记录。它解决的问题很具体你想批量拿到维普的检索结果无论是做文献计量、主题聚类还是给内部知识库做数据补充手动翻页都不现实。而直接拿 requests 去怼接口返回的永远是那一段混淆过的 JS 或者一个空壳。适合谁看做数据采集的、做学术工具二次开发的、以及想搞明白瑞数这套东西到底怎么落地的人。这篇不聊虚的从接口长什么样到参数怎么补到翻车点在哪一步步来。2. 维普高级检索接口的请求结构与瑞数参数定位2.1 抓包先看什么URL、Method 与核心 Header打开维普高级检索页F12 切到 Network勾选 Preserve log然后在检索框里输入关键词点检索。你会看到两类请求一类是页面本身的资源加载另一类是 XHR/Fetch。真正返回检索结果的那条通常 URL 里带search或result字样Method 是 POSTContent-Type 是application/x-www-form-urlencoded或application/json。重点看 Request Headers 里的几个字段。除了常规的User-Agent、Referer、Cookie瑞数会在里面塞一个或多个以rs开头的字段常见的有rs、rsid、rst之类。这些值不是固定的每次刷新页面都会变。你如果直接把某一次抓到的值复制到脚本里第一次可能通第二次就 403 或者返回一段 JS。提示抓包时把Fetch/XHR过滤打开别在 All 里大海捞针。另外维普的检索结果接口有时候会走https://qikan.cqvip.com/Qikan/Search/Index这类路径具体以你实际抓到的为准。2.2 瑞数参数是怎么生成的从 Cookie 到动态字段瑞数的核心逻辑分两层。第一层是首次访问时服务器返回一段混淆 JS这段 JS 在浏览器里执行后会往 Cookie 里写一个类似rsid的值同时生成一个本地存储的标识。第二层是后续每次请求前端 JS 会基于这个标识、当前时间戳、请求参数等算出一组动态字段附加到 Header 或 URL 上。这套东西之所以难搞是因为它用了自定义的 JS 虚拟机。你看到的代码不是普通的eval或者Function而是一堆while循环加switch嵌套变量名全是_0x开头。直接静态分析几乎不可能常见做法是补环境用 Node.js 模拟浏览器环境把window、document、navigator、location这些对象补齐让那段 JS 在 Node 里跑起来然后 hook 住它生成参数的关键函数把结果导出来。补环境的关键在于「像」。瑞数会检测navigator.userAgent、navigator.plugins、window.screen、document.cookie的可写性甚至检测Function.prototype.toString是否被改写。你补得越真它越不触发风控。业内说的「瑞数6代补环境」指的就是针对第 6 代版本需要额外补window.chrome、performance、WebGL相关的一些属性。2.3 用 Playwright 过瑞数的最小验证路径如果你不想一上来就死磕补环境Playwright 是一条更稳的路。它直接驱动真实浏览器内核瑞数的环境检测天然通过。你只需要在页面加载完成后等检索结果出来再把接口返回的数据截下来。from playwright.sync_api import sync_playwright import json def fetch_weipu(keyword): with sync_playwright() as p: # headlessFalse 方便观察调试完再改 True browser p.chromium.launch(headlessFalse) context browser.new_context( user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ) page context.new_page() # 拦截检索结果接口具体 URL 以实际抓包为准 results [] def handle_response(response): if Search/Index in response.url and response.status 200: try: results.append(response.json()) except Exception: pass page.on(response, handle_response) page.goto(https://qikan.cqvip.com/Qikan/Search/Index?keytest) # 等待检索框出现并输入关键词 page.wait_for_selector(input[typetext], timeout15000) page.fill(input[typetext], keyword) page.click(button:has-text(检索)) page.wait_for_timeout(5000) # 给接口留出返回时间 browser.close() return results if __name__ __main__: data fetch_weipu(机器学习) print(json.dumps(data, ensure_asciiFalse, indent2))这段代码的逻辑很直白启动 Chromium设置一个正常的 UA监听所有响应把 URL 里带Search/Index且状态码 200 的 JSON 存下来。page.fill和page.click模拟人工操作wait_for_timeout是给瑞数 JS 执行和接口返回留时间。参数上headlessFalse在调试阶段必开否则你看不到页面到底卡在哪一步user_agent别用默认的 HeadlessChrome那等于自报家门。注意Playwright 方案适合中小规模采集单机并发别超过 35 个 context否则容易触发 IP 层面的频率限制。真要上量得配合代理池和请求间隔随机化。3. 补环境方案在 Node.js 里把瑞数 JS 跑起来3.1 补环境的基本框架与必备对象补环境不是把浏览器所有 API 都实现一遍而是按需补。瑞数 JS 执行时会访问哪些对象你就补哪些。常见的有window、document、navigator、location、screen、history、localStorage、sessionStorage、XMLHttpRequest、fetch。其中document.cookie的 getter/setter 必须实现因为瑞数会往里写值再读出来校验。一个最小可跑的框架长这样// env.js const vm require(vm); const fs require(fs); // 用 Proxy 拦截未定义的属性避免直接报错 function createProxy(target, name) { return new Proxy(target, { get(obj, prop) { if (prop in obj) return obj[prop]; // 返回一个函数或空对象让 JS 继续跑 return typeof prop symbol ? undefined : function() {}; }, set(obj, prop, value) { obj[prop] value; return true; } }); } const navigator createProxy({ userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, platform: Win32, language: zh-CN, languages: [zh-CN, zh, en], plugins: [1, 2, 3, 4, 5], // 长度要像真的 mimeTypes: [1, 2, 3, 4], webdriver: false }, navigator); const document createProxy({ cookie: , referrer: https://qikan.cqvip.com/, title: 维普期刊, createElement: function(tag) { return createProxy({ tagName: tag.toUpperCase() }, tag); }, getElementsByTagName: function() { return []; }, getElementById: function() { return null; }, addEventListener: function() {}, documentElement: createProxy({}, documentElement) }, document); const location createProxy({ href: https://qikan.cqvip.com/Qikan/Search/Index, protocol: https:, host: qikan.cqvip.com, hostname: qikan.cqvip.com, pathname: /Qikan/Search/Index, search: , hash: }, location); const window createProxy({ navigator, document, location, screen: createProxy({ width: 1920, height: 1080, availWidth: 1920, availHeight: 1040 }, screen), localStorage: createProxy({}, localStorage), sessionStorage: createProxy({}, sessionStorage), setTimeout: setTimeout, setInterval: setInterval, clearTimeout: clearTimeout, clearInterval: clearInterval, btoa: (s) Buffer.from(s, binary).toString(base64), atob: (s) Buffer.from(s, base64).toString(binary), chrome: createProxy({ runtime: {} }, chrome), performance: createProxy({ now: () Date.now() }, performance) }, window); window.window window; window.self window; window.top window; window.globalThis window; const sandbox { window, document, navigator, location, console }; vm.createContext(sandbox); // 加载瑞数 JS这里假设你已经把那段混淆代码保存为 rs.js const rsCode fs.readFileSync(./rs.js, utf-8); vm.runInContext(rsCode, sandbox); // 执行完后从 document.cookie 或 window 上取生成的参数 console.log(cookie:, sandbox.document.cookie);这段代码的核心是Proxy。瑞数 JS 里会大量访问navigator上不存在的属性或者调用document.createElement后访问返回对象的属性。用 Proxy 兜底可以让 JS 不因为undefined而中断。navigator.plugins给一个长度为 5 的数组是因为真实 Chrome 里插件数量通常不为 0给空数组反而可疑。webdriver: false是必须的Playwright 和 Selenium 默认会把它设成 true。3.2 Hook 关键函数把 rs 参数导出来补环境只是让 JS 能跑真正要拿到参数得 hook 住它生成参数的那一步。常见做法是监控document.cookie的 setter或者 hookXMLHttpRequest.prototype.setRequestHeader。// 在 env.js 的 sandbox 创建之后加载 rs.js 之前插入 const originalSetHeader sandbox.window.XMLHttpRequest ? sandbox.window.XMLHttpRequest.prototype.setRequestHeader : null; // 如果 rs.js 里自己实现了 XHR就在它执行完后重新 hook vm.runInContext(rsCode, sandbox); // 方案一监控 cookie 变化 let cookieValue ; Object.defineProperty(sandbox.document, cookie, { get() { return cookieValue; }, set(val) { console.log([Cookie Set], val); cookieValue val; } }); // 方案二如果 rs.js 把参数挂到了 window 上直接遍历找 for (let key in sandbox.window) { if (key.startsWith(rs) || key.includes(token)) { console.log(found:, key, sandbox.window[key]); } }逻辑说明瑞数生成参数后要么写 Cookie要么通过 XHR 的 Header 发出去。Hookdocument.cookie的 setter 是最稳的因为不管它后面怎么用写 Cookie 这一步跑不掉。参数说明Object.defineProperty的get和set必须都实现只实现 set 会导致 JS 读 Cookie 时拿到undefined触发异常。如果 rs.js 里用了Object.defineProperty自己重定义 cookie你的 hook 会被覆盖这时候要在它执行完之后再重新定义一次。3.3 补环境与 Playwright 的取舍什么时候用哪个补环境方案的优势是快、省资源一个 Node 进程能跑很多次适合高频调用。缺点是维护成本高瑞数一更新补的环境可能就失效了得重新跟。Playwright 方案的优势是稳只要浏览器能跑它就能跑缺点是慢、吃内存不适合大规模并发。我一般的做法是先用 Playwright 把整个流程跑通确认接口结构和参数位置然后再用补环境方案去替换把性能提上来。如果只是偶尔跑一次Playwright 足够了。如果要做成服务每天跑几万次那必须上补环境。对比项补环境Node.jsPlaywright单次耗时50200ms25s内存占用低高维护成本高需跟进版本低适用场景高频、批量调试、低频风控触发概率中取决于环境补得真不真低真实浏览器4. 避坑与排查维普瑞数逆向里最容易翻车的 5 个点4.1 现象第一次请求成功第二次返回一段 JS原因瑞数的rs参数是一次性的或者跟当前会话绑定。你复用了上一次的值服务器校验不通过就返回 JS 让你重新执行。解决每次请求前重新生成参数或者维护一个会话池每个会话独立生成。别想着一个参数用到底。4.2 现象补环境时 JS 报TypeError: Cannot read property xxx of undefined原因某个对象没补全瑞数 JS 访问了它的属性。常见的是window.chrome、navigator.plugins的某一项、document.documentElement的style。解决在 Proxy 的 get 里打日志看它到底访问了什么。或者用try/catch包住vm.runInContext把错误堆栈打出来定位到具体行。4.3 现象Playwright 跑着跑着页面卡住检索按钮点不动原因瑞数检测到自动化特征故意让页面假死。常见触发点是navigator.webdriver为 true或者鼠标轨迹太机械。解决启动时加--disable-blink-featuresAutomationControlled并且在context里覆盖navigator.webdriver。点击前加随机延迟别用固定的wait_for_timeout。context browser.new_context( user_agent..., viewport{width: 1920, height: 1080} ) context.add_init_script( Object.defineProperty(navigator, webdriver, { get: () false }); )4.4 现象返回的数据是空的但状态码是 200原因维普的接口有时候会返回一个空数组但实际数据在另一个接口里。或者你的关键词触发了敏感词过滤被静默拦截。解决把 Network 里所有 XHR 都看一遍别只盯一个。另外换一个普通关键词测试比如「计算机」排除关键词本身的问题。4.5 现象补环境跑一段时间后CPU 飙到 100%原因瑞数 JS 里有死循环或者定时器在 Node 里跑的时候没被正确清理。常见的是setInterval被反复调用。解决在 sandbox 里把setInterval替换成一个空函数或者限制调用次数。瑞数生成参数通常不需要真正的定时器它只是用这个来检测环境。let intervalCount 0; window.setInterval function(fn, delay) { if (intervalCount 10) return 0; // 超过 10 次直接忽略 return setInterval(fn, delay); };5. 进阶把 rs_weipu_reverse 做成可复用的采集模块5.1 参数缓存与失效重试补环境方案跑通之后别每次请求都重新执行一遍 JS。瑞数生成的rs参数通常有几分钟的有效期你可以把它缓存起来过期了再重新生成。用一个简单的字典存{session_id: {rs_param, expire_at}}请求前检查是否过期。import time import requests class WeipuSession: def __init__(self): self.rs_param None self.expire_at 0 def _refresh(self): # 这里调用你的补环境脚本或 Playwright 脚本 # 返回新的 rs 参数 self.rs_param generate_rs_param() self.expire_at time.time() 180 # 假设 3 分钟有效期 def request(self, url, data): if time.time() self.expire_at: self._refresh() headers { User-Agent: ..., rs: self.rs_param, Referer: https://qikan.cqvip.com/ } resp requests.post(url, datadata, headersheaders) if resp.status_code 403 or text/javascript in resp.headers.get(Content-Type, ): # 参数失效强制刷新重试一次 self._refresh() headers[rs] self.rs_param resp requests.post(url, datadata, headersheaders) return resp逻辑说明_refresh负责生成新参数request里先检查过期时间过期就刷新。如果请求返回 403 或者 Content-Type 是 JS说明参数被拒强制刷新再试一次。参数说明expire_at设 180 秒是保守估计实际可以抓包看服务器返回的 Cookie 过期时间。重试只做一次避免死循环。5.2 验证采集结果是否完整跑完一批关键词后别急着入库。先做三件事一是检查返回条数是否和页面显示的总数一致二是随机抽几条去维普官网核对三是看有没有重复数据。我一般会写一个校验脚本把采集结果和页面上的「共 X 条」做对比。def validate(result_json, expected_total): items result_json.get(data, {}).get(list, []) if len(items) 0: print(警告返回空列表) return False if expected_total and len(items) expected_total * 0.9: print(f警告只拿到 {len(items)} 条预期 {expected_total} 条) return False # 检查关键字段是否缺失 for item in items[:5]: if not item.get(title) or not item.get(author): print(警告字段缺失, item) return False return True这个校验不复杂但能挡住大部分低级错误。比如参数没补对导致只返回第一页或者字段名变了导致解析失败。5.3 我踩过的最大一个坑说个血泪经验。有一次我补环境跑得好好的突然全部返回空。查了半天发现是瑞数更新了它在 JS 里加了一个对window.performance.timing的检测。我原来补的performance只有一个now方法没有timing对象导致 JS 执行到一半就跳出了参数没生成完整。后来我把performance.timing补上问题解决。这件事给我的教训是补环境不是一劳永逸的每次瑞数更新你都得重新跑一遍看它新加了什么检测点。我的习惯是在补环境脚本里加一个日志开关把 JS 访问过的所有未定义属性都记下来定期 review 一遍。这样下次它再加新检测你能第一时间发现。希望帮到你。本文还有配套的精品资源点击获取