资讯动态

美团App爬虫实战:AES加密、设备指纹与Frida动态签名

发布时间:2026/10/19 3:43:58 来源:尧图企业网站定制
简介这是一份面向Python中级开发者与数据采集实践者的美团App端数据爬取实战项目聚焦反爬应对、接口逆向与结构化存储等核心难点适用于安全研究、竞品分析及本地化数据集构建等场景。压缩包共23个文件含7个Python主控与逻辑脚本如sign.py、main.py、shopDetail.py、6个JSON格式的示例数据与配置如shopList.json、comment.json、5个XML配置文件来自.idea开发环境以及README.md、note说明文档和jscode.js逆向JS代码整体仅157KB轻量易读。已有1569人学习下载资源结构清晰从初始化、签名生成、请求封装到店铺/评论多级解析覆盖完整链路特别包含可运行的request.py封装、动态sign算法实现及真实响应数据样例便于快速复现、调试与二次开发。1. 美团App爬虫不是“点开即用”的黑盒而是一套需要你亲手拧紧每颗螺丝的反调试实战沙盒你刚下载完美团app爬虫.zip双击解压、pip install、python run.py——然后卡在「请求超时」或「403 Forbidden」上整整两小时别急这不是你的环境问题而是这个项目从第一天起就默认你已理解美团App的流量不是HTTP明文管道而是一条布满JS混淆、设备指纹校验、动态Token签名校验、滑块/点击行为验证的防御链路。它不面向“想抓点店铺数据做竞品分析”的轻量用户而是为某高校实验室做本地生活服务画像研究、或某公司搭建私有化商户数据库的开发者准备的——你需要能看懂Frida hook日志、能重放Burp里被拦截的加密请求、能识别出_token和__mtan哪个是设备级签名、哪个是会话级签名。项目里没有一行代码教你“如何注册账号”但有三处硬编码的UA字符串、两套并行的AES密钥推导逻辑、一个被注释掉80%的sign_v2生成函数——它假设你已经翻过美团Android端的so层符号表知道libmtguard.so里getSignData的调用栈怎么打。如果你只想要“自动登录自动翻页自动存Excel”请立刻关掉这个页面但如果你正卡在“为什么同样的headers在Postman里能通在Python里必401”那这份源码就是你缺了三天的后悔药。2. 架构拆解三层防御穿透设计与模块职责边界这个项目不是单脚本暴力轮询而是一个分层解耦的对抗系统。核心逻辑被切为三个物理隔离层协议解析层protocol/负责解密响应体与构造请求头设备模拟层device/负责生成可信设备指纹与行为轨迹业务编排层spider/负责调度任务流与异常熔断。所有模块通过config.yaml注入参数而非硬编码拼接URL——这意味着你改一个device_id整个链路的签名、时间戳、设备特征都会联动刷新。这种设计让二次开发成本陡降想换华为P60模拟只需改device/profiles/huawei_p60.json想绕过新上线的滑块验证直接在spider/middleware/verify.py里注入自定义verify_handler。下面逐层展开关键实现。2.1 协议解析层protocol/encryptor.py的AESRSA混合加解密链美团对关键接口如/api/v1/shop/search的请求体强制AES-CBC加密且每次请求的iv由当前时间戳与设备ID双重哈希生成。encryptor.py封装了完整的加解密闭环# protocol/encryptor.py from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import hashlib class MeituanEncryptor: def __init__(self, device_id: str, app_version: str 12.12.204): self.device_id device_id self.app_version app_version # 注意此处密钥并非固定值而是从libmtguard.so中提取的硬编码seed # 实际项目中该seed存储在config.yaml的encrypt.seed字段 self.aes_key self._derive_aes_key() def _derive_aes_key(self) - bytes: # 真实逻辑调用JNI方法 com.meituan.mtguard.MtGuard.getAesKey() # 开源版简化为SHA256(device_id app_version mt_guard_seed) seed mt_guard_seed key_str f{self.device_id}{self.app_version}{seed} return hashlib.sha256(key_str.encode()).digest()[:16] # AES-128 def encrypt_request(self, data: dict) - dict: # 步骤1JSON序列化并填充至16字节倍数 raw json.dumps(data, separators(,, :)).encode() padded pad(raw, AES.block_size) # 步骤2生成IV关键必须与服务端一致 iv_str f{int(time.time() * 1000)}{self.device_id[:8]} iv hashlib.md5(iv_str.encode()).digest()[:16] # 步骤3AES加密 cipher AES.new(self.aes_key, AES.MODE_CBC, iv) encrypted cipher.encrypt(padded) # 步骤4Base64编码并注入到请求体 return { data: base64.b64encode(encrypted).decode(), iv: base64.b64encode(iv).decode(), ts: int(time.time() * 1000) }提示_derive_aes_key()中的mt_guard_seed是项目安全核心。原始libmtguard.so中该seed被混淆为16进制字符串数组本项目已将其还原为明文并写入config.yaml。若你发现加密后服务端返回{code:40001,msg:invalid sign}90%概率是encrypt.seed配置错误或device_id未同步更新。2.2 设备模拟层device/fingerprint.py的五维可信指纹生成器美团服务端会校验5类设备特征device_idUUIDv4、os_versionAndroid 13.0、modelHUAWEI P60、screen_size1212x2700、mac_address随机生成但符合OUI规范。fingerprint.py将这些维度封装为可复用对象# device/fingerprint.py import uuid import random class DeviceFingerprint: def __init__(self, profile_path: str): with open(profile_path) as f: self.profile json.load(f) # 如 device/profiles/huawei_p60.json def generate(self) - dict: # device_id必须全局唯一且持久化否则token失效 device_id str(uuid.uuid4()).replace(-, ) # mac_address前3字节取自华为OUI列表后3字节随机 oui_list [00:1A:11, 00:1E:67, 00:23:CD] mac_prefix random.choice(oui_list) mac_suffix :.join([f{random.randint(0,255):02x} for _ in range(3)]) mac f{mac_prefix}:{mac_suffix} return { device_id: device_id, os_version: self.profile[os_version], model: self.profile[model], screen_size: self.profile[screen_size], mac_address: mac, brand: self.profile[brand] } # 使用示例生成华为P60指纹 fp DeviceFingerprint(device/profiles/huawei_p60.json) fingerprint fp.generate() print(fingerprint) # {device_id: e8a3b1c2d4e5f6a7b8c9d0e1f2a3b4c5, ...}注意device_id一旦生成必须写入本地文件如data/device_state.json并跨会话复用。项目中spider/core.py第87行有强制检查若检测到device_id变更自动清空cookies并重新触发登录流程——这是防止因设备指纹漂移导致的批量封号。2.3 业务编排层spider/task_scheduler.py的熔断式任务队列面对美团QPS限流通常5次/秒/IP硬扛只会触发IP封禁。task_scheduler.py采用三级熔断请求级单次超时8s则跳过、IP级连续3次429则暂停该IP 60s、设备级单设备24h内失败50次则标记为不可用# spider/task_scheduler.py from collections import defaultdict, deque import time class TaskScheduler: def __init__(self): self.ip_failures defaultdict(deque) # {ip: deque([timestamp, ...])} self.device_failures defaultdict(int) self.max_failures_per_device 50 self.cooldown_seconds 60 def should_retry(self, ip: str, device_id: str) - bool: # IP级熔断60s内失败3次则冷却 now time.time() self.ip_failures[ip] deque( [t for t in self.ip_failures[ip] if now - t self.cooldown_seconds], maxlen3 ) if len(self.ip_failures[ip]) 3: return False # 设备级熔断 if self.device_failures[device_id] self.max_failures_per_device: return False return True def record_failure(self, ip: str, device_id: str): self.ip_failures[ip].append(time.time()) self.device_failures[device_id] 1玄学经验美团服务端对User-Agent中的Build/字段极其敏感。项目config.yaml中headers.user_agent模板为MeituanGroup/12.12.204 (com.sankuai.meituan; build:204; Android 13; HUAWEI P60)其中build:204必须与app_version完全匹配否则即使签名正确也会返回40003。这个细节在官方文档里根本找不到是某开发者抓包对比200个请求后确认的。3. 运行指南从零构建可稳定运行的爬虫环境别被requirements.txt里23个依赖吓到——真正不可替代的只有4个requests发请求、pycryptodomeAES/RSA、frida-tools动态Hook、fake-useragentUA池。其他如pandas、openpyxl只是结果导出用可按需删减。下面是从解压到跑通第一个店铺列表的完整路径每一步都标注了为什么必须这么做。3.1 环境初始化Python 3.9 与 Frida 的强制绑定美团App的签名算法藏在libmtguard.so的JNI方法中纯Python无法逆向。必须用Frida在真机/模拟器上Hook并导出密钥。因此第一步不是pip install而是部署Frida环境# 步骤1安装Frida服务端以Android为例 # 下载对应架构的frida-server如frida-server-16.1.4-android-arm64.xz # 解压后adb push到手机/data/local/tmp/并chmod 755 adb push frida-server-16.1.4-android-arm64 /data/local/tmp/frida-server adb shell chmod 755 /data/local/tmp/frida-server # 步骤2启动Frida服务端需root权限 adb shell /data/local/tmp/frida-server # 步骤3安装Python Frida客户端 pip install frida-tools11.2.0 # 注意必须锁定11.2.0新版Frida 16.x与Python SDK存在ABI不兼容避坑 / 常见问题 / 排查现象1frida-ps -U列不出美团进程或报错Failed to enumerate processes: unable to connect to remote frida-server原因frida-server未正确运行或手机未开启USB调试安装未知应用权限或frida-server版本与手机CPU架构不匹配如arm64手机用了arm版本解决执行adb shell ps | grep frida确认进程存在用adb shell getprop ro.product.cpu.abi查CPU架构重新下载对应架构的frida-server现象2python hook_sign.py报错frida.core.RPCException: TypeError: Cannot convert undefined to object原因Hook脚本中Java.use(com.meituan.mtguard.MtGuard).getSignData.overload(...)的overload签名与实际so层不符解决用objdump -t libmtguard.so | grep getSignData查看真实符号或改用Java.choose动态搜索类实例现象3encryptor.py生成的iv与Frida Hook到的iv不一致原因encryptor.py中iv_str的时间戳精度为毫秒但so层实际使用微秒或纳秒解决在hook_sign.py中打印System.currentTimeMillis()和System.nanoTime()将iv_str改为f{int(time.time_ns() // 1000000)}{self.device_id[:8]}3.2 配置文件注入config.yaml的7个必填字段项目根目录的config.yaml不是可选配置而是运行前提。漏填任一字段都会导致KeyError崩溃。以下是必须修改的7项其余可保持默认# config.yaml device: profile: device/profiles/huawei_p60.json # 必填设备档案路径 state_file: data/device_state.json # 必填设备状态持久化路径 encrypt: seed: a1b2c3d4e5f6g7h8 # 必填从Frida Hook获取的AES种子 rsa_public_key: -----BEGIN PUBLIC KEY-----\n... # 必填从APK assets中提取的RSA公钥 headers: user_agent: MeituanGroup/12.12.204 (com.sankuai.meituan; build:204; Android 13; HUAWEI P60) # 必填严格匹配App版本 referer: https://www.meituan.com/ # 必填Referer必须为meituan.com域名 network: timeout: 15 # 必填请求超时美团部分接口响应慢 retry_times: 3 # 必填单请求重试次数 output: format: json # 必填输出格式json/csv/excel提示encrypt.seed的获取方式已在docs/FRIDA_HOOK_GUIDE.md中详细说明。简言之启动美团App → Frida注入 → HookMtGuard.getAesKey()→ 打印返回值。该值为32字节hex字符串填入seed时需去掉0x前缀并转为小写。3.3 首次运行python spider/main.py --mode search --keyword 火锅的完整链路执行命令后控制台将输出6阶段日志每阶段对应一个核心模块# 阶段1加载设备指纹 [INFO] Loading device profile from device/profiles/huawei_p60.json [INFO] Generated fingerprint: {device_id: e8a3b1c2..., model: HUAWEI P60, ...} # 阶段2初始化加密器此时会校验encrypt.seed [INFO] Initialized MeituanEncryptor with AES key derived from seed # 阶段3构造搜索请求含动态签名 [DEBUG] Signing request for /api/v1/shop/search: ts1712345678901, data_len128 # 阶段4发送请求自动处理302重定向与Cookie同步 [INFO] Request to https://apimobile.meituan.com/api/v1/shop/search returned 200 # 阶段5解密响应体调用decrypt_response [INFO] Decrypted response: {data: [{id: 123456, name: 海底捞}]} # 阶段6保存结果 [INFO] Saved 12 shops to output/search_火锅_20240405.json避坑 / 常见问题 / 排查现象1控制台卡在[INFO] Signing request...超过10秒原因encrypt.seed错误导致AES加密后服务端无法解密进入无限重试解决立即CtrlC中断检查config.yaml中encrypt.seed是否为32字符hex串且与Frida Hook结果完全一致现象2返回{code:40002,msg:invalid timestamp}原因设备时间与NTP服务器偏差超过30秒美团校验ts参数解决在手机设置中开启“自动设置日期和时间”或在spider/core.py中强制同步时间os.system(adb shell settings put global auto_time 1)现象3返回{code:40004,msg:device not trusted}原因device_id未持久化每次运行都生成新ID解决确认device.state_file路径可写且spider/core.py第87行load_device_state()未被注释4. 核心模块深度解析spider/middleware/sign_middleware.py的动态签名注入机制签名中间件是整个项目的“心脏起搏器”。它不依赖静态密钥而是每次请求前实时调用Frida脚本从正在运行的美团App进程中提取最新签名。这种设计规避了密钥硬编码被反编译的风险但也带来了调试复杂度。我们来逐行拆解其工作原理。4.1 签名中间件的生命周期从Hook到注入sign_middleware.py本质是一个Requests的Session钩子。当spider/core.py调用session.post(url, jsonpayload)时该中间件会拦截请求执行三步操作1构造待签名原始数据2调用Frida脚本计算签名3将签名注入请求头。关键代码如下# spider/middleware/sign_middleware.py import frida import json import time class SignMiddleware: def __init__(self, device_id: str, app_version: str): self.device_id device_id self.app_version app_version # 步骤1连接Frida服务端必须提前运行frida-server self.device frida.get_usb_device() # 步骤2获取美团App进程ID需已启动App try: self.pid self.device.spawn([com.sankuai.meituan]) except frida.ProcessNotFoundError: raise RuntimeError(Meituan App not found. Please launch it manually.) def inject_sign(self, request): # 步骤3构造原始签名数据美团要求JSON字符串无空格 raw_data json.dumps(request.json, separators(,, :)) # 步骤4调用Frida脚本脚本路径在config.yaml中指定 script_content self._load_frida_script() session self.device.attach(self.pid) script session.create_script(script_content) script.load() # 步骤5执行签名计算传入raw_data和device_id result script.exports.sign_data(raw_data, self.device_id) # 步骤6注入到请求头 request.headers.update({ X-Request-ID: result[request_id], X-Sign: result[sign], X-Timestamp: str(result[timestamp]) }) return request def _load_frida_script(self) - str: # 读取frida_scripts/sign_hook.js该脚本已预置Hook逻辑 with open(frida_scripts/sign_hook.js) as f: return f.read()逻辑说明sign_middleware.py不直接实现签名算法而是作为“翻译官”——把Python请求数据翻译成Frida能理解的格式再把Frida返回的签名结果翻译回HTTP头。这种解耦让算法升级只需替换sign_hook.js无需改动Python主逻辑。4.2 Frida脚本frida_scripts/sign_hook.js的JNI调用链sign_hook.js是真正的签名引擎。它通过Frida的Java.perform在美团App进程内存中定位MtGuard类并调用其signData方法。以下是精简后的核心逻辑// frida_scripts/sign_hook.js Java.perform(function () { const MtGuard Java.use(com.meituan.mtguard.MtGuard); // Hook signData方法重载版本String, String, String MtGuard.signData.overload(java.lang.String, java.lang.String, java.lang.String).implementation function (data, deviceId, appVersion) { console.log([FRIDA] Signing data:, data.substring(0, 50) ...); // 调用原生方法获取签名 const result this.signData.overload(java.lang.String, java.lang.String, java.lang.String).call(this, data, deviceId, appVersion); // 提取关键字段 const signObj JSON.parse(result); send({ sign: signObj.sign, request_id: signObj.requestId, timestamp: signObj.timestamp }); return result; }; });参数说明signData方法接收三个参数data待签名的JSON字符串、deviceId设备ID、appVersionApp版本。其中data必须是紧凑JSON无空格否则服务端校验失败。send()函数将结果传递给Python端script.exports.sign_data()即对此的封装。4.3 动态签名的可靠性保障心跳检测与Fallback机制Frida连接不稳定是常态。为此SignMiddleware内置了心跳检测每5分钟尝试ping一次Frida服务端若失败则自动重启frida-server。更关键的是Fallback机制——当Frida不可用时启用降级签名算法基于encrypt.seed的HMAC-SHA256# spider/middleware/sign_middleware.py def inject_sign(self, request): try: # 尝试Frida签名主路径 result self._frida_sign(request.json, self.device_id) except (frida.ServerNotRunningError, frida.ProcessNotFoundError): # Frida失败启用Fallback仅用于调试生产环境禁用 result self._fallback_sign(request.json, self.device_id) print([WARNING] Using fallback signature. Frida connection lost!) request.headers.update({ X-Sign: result[sign], X-Timestamp: str(result[timestamp]) }) return request def _fallback_sign(self, data: dict, device_id: str) - dict: # 降级算法HMAC-SHA256(device_id json.dumps(data)) raw json.dumps(data, separators(,, :)) key self.config[encrypt][seed].encode() sign hmac.new(key, f{device_id}{raw}.encode(), hashlib.sha256).hexdigest() return { sign: sign, timestamp: int(time.time() * 1000), request_id: str(uuid.uuid4()) }注意Fallback签名仅用于开发调试。美团服务端会定期更新签名算法Fallback可能随时失效。生产环境必须确保Frida稳定运行——这也是项目强制要求真机而非模拟器的原因模拟器上Frida Hook成功率低于30%。5. 二次开发实战如何将店铺搜索扩展为全量商户采集系统现在你已能跑通单次搜索但真实需求往往是“采集北京朝阳区所有火锅店每家店的评论、营业时间、人均消费”。这就需要改造项目结构使其支持地理围栏分片、增量去重、多级页面爬取。下面是我为某公司落地该项目时的真实改造路径每一步都经过线上验证。5.1 地理围栏分片spider/geocode.py的高德地图坐标转换器美团API不接受行政区名称如“朝阳区”只接受经纬度坐标。因此需先将区域名转为坐标中心点再生成网格。geocode.py封装了高德地图API调用# spider/geocode.py import requests import json class GeoCoder: def __init__(self, amap_key: str): self.amap_key amap_key def get_district_center(self, city: str, district: str) - tuple: 获取行政区中心坐标 url fhttps://restapi.amap.com/v3/config/district?keywords{district}subdistrict0key{self.amap_key} resp requests.get(url).json() if resp[status] 1 and resp[districts]: center resp[districts][0][center] return tuple(map(float, center.split(,))) raise ValueError(fDistrict {district} not found) def generate_grid(self, center: tuple, radius_km: int 5) - list: 生成半径为radius_km的正方形网格单位度 lat, lng center # 经纬度1度≈111km纬度经度随纬度变化此处简化 lat_step radius_km / 111 lng_step radius_km / (111 * abs(math.cos(math.radians(lat)))) grid [] for i in range(-2, 3): # 5x5网格 for j in range(-2, 3): grid.append(( round(lat i * lat_step, 6), round(lng j * lng_step, 6) )) return grid # 使用示例 gc GeoCoder(your_amap_key_here) center gc.get_district_center(北京, 朝阳区) grid gc.generate_grid(center, radius_km3) print(fGenerated {len(grid)} grid points) # [(39.923456, 116.456789), (39.923456, 116.456789), ...]改造点在spider/main.py中新增--geo参数调用GeoCoder生成网格每个网格点作为独立搜索任务提交到TaskScheduler。这样可将“朝阳区”拆为25个子任务并发采集。5.2 增量去重spider/storage/deduplicator.py的布隆过滤器实现避免重复采集同一店铺美团店铺ID可能跨搜索词出现需在内存中维护去重集合。但全量店铺ID达千万级不能用Python set。deduplicator.py采用布隆过滤器# spider/storage/deduplicator.py from pybloom_live import ScalableBloomFilter class Deduplicator: def __init__(self, initial_capacity1000000, error_rate0.001): # 可扩容布隆过滤器初始容量100万误判率0.1% self.bf ScalableBloomFilter( initial_capacityinitial_capacity, error_rateerror_rate, modeScalableBloomFilter.SMALL_SET_GROWTH ) def add(self, shop_id: str): self.bf.add(shop_id) def contains(self, shop_id: str) - bool: return shop_id in self.bf # 在spider/core.py中集成 dedup Deduplicator() for shop in search_result[data]: if not dedup.contains(shop[id]): dedup.add(shop[id]) save_shop_detail(shop[id]) # 采集详情页血泪经验布隆过滤器的error_rate不能设为0数学上不可能。我测试过error_rate0.0001时内存占用翻倍但误判率未显著下降最终选定0.001——线上运行3个月误判导致的重复采集5次可接受。5.3 多级页面爬取spider/pipeline/detail_pipeline.py的异步详情采集链店铺搜索只返回ID和名称详情页/api/v1/shop/detail才包含评论、营业时间等。detail_pipeline.py构建了异步采集链# spider/pipeline/detail_pipeline.py import asyncio import aiohttp class DetailPipeline: def __init__(self, session: aiohttp.ClientSession): self.session session async def fetch_shop_detail(self, shop_id: str) - dict: url fhttps://apimobile.meituan.com/api/v1/shop/detail?id{shop_id} async with self.session.get(url) as resp: data await resp.json() # 解密响应体复用protocol/encryptor.py decrypted self.encryptor.decrypt_response(data[data]) return { id: shop_id, name: decrypted[name], comments: decrypted.get(comments, []), business_hours: decrypted.get(business_hours, ), avg_price: decrypted.get(avg_price, 0) } async def run(self, shop_ids: list) - list: tasks [self.fetch_shop_detail(shop_id) for shop_id in shop_ids] return await asyncio.gather(*tasks, return_exceptionsTrue) # 在main.py中调用 async def main(): async with aiohttp.ClientSession() as session: pipeline DetailPipeline(session) details await pipeline.run(shop_ids[:100]) # 每次最多100个关键技巧美团详情页QPS限制比搜索页更严2次/秒/IP。DetailPipeline内置了asyncio.Semaphore(2)限流器确保并发数不超过2。同时所有aiohttp请求复用同一个ClientSession避免TCP连接风暴。从那以后我每次启动新任务都强制走一遍geocode.py的坐标校验 Deduplicator的布隆过滤器初始化 TaskScheduler的熔断状态重置——这三步耗时不到2秒却能避免80%的线上翻车。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑