1. 这不是“抓个包改个参数”就能搞定的活儿你有没有试过用Charles把App的请求全抓下来把Header里那个看似随机的X-Signature字段复制粘贴进Postman结果返回403或者好不容易找到一个疑似加密函数的Java方法用Frida一hook控制台刷出一堆undefined和null连入参都打不出来这不是你工具没装对也不是手机没root——这是典型的“表面能抓实际跑不通”的移动端反爬深水区。我去年帮一家做海外本地生活服务的团队做数据采集支持他们卡在同一个问题上整整三周Charles能看到所有请求但任何重放都失败Frida脚本能跑但关键加密逻辑始终无法定位。后来发现问题根本不在工具链而在于整个对抗链条被拆成了三层网络层混淆Charles可见但不可信、运行时逻辑隐藏Frida可挂但不可读、以及最关键的——上下文绑定校验签名依赖设备指纹、时间戳、内存状态三者联动。这篇内容就是围绕“Python移动端反爬”这个标题展开的实战复盘不讲原理图、不列API文档只说我在真实项目中怎么用Charles定位入口、用Frida穿透加固壳、用Python复现签名逻辑的每一步操作、每一个坑、每一次推翻重来的判断依据。适合已经会基础抓包、能写简单Frida脚本但总在“看到加密函数却调不通”“抓到参数却验签失败”环节卡住的中级开发者。如果你还在用“抓包→改参→重放”三板斧那这篇文章会帮你把这三板斧锻造成一把带温度传感器和压力反馈的智能扳手。2. Charles不是万能的但它是最准的“手术定位仪”很多人把Charles当流量记录仪其实它真正的价值是上下文锚点生成器。在移动端反爬场景下它的核心任务不是“抓到所有包”而是“精准锁定那个唯一触发校验失败的请求节点”。我见过太多人一上来就开全局SSL Proxy结果抓出上万条请求最后在/api/v2/user/profile和/api/v2/feed/list之间反复横跳却漏掉了真正关键的/api/v2/auth/token?step2——因为这个请求只在用户滑动首页第3次后才发出且带有一个64位base64编码的x-context-id头而这个ID正是后续所有签名计算的种子。2.1 关键配置必须关掉的三个默认项Charles默认开启的三个功能在反爬调试中全是干扰源Auto Save to Disk必须关闭。原因很简单——当你在Frida里动态修改某个变量时Charles会把修改前的原始请求也存下来导致你分不清哪条是“真实发出的”哪条是“被Hook拦截后重发的”。我建议只在确认某次操作必然触发目标请求时手动点击Save Session并立刻重命名成session_step3_auth_token_pre_hook.chls这种带明确上下文的文件名。Sequence Numbers必须关闭。移动端App经常使用HTTP/2多路复用同一个TCP连接里混着几十个stream。Charles默认给每个请求加序号看起来整齐但会掩盖真实的请求时序关系。比如/auth/token和/feed/list可能在同一个connection_id里交替出现而序号会把它们强行拉成线性队列误导你判断依赖关系。关掉后直接按时间戳排序配合右键菜单里的Show Connection ID才能看清真实链路。Throttling必须关闭。很多App的反爬逻辑内置了“请求间隔检测”比如要求两次/feed/list之间必须大于800ms。Charles默认的限速会人为制造延迟导致你误判为“服务器限流”其实只是客户端自己在做节流。实测中我们曾因开着200kbps限速连续三天以为是服务端做了IP封禁最后发现是App在OkHttpClient.Builder.connectTimeout(800, TimeUnit.MILLISECONDS)里埋了伏笔。提示打开Proxy → SSL Proxying Settings勾选Enable SSL Proxying并在Locations里只添加目标域名如*.example.com绝对不要用通配符*.*。某次我误加了*.*结果Charles开始解密微信支付SDK的TLS流量触发了微信的证书固定Certificate Pinning检测整个App闪退三次最后靠重装App才恢复。2.2 真正有用的四个过滤与标记技巧光关默认项不够还得主动构建分析视角Filter by Request Method Path Regex在Structure视图右上角搜索框输入method:POST path:/api/v2/.*token.*。注意这里用的是Charles原生的正则语法不支持\d这种PCRE写法必须写成[0-9]。这个过滤能瞬间从几百个POST请求里筛出所有token相关接口比手动翻页快十倍。Breakpoints on Response Status右键某个疑似接口→Breakpoint在弹窗里勾选Break on response并设置Status code is 403。这样每次验签失败Charles会自动暂停你可以直接在Response标签页里看原始错误体——很多App在这里会返回明文提示比如{code:403,msg:Invalid signature: timestamp expired}这就是最直接的突破口。Custom Column for x-signature Length右键列标题→Configure Columns→Add Column→选择Request Header→输入X-Signature。这个自定义列会显示每个请求的签名长度。我们发现目标App的签名长度只有两种64SHA256和128双SHA256拼接而失败请求全是128位。这说明验签逻辑里存在分支判断为后续Frida Hook提供了明确目标。Export as HAR with Full Body右键选中一组关键请求→Export Sessions...→选择HAR with full request/response bodies。这个HAR文件不是用来重放的重放必失败而是导入到Python里做特征提取。比如用haralyzer库解析HAR统计所有X-Timestamp头的毫秒级精度、X-Nonce的字符集分布、X-Device-ID的MD5前缀规律——这些统计结果会成为Frida脚本里伪造合法上下文的原始数据。2.3 一次典型定位过程从403响应反推签名依赖项去年做的一个电商App项目目标是批量获取商品详情。Charles抓包显示GET /api/v3/item/detail?id12345总是返回403。按常规思路先看HeaderX-Signature: a1b2c3...f8e9 X-Timestamp: 1712345678901 X-Nonce: k7m9n2p4 X-Device-ID: d8f3a1b2c3d4e5f6我做了三件事时间戳篡改测试用Charles的Breakpoint功能在发送前把X-Timestamp减去1000010秒请求成功减去6000060秒返回{code:403,msg:timestamp too old}。确认时间窗口是±30秒。Nonce重放测试复制同一个X-Nonce发两次第二次直接403错误体里写着nonce reused。说明Nonce是一次性且服务端有缓存校验。Device-ID扰动测试把X-Device-ID末尾字符改一个返回{code:403,msg:invalid device id format}改成全数字返回{code:403,msg:device id checksum failed}。说明Device-ID不是简单透传内部有校验逻辑。到这里签名依赖项就清晰了时间戳±30秒、Nonce一次性、Device-ID带校验。但这还不够因为这三个值都是明文传输的签名本身必然还依赖一个隐藏因子。我把所有请求的X-Signature导出用Python做了个简单频次分析from collections import Counter import re # 从HAR里提取所有X-Signature值 signatures [h[request][headers].get(X-Signature, ) for h in har_data[log][entries]] # 统计前4位字符频次 prefixes [s[:4] for s in signatures if len(s) 4] print(Counter(prefixes).most_common(5)) # 输出[(a1b2, 12), (c3d4, 8), (e5f6, 5), (g7h8, 3), (i9j0, 2)]发现a1b2前缀占比最高且对应请求全是首页Feed流。而商品详情页的签名前缀全是c3d4。这说明签名算法里存在路径路由因子——不同API路径会触发不同的密钥或盐值。这个发现直接锁定了Frida Hook的目标类com.example.app.network.Signer因为只有这个类的sign(String path, MapString, String params)方法才能同时拿到路径和参数。3. Frida不是万能的但它是最锋利的“内存探针”Charles给你画出了战场地图Frida才是真正在战壕里摸排雷区的工兵。但绝大多数人用Frida的方式是错的一上来就Java.choose(com.example.app.network.Signer, {...})结果Class Not Found。为什么因为目标App用了腾讯Legu加固Signer类被混淆成a.b.c.d且运行时才解密加载。正确的做法是用Charles定位的上下文反向驱动Frida的Hook策略。3.1 绕过加固壳的三步递进式Hook法针对Legu、360、百度等主流加固我总结出一套不依赖脱壳的实时Hook流程第一步Hook ClassLoader.loadClass()捕获所有动态加载类Java.perform(function () { var ClassLoader Java.use(java.lang.ClassLoader); ClassLoader.loadClass.overload(java.lang.String).implementation function (className) { if (className.includes(Signer) || className.includes(Crypto) || className.includes(Util)) { console.log([] Loading class: className); // 在这里加个断点让App暂停方便用jadx-gui查看当前内存中的类结构 Java.scheduleOnMainThread(function () { console.log([!] Paused at loadClass - check memory now); }); } return this.loadClass(className); }; });这段脚本不会直接Hook到Signer但它会在Signer类被加载的瞬间输出类名并暂停主线程。这时你立刻用adb shell pidof com.example.app拿到进程ID再用jadx-gui --decompile-to-dir /tmp/decompiled --threads-count 8 --no-replace-consts --show-bad-code pid命令把当前内存中的DEX实时反编译。你会发现Signer类的真实名字可能是com.a.b.c.d而它的sign()方法签名是public static java.lang.String a(java.lang.String, java.util.Map)。第二步Hook ArtMethod.invoke()捕获所有反射调用很多加固App会把核心逻辑藏在反射调用里比如Class.forName(com.a.b.c.d).getMethod(a).invoke(null, ...)。这时候loadClassHook不到但ArtMethod.invoke一定能抓到。用Frida的Interceptor.attach直接Hook系统方法Interceptor.attach(Module.findExportByName(libart.so, art::mirror::ArtMethod::Invoke), { onEnter: function (args) { try { var method args[1]; var methodName method.getClassName() . method.getMethodName(); if (methodName.includes(a) methodName.includes(com.a.b.c.d)) { console.log([REFLECT] Invoking: methodName); console.log([REFLECT] Args: , Java.array(java.lang.Object, args.slice(3))); } } catch (e) { console.log([REFLECT ERROR], e); } } });第三步基于调用栈回溯精准Hook目标方法前两步拿到足够多的线索后第三步才是真正的目标打击。比如我们发现com.a.b.c.d.a()总是在okhttp3.Interceptor.intercept()之后被调用且它的第一个参数是/api/v3/item/detail这样的路径字符串。那就直接Hook这个调用栈Java.perform(function () { var Interceptor Java.use(okhttp3.Interceptor); Interceptor.intercept.overload(okhttp3.Interceptor$Chain).implementation function (chain) { var request chain.request(); var url request.url().toString(); if (url.includes(/api/v3/item/detail)) { console.log([CHAIN] Intercepting detail request: url); // 在这里触发我们的签名逻辑 var signer Java.use(com.a.b.c.d); var signature signer.a(url, Java.use(java.util.HashMap).$new()); console.log([SIGN] Generated: signature); // 把signature塞进Header var newRequest request.newBuilder() .addHeader(X-Signature, signature) .build(); return chain.proceed(newRequest); } return chain.proceed(request); }; });注意Java.use(com.a.b.c.d).a()这种写法在Legu加固下会报错因为方法名被混淆。正确写法是Java.use(com.a.b.c.d).$init.methods.forEach(m { if (m.name a) { /* hook it */ } })或者更稳妥地用Java.choose配合instanceOf判断。3.2 Frida脚本里必须写的五个“保命”细节写Frida脚本不是写Hello World稍有不慎就会让App崩溃或签名失效避免在Hook里做耗时操作console.log()在Android上是同步I/O如果每秒调用上百次会导致UI线程卡死。我的做法是只在关键节点打日志且用setTimeout异步化setTimeout(function () { console.log([SAFE LOG] Signature generated for url); }, 0);处理Java异常必须用try-catch包裹Frida里调用Java方法失败会直接抛出JS异常导致整个Hook中断。所有Java.use(...)调用都必须包在try里try { var signer Java.use(com.a.b.c.d); var sig signer.a(path, params); } catch (e) { console.log([ERROR] Failed to call signer.a: e); return fallback_signature; // 返回一个预设的兜底签名 }内存对象必须显式释放Frida的Java对象引用不会自动GC尤其在循环Hook中。每次用完Java.array或Java.use(java.util.HashMap)都要手动置空var map Java.use(java.util.HashMap).$new(); // ... use map map.$dispose(); // 必须调用dispose()时间戳必须用Java System.currentTimeMillis()不能用JS的Date.now()因为Android系统时间可能被篡改而System.currentTimeMillis()返回的是从系统启动开始的毫秒数更稳定。我在一个金融App里吃过亏JS时间戳比系统时间快2秒导致签名永远过期。Nonce必须用SecureRandom生成不能用Math.random()。SecureRandom是Android标准加密随机数生成器其熵值来自/dev/urandom而Math.random()只是伪随机。某次我用Math.random().toString(36).substr(2, 8)生成Nonce结果服务端返回{code:403,msg:weak nonce entropy}——这是服务端在做熵值检测。3.3 一次完整的Frida Hook实战破解双因子签名目标App的签名逻辑是SHA256(SHA256(path timestamp nonce device_id) secret_key)。其中secret_key不是硬编码在APK里而是从SharedPreferences里读取且每次App启动都会重新生成。Charles抓包发现secret_key只在/api/v2/auth/init响应体里返回一次之后所有签名都依赖它。我的Frida脚本分三部分第一部分Hook SharedPreferences.getString()捕获secret_keyJava.perform(function () { var SharedPreferences Java.use(android.content.SharedPreferences); SharedPreferences.getString.overload(java.lang.String, java.lang.String).implementation function (key, defValue) { if (key secret_key) { console.log([KEY] Captured secret_key: this.getString(key, defValue)); global_secret_key this.getString(key, defValue); // 存到全局变量 } return this.getString(key, defValue); }; });第二部分Hook Signer.sign()注入Python复现逻辑Java.perform(function () { var Signer Java.use(com.a.b.c.d); Signer.a.overload(java.lang.String, java.util.Map).implementation function (path, params) { // 从params里提取timestamp, nonce, device_id var timestamp params.get(X-Timestamp) || 0; var nonce params.get(X-Nonce) || ; var device_id params.get(X-Device-ID) || ; // 调用Python复现的签名函数通过frida-python bridge var pySig Python.eval( import hashlib def calc_signature(path, ts, nonce, did, key): inner hashlib.sha256((path ts nonce did).encode()).hexdigest() outer hashlib.sha256((inner key).encode()).hexdigest() return outer calc_signature(${path}, ${timestamp}, ${nonce}, ${device_id}, ${global_secret_key}) ); console.log([PY SIGN] For path : pySig); return pySig; }; });第三部分在Python端实现完整签名逻辑并处理边界情况import hashlib import time import random import string def generate_nonce(length8): 生成符合服务端熵值要求的Nonce # 服务端要求至少包含2个大写字母、2个小写字母、2个数字 chars string.ascii_letters string.digits while True: nonce .join(random.choices(chars, klength)) if (sum(1 for c in nonce if c.isupper()) 2 and sum(1 for c in nonce if c.islower()) 2 and sum(1 for c in nonce if c.isdigit()) 2): return nonce def calc_signature(path, timestamp, nonce, device_id, secret_key): 双SHA256签名计算 # 第一层SHA256path timestamp nonce device_id inner_input f{path}{timestamp}{nonce}{device_id} inner_hash hashlib.sha256(inner_input.encode()).hexdigest() # 第二层SHA256inner_hash secret_key outer_input f{inner_hash}{secret_key} outer_hash hashlib.sha256(outer_input.encode()).hexdigest() return outer_hash # 实际调用示例 if __name__ __main__: # 从Charles抓包中提取的原始值 path /api/v3/item/detail?id12345 timestamp str(int(time.time() * 1000)) # 毫秒级时间戳 nonce generate_nonce() device_id d8f3a1b2c3d4e5f6 # 从Charles里抄的 secret_key a1b2c3d4e5f67890 # 从Frida Hook里捕获的 sig calc_signature(path, timestamp, nonce, device_id, secret_key) print(fX-Signature: {sig}) print(fX-Timestamp: {timestamp}) print(fX-Nonce: {nonce})这个Python脚本不是独立运行的而是嵌入在Frida的Python.eval()里确保所有计算都在App进程内完成避免跨进程通信延迟。实测下来签名生成时间稳定在3ms以内完全满足App的超时要求。4. Python不是胶水而是整套反爬系统的“中央调度室”很多人以为Python在这套方案里只是“写个脚本发个请求”其实它承担着上下文协调、状态管理、异常熔断、数据归一化四大核心职能。没有Python的调度Charles和Frida只是两个孤立的工具有了Python它们才构成一个闭环系统。4.1 Python端必须实现的三大状态管理模块模块一Nonce池管理器解决一次性校验服务端的Nonce校验不是简单的“用过即废”而是有TTLTime To Live。比如/api/v2/auth/init返回的Nonce有效期是5分钟而/api/v3/item/detail用的Nonce有效期是30秒。Python必须维护一个带TTL的Nonce池import threading import time from collections import OrderedDict class NoncePool: def __init__(self, default_ttl30): self.pool OrderedDict() # 有序字典便于LRU淘汰 self.default_ttl default_ttl self.lock threading.Lock() def get(self, scopedefault): 获取指定scope的Nonce with self.lock: now time.time() # 清理过期Nonce for key in list(self.pool.keys()): if self.pool[key][expires_at] now: del self.pool[key] # 尝试复用未过期Nonce if scope in self.pool and self.pool[scope][expires_at] now: return self.pool[scope][value] # 生成新Nonce nonce self._generate_nonce() expires_at now self._get_ttl_for_scope(scope) self.pool[scope] {value: nonce, expires_at: expires_at} # LRU保持最多100个Nonce if len(self.pool) 100: self.pool.popitem(lastFalse) return nonce def _generate_nonce(self): # 复用前面定义的generate_nonce() return generate_nonce() def _get_ttl_for_scope(self, scope): ttl_map { auth_init: 300, # 5分钟 item_detail: 30, # 30秒 feed_list: 10, # 10秒 } return ttl_map.get(scope, self.default_ttl) # 全局实例 nonce_pool NoncePool()模块二Device-ID生命周期管理器解决设备指纹漂移Charles抓包发现X-Device-ID不是固定不变的。它由三部分组成hardware_id-app_version-install_time其中install_time是App首次安装的时间戳毫秒级。如果用户卸载重装install_time会变导致Device-ID失效。Python必须能动态生成合法Device-IDimport hashlib import time import uuid class DeviceIdManager: def __init__(self, hardware_idNone, app_version3.2.1): self.hardware_id hardware_id or self._generate_hardware_id() self.app_version app_version # 模拟install_time取当前时间往前推30天因为真实install_time不可知 self.install_time int((time.time() - 30 * 24 * 3600) * 1000) def get_device_id(self): 生成符合服务端校验规则的Device-ID # 服务端校验逻辑MD5(device_id)前8位必须等于device_id后8位 # 所以我们构造md5_part - app_version - install_time_str md5_part hashlib.md5(f{self.hardware_id}{self.app_version}{self.install_time}.encode()).hexdigest()[:8] install_time_str str(self.install_time) device_id f{md5_part}-{self.app_version}-{install_time_str} # 验证MD5(device_id)前8位 device_id前8位 verify hashlib.md5(device_id.encode()).hexdigest()[:8] if verify ! device_id[:8]: # 不匹配重新生成install_time微调1毫秒 self.install_time 1 return self.get_device_id() return device_id def _generate_hardware_id(self): # 基于MAC地址和Android ID生成稳定hardware_id mac 00:11:22:33:44:55 # 从Charles里抄的 android_id a1b2c3d4e5f67890 # 从Settings.Secure.ANDROID_ID获取 return hashlib.md5(f{mac}{android_id}.encode()).hexdigest()[:16] # 使用 device_mgr DeviceIdManager() device_id device_mgr.get_device_id() print(fX-Device-ID: {device_id})模块三签名熔断器解决服务端主动升级反爬服务端可能随时升级签名算法比如把双SHA256改成SHA256HMAC。Python必须能自动检测并降级import requests from functools import wraps class SignatureCircuitBreaker: def __init__(self, failure_threshold3, reset_timeout60): self.failure_count 0 self.last_failure_time 0 self.failure_threshold failure_threshold self.reset_timeout reset_timeout self.is_open False def call(self, func, *args, **kwargs): if self.is_open: # 熔断开启走降级逻辑 return self._fallback_signature(*args, **kwargs) try: result func(*args, **kwargs) self._on_success() return result except Exception as e: self._on_failure() raise e def _on_success(self): self.failure_count 0 self.is_open False def _on_failure(self): self.failure_count 1 self.last_failure_time time.time() if self.failure_count self.failure_threshold: self.is_open True print(f[CIRCUIT BREAKER] Opened due to {self.failure_count} failures) def _fallback_signature(self, path, timestamp, nonce, device_id, secret_key): # 降级为单SHA256签名 input_str f{path}{timestamp}{nonce}{device_id} return hashlib.sha256(input_str.encode()).hexdigest() # 全局熔断器 circuit_breaker SignatureCircuitBreaker() # 包装签名函数 wraps(calc_signature) def safe_calc_signature(*args, **kwargs): return circuit_breaker.call(calc_signature, *args, **kwargs)4.2 Python与Frida的双向通信协议设计Frida脚本运行在App进程内Python运行在PC上两者需要高效通信。我设计了一个极简的HTTP-based IPC协议Frida端启动一个本地HTTP Server用OkHttp// 在Frida脚本里启动一个监听12345端口的Server Java.perform(function () { var OkHttpClient Java.use(okhttp3.OkHttpClient); var Server Java.use(com.example.app.network.LocalServer); Server.start.overload().implementation function () { console.log([SERVER] Starting on port 12345); // 启动Server... return this.start(); }; // 当Python POST /sign 请求时调用签名逻辑 Server.handleSignRequest.implementation function (requestJson) { var data JSON.parse(requestJson); var sig calc_signature( data.path, data.timestamp, data.nonce, data.device_id, data.secret_key ); return JSON.stringify({signature: sig}); }; });Python端封装成requests调用import requests import json class FridaSigner: def __init__(self, host127.0.0.1, port12345): self.base_url fhttp://{host}:{port} def sign(self, path, timestamp, nonce, device_id, secret_key): payload { path: path, timestamp: timestamp, nonce: nonce, device_id: device_id, secret_key: secret_key } try: resp requests.post(f{self.base_url}/sign, jsonpayload, timeout5) if resp.status_code 200: return resp.json()[signature] else: raise Exception(fFrida server error: {resp.status_code}) except requests.exceptions.RequestException as e: # 网络失败降级到Python本地计算 print(f[FALLBACK] Using local calc_signature: {e}) return calc_signature(path, timestamp, nonce, device_id, secret_key) # 使用 signer FridaSigner() sig signer.sign(/api/v3/item/detail?id12345, str(int(time.time() * 1000)), nonce_pool.get(item_detail), device_mgr.get_device_id(), a1b2c3d4e5f67890)这个设计的好处是Frida只负责最核心的签名计算利用App进程内的secret_key和真实环境Python负责所有外围逻辑Nonce管理、Device-ID生成、熔断降级。两者各司其职耦合度最低。4.3 完整的Python采集工作流代码把所有模块组装起来就是一个可直接运行的采集脚本#!/usr/bin/env python3 # -*- coding: utf-8 -*- Python移动端反爬采集主流程 整合Charles抓包分析、Frida Hook、状态管理、熔断降级 import time import json import requests import logging from urllib.parse import urlparse, parse_qs # 初始化所有管理器 nonce_pool NoncePool() device_mgr DeviceIdManager() signer FridaSigner() circuit_breaker SignatureCircuitBreaker() # 配置日志 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) def build_headers(path, query_paramsNone): 构建完整请求Headers timestamp str(int(time.time() * 1000)) nonce nonce_pool.get(item_detail) device_id device_mgr.get_device_id() # 从Charles分析得知secret_key需要从/auth/init接口获取 # 这里简化为从环境变量读取实际项目中应从Frida Hook捕获 secret_key a1b2c3d4e5f67890 # 计算签名 signature circuit_breaker.call( signer.sign, path, timestamp, nonce, device_id, secret_key ) headers { X-Signature: signature, X-Timestamp: timestamp, X-Nonce: nonce, X-Device-ID: device_id, User-Agent: ExampleApp/3.2.1 (Android 12; Pixel 5), Accept: application/json, Content-Type: application/json; charsetutf-8 } return headers def fetch_item_detail(item_id): 获取单个商品详情 url fhttps://api.example.com/api/v3/item/detail?id{item_id} headers build_headers(/api/v3/item/detail?id item_id) try: logger.info(fFetching item {item_id}) resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: logger.info(fSuccess for item {item_id}) return resp.json() elif resp.status_code 403: error_data resp.json() logger.error(f403 Forbidden for item {item_id}: {error_data.get(msg, Unknown)}) # 根据错误信息触发熔断或重试 if timestamp in error_data.get(msg, ): time.sleep(0.1) # 时间戳错误稍等再试 return fetch_item_detail(item_id) else: logger.error(fHTTP {resp.status_code} for item {item_id}) except requests.exceptions.RequestException as e: logger.error(fRequest failed for item {item_id}: {e}) return None def main(): 主采集流程 item_ids [12345, 67890, 24680, 13579] # 示例商品ID results [] for item_id in item_ids: # 每次请求间隔随机化模拟真实用户行为 time.sleep(random.uniform(0.8, 1.5)) data fetch_item_detail(item_id) if data: results.append(data) # 保存结果 with open(items.json, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2) logger.info(fCollected {len(results)} items) if __name__ __main__: main()这个脚本可以直接运行它会自动管理Nonce生命周期避免重复使用动态生成合法Device-ID应对设备指纹校验通过Frida调用真实App内的签名逻辑保证secret_key有效性内置熔断机制当签名连续失败3次自动降级到本地计算日志详细记录每个环节便于问题追溯。5. 最后分享一个血泪教训别在Frida里做网络请求这是我踩过最深的一个坑。