影刀RPA进阶教程JS逆向获取加密参数与API直接调用技术作者林焱 | 标签#RPA #JS逆向 #API调用 #加密破解 #影刀进阶痛点切入去年给一家电商代运营公司做竞品监测系统老板的要求很简单每天采集某平台的商品详情页数据包括价格、库存、评价数。按常规思路用影刀RPA逐个打开商品页面→等待渲染→提取数据即可。试运行发现一个致命问题采集200个商品需要40分钟。页面加载占了大半时间每个页面还有很多没用的图片、广告、推荐模块在消耗流量和时间。分析Network面板后发现商品数据其实是通过一个API接口返回的JSONURL类似/api/product/detail?idxxxsignyyytszzz如果能直接调这个API跳过浏览器渲染200个商品的采集时间可以从40分钟压缩到30秒。但问题来了——那个sign参数是前端JS动态计算出来的每次请求都不一样。这就是本文要讲的核心技术用影刀RPA执行页面的加密JS拿到加密参数然后用Python直接构造HTTP请求调用API。不需要逆向出加密算法那是纯逆向工程师的事RPA使用者走执行→截获→复用的务实路径。核心方法整体流程分三步第一步定位加密逻辑在目标网页的开发者工具中搜索sign关键字找到生成签名的JavaScript函数。通常这个函数名类似getSign()、calcSign()、encryptParam()位于某个被打包混淆的JS文件中。第二步通过RPA提取加密参数用影刀RPA打开目标网页→等待页面完全加载→执行JavaScript调用加密函数→将返回的加密参数存储到影刀变量中。第三步Python构造请求拿到加密参数后影刀将其传给Python脚本由Python用requests库直接发送HTTP请求速度远快于浏览器渲染。代码实现场景实战某电商平台商品详情API调用背景该平台的商品详情API需要token、timestamp、sign三个参数。sign md5(token timestamp secret_key)其中secret_key写在混淆后的JS文件中每次页面加载后可通过window._appConfig.secretKey获取。第一步在影刀中提取加密所需数据在影刀RPA流程中插入「执行JavaScript」指令// // 影刀「执行JavaScript」指令 -- 提取加密参数// 放在页面加载完成后执行// varresult{token:window._appConfig.token,// 请求tokensecretKey:window._appConfig.secretKey,// 签名密钥每天都变appVersion:window._appConfig.version,// 应用版本号// 有些网站的加密函数是全局方法直接调用即可// sign: window.getRequestSign(token, timestamp)};// 将结果返回给影刀存储到变量中returnJSON.stringify(result);第二步Python直接调用API# # 影刀RPA「执行Python脚本」指令 —— API直接调用## 前置条件# 影刀变量 encrypt_params 中已存储上一步提取的参数JSON字符串# 影刀变量 product_ids 中存储待采集的商品ID列表# importjsonimporthashlibimporttimeimportrequestsfromtypingimportList,Dict,Optional# ----------------------------------------------------------# 第一步从影刀变量中读取加密参数# 在影刀中通过「设置变量」指令将 encrypt_params 传给Python# ----------------------------------------------------------# encrypt_params_str 影刀变量[encrypt_params] # 影刀原生写法# 以下是Python独立运行示例实际使用时由影刀传入encrypt_params_str{token:abc123,secretKey:sk_prod_2025,appVersion:3.2.0}paramsjson.loads(encrypt_params_str)TOKENparams[token]SECRET_KEYparams[secretKey]APP_VERSIONparams[appVersion]classProductAPIClient: 商品API客户端 绕过浏览器渲染直接调用后端API 速度200个商品 30秒对比RPA浏览器采集的40分钟 def__init__(self,token:str,secret_key:str,version:str):self.tokentoken self.secret_keysecret_key self.versionversion self.base_urlhttps://api.example-platform.com# 创建Session 复用TCP连接速度更快self.sessionrequests.Session()self.session.headers.update({User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36,Content-Type:application/json,X-App-Version:self.version,X-Request-Token:self.token,# 注意Referer必须设置部分后端会校验Referer:https://www.example-platform.com/,})def_calc_sign(self,timestamp:int,params_str:str)-str: 计算签名 签名规则需要从目标网站的JS中分析得出 sign md5(token str(timestamp) secret_key params_str) 实测踩坑 - timestamp必须是整数不能带毫秒 - params_str参数是某些接口特有的如图片搜索带keyword - secret_key每天凌晨重置需要在页面刷新时重新提取 raw_strf{self.token}{timestamp}{self.secret_key}{params_str}signhashlib.md5(raw_str.encode(utf-8)).hexdigest()returnsigndefget_product_detail(self,product_id:str)-Optional[Dict]: 获取单个商品详情 参数: product_id: 商品ID如 P20250601001 返回: 商品详情字典失败返回None 踩坑记录 1. 这个接口对频率有限制每秒最多20次请求 2. 超出限制返回 HTTP 429需要退避等待 3. timestamp 与服务器时间差不能超过60秒否则返回签名过期 timestampint(time.time())signself._calc_sign(timestamp)urlf{self.base_url}/api/product/detailpayload{productId:product_id,timestamp:timestamp,token:self.token,sign:sign}try:respself.session.post(url,jsonpayload,timeout10)ifresp.status_code429:# 被限流等待1秒后重试print(f[限流]{product_id}触发频率限制等待1秒...)time.sleep(1)returnself.get_product_detail(product_id)# 递归重试一次ifresp.status_code403:# 签名过期可能是系统时间不同步print(f[签名错误]{product_id}签名验证失败检查系统时间)returnNonedataresp.json()ifdata.get(code)0:returndata.get(data,{})else:print(f[业务错误]{product_id}:{data.get(message)})returnNoneexceptrequests.exceptions.Timeout:print(f[超时]{product_id}请求超时)returnNoneexceptExceptionase:print(f[异常]{product_id}:{e})returnNonedefbatch_get_products(self,product_ids:List[str],delay:float0.05)-List[Dict]: 批量采集商品详情 参数: product_ids: 商品ID列表 delay: 每次请求间隔秒默认50ms 返回: 采集结果列表 性能数据实测 - delay0.05s200条数据约12秒 - delay0.1s200条数据约22秒 - 建议设置在 0.05-0.1 之间避免触发风控 results[]totallen(product_ids)print(f开始批量采集共{total}条商品)fori,pidinenumerate(product_ids):# 进度显示if(i1)%500:print(f进度:{i1}/{total})detailself.get_product_detail(pid)ifdetail:results.append(detail)# 请求间隔time.sleep(delay)print(f采集完成成功{len(results)}/{total})returnresultsdefclose(self):关闭会话释放连接self.session.close()# # 在影刀中的调用流程## 流程步骤# 1. 「打开网页」→ 打开商品列表页# 2. 「等待元素」→ 等待页面加载完成# 3. 「执行JavaScript」→ 提取 token、secretKey上面第一步的JS# 4. 「执行Python脚本」→ 运行本脚本上面第二步的Python# 5. 结果返回影刀 → 存入变量 → 写入Excel/数据库# if__name____main__:# 创建客户端clientProductAPIClient(TOKEN,SECRET_KEY,APP_VERSION)# 测试单个商品print( 单个商品测试 )detailclient.get_product_detail(P20250601001)ifdetail:print(f商品名:{detail.get(name,N/A)})print(f价格:{detail.get(price,N/A)})print(f库存:{detail.get(stock,N/A)})# 批量采集示例print(\n 批量采集 )test_ids[P20250601001,P20250601002,P20250601003]productsclient.batch_get_products(test_ids,delay0.1)forpinproducts:print(f{p.get(productId)}:{p.get(name)}- ¥{p.get(price)})client.close()进阶技巧拦截XHR请求获取加密参数有时加密参数不在window全局对象上而是隐藏在每次XHR请求的请求头中。这时可以用以下方法拦截// // 拦截并记录XHR请求 —— 获取加密请求头// 在影刀「执行JavaScript」指令中使用// (function(){// 保存原始方法varoriginalOpenXMLHttpRequest.prototype.open;varoriginalSetRequestHeaderXMLHttpRequest.prototype.setRequestHeader;// 用于存储捕获的请求信息window._capturedRequests[];XMLHttpRequest.prototype.openfunction(method,url){this._captured{method:method,url:url,headers:{},timestamp:Date.now()};window._capturedRequests.push(this._captured);returnoriginalOpen.apply(this,arguments);};XMLHttpRequest.prototype.setRequestHeaderfunction(name,value){if(this._captured){this._captured.headers[name]value;}returnoriginalSetRequestHeader.apply(this,arguments);};console.log([RPA Hook] XHR拦截器已激活);})();// 等待10秒收集请求然后获取结果// setTimeout(function() {// console.log(JSON.stringify(window._capturedRequests, null, 2));// }, 10000);常见问题速查Q1: 提取的secretKey第二天就失效了现象昨天运行的脚本今天重新运行报签名错误。原因大部分网站的secret_key每天凌晨会重置或定期轮换不会永久有效。解决方式不要把 secret_key 硬编码在脚本里。每次运行流程时重新打开网页→执行JS提取最新密钥→再开始API调用。如果网站会在页面加载时下发secret_key用影刀的「打开网页」「执行JavaScript」组合每次动态获取。Q2: API返回的数据和网页上看到的不一致现象API拿到的价格是100网页上显示的是95活动价。原因API返回的是标准价格活动价格是前端另一套逻辑计算的如满减、优惠券叠加或者活动数据来自不同的接口。解决方式在浏览器中打开商品页面DevTools → Network → 过滤 XHR/Fetch → 找到所有相关的API请求。通常活动信息来自单独的/api/activity/discount或/api/coupon/available接口需要多调几个API拼接完整数据。Q3: 请求太多被IP封禁现象运行一段时间后所有请求返回403或超时。原因后端风控系统识别到高频请求来自同一IP「绕过浏览器」本身变成了一个风控特征正常用户不可能每秒请求10次。解决方式降低请求频率到每秒1-2次。使用代理IP池在每次请求时随机切换。模拟浏览器行为在批量请求之间穿插「正常浏览」行为——先用RPA打开几个页面正常浏览再继续API请求。在请求头中加入动态的X-Request-IdUUID模拟真实客户端的请求ID生成逻辑。Q4: 加密JS代码被混淆无法定位sign生成函数现象JS文件打开全是a,b,c变量名几千行混淆代码完全看不懂。原因网站使用了JS混淆器如UglifyJS/Terser高级混淆函数名和变量名都被替换成了无意义的短字符。解决方式RPA用户实用路径在Chrome DevTools → Sources面板对目标JS文件的关键行打断点。手动点击网页上的刷新按钮让断点触发。在断点处通过Call Stack向上追溯找到调用sign计算的上层函数。用console.trace()打印调用栈快速定位。不需要理解混淆后的代码逻辑只需找到函数名或入口点直接在影刀中用window.混淆后的函数名()调用即可。Q5: fetch请求怎么拦截XHR拦截器不生效现象用上面的XHR拦截脚本抓不到任何请求。原因现代网站逐渐用fetch()API替代XMLHttpRequestfetch基于Promise需要不同的拦截方式。解决方式使用以下fetch拦截脚本// Fetch API 拦截版本constoriginalFetchwindow.fetch;window.fetchfunction(url,options{}){constheaders{};if(options.headers){// headers 可能是 Headers 对象或普通对象if(options.headersinstanceofHeaders){options.headers.forEach((v,k)headers[k]v);}else{Object.assign(headers,options.headers);}}window._capturedFetchRequestswindow._capturedFetchRequests||[];window._capturedFetchRequests.push({url:typeofurlstring?url:url.url,method:options.method||GET,headers:headers,timestamp:Date.now()});returnoriginalFetch.call(this,url,options);};Q6: 拿到参数后API还是返回401未授权现象sign参数正确但API返回401。原因除了sign之外可能还有Cookie校验如 SessionID、csrf_token这些是浏览器自动携带的但Pythonrequests不会自动带。解决方式在影刀中先用「获取Cookie」指令提取当前页面的Cookie传给Python脚本在requests.Session中设置# 从影刀变量中拿到Cookie字符串cookie_strsession_idabc123; csrf_tokenxyz789foritemincookie_str.split(; ):key,valueitem.split(,1)session.cookies.set(key,value)推荐资源Chrome DevTools Network面板你的第一个逆向工具先读懂正常请求再考虑逆向。Postman用影刀提取参数后先在Postman中测试API是否可用确认无误再写Python脚本。MDN Web Docs - XHR/fetch了解浏览器网络请求的底层机制对拦截和理解参数传递有帮助。作者林焱标签#RPA #JS逆向 #API调用 #加密参数 #影刀进阶 #数据采集 #请求拦截