简介:一套面向Python爬虫学习与美团App数据采集场景的压缩包工程,聚焦签名生成、请求封装、店铺与评论信息解析等核心模块。包内共二十三个文件,以七个Python脚本为主干,涵盖入口调度、签名计算、主流程请求、列表页与详情页解析等职责;另有六个JSON示例数据、五个XML工程配置、一个JavaScript脚本及Markdown说明文档,整体约一百五十七KB,目录层次清晰,方便直接阅读和二次修改。目前已有1565人学习下载。该工程演示了从URL收集、请求网页、解析内容到数据存储的完整爬虫链路,并在代码中体现User-Agent设置、JS辅助签名、反爬应对等实践,还给出店铺、评论、详情等样例数据用于结果校验。适合具备一定Python基础、希望了解App爬虫签名机制与数据采集流程的开发者,既可作为入门到进阶的参考案例,也能在研究反爬策略、设计合规采集方案时提供直接素材。
1. 解压这份爬虫项目之前,先看清它到底能干什么
解压一份“本地生活App爬虫.zip”之后,大多数人会卡在同一个地方:代码能跑,但它抓的不是你想抓的数据。这类压缩包其实是一条研究链路,先通过中间人抓包拿到App的HTTP请求,再反编译定位签名生成逻辑,最后用Hook把动态参数补全,复现出能主动控制的请求。它能解决的问题是:App接口在网络层是加密且带签名的,浏览器里看不到,抓包也只是一堆乱码。适合手里正好有逆向任务、又不想从零啃协议的人。这篇文章按抓包→反编译→Hook→脚本化这条链路讲,每一步的命令、参数和翻车点都会交代清楚。
2. 先把路线定对:抓包、逆向与Hook,为什么一个都不能少
整条路线的难点不在某一个工具,而在于把三个阶段的产出串起来:抓包出协议,反编译出算法,Hook出参数。顺序反了会事倍功半。我见过有人先把APK反编译了个遍,没抓包就去找签名逻辑,最后连哪个接口要签名都不知道。所以这一章先把“为什么是这个顺序”讲透。
2.1 为什么从抓包入手:让请求先“裸奔”
App里所有能被采集的数据,最后都以HTTP请求的形式出现在网络层。抓包的意义是把请求从黑匣子里拉出来,让你看到URL、请求头、请求体、参数名。我一般会先抓一轮完整流量,再决定要不要进入逆向阶段。判断标准很直接:请求里有没有sign、token、timestamp这类字段。如果有,说明每个请求都带签名,直接看网页版接口没有参考意义,必须进入反编译;如果只是普通的公开数据接口,抓包之后直接用脚本复现就行,根本不用碰代码。
这里有个新手常犯的误区:一上来就装抓包工具,但不对HTTPS解密,抓到的全是加密流,于是认为App做了高强度防护。其实大多数时候只是证书信任问题,密解开之后请求结构一目了然。抓包阶段的目标不是拿到全部数据,而是拿到三样东西:接口路径、参数名、签名参数的位置。这三样东西直接决定后面往哪个方向搜代码。如果连这三样都没摸清,反编译只会变成大海捞针。
2.2 反编译不是玄学:定位签名与请求生成代码
反编译的目的不是把整个App读一遍,而是按图索骥。抓到请求后,记下几个特殊字段名,比如token、sign、nonce,然后用jadx按关键词搜索,往往能直接跳到生成签名的工具类。常见套路是:签名算法集中在一个工具类的静态方法里,入参是参数集合,出参是一个摘要串。这类类在反编译结果里非常好认:全是静态方法,内部全是字符串拼接和hash调用,几乎没有UI相关代码。比如你搜“sign”命中某个工具类,打开之后看到generateSign(Map params),下一步就是分析拼接格式和hash类型。
如果APK加固过,直接反编译出来的全是壳代码,看不到业务逻辑。这时要先过脱壳这一步,常见做法是用动态加载方式把dex从内存里dump出来,再丢给jadx。这条路稍微麻烦一点,但原理不复杂:壳再强,最终要执行真正的逻辑,内存里一定存在完整的dex。脱壳工具选型上,不追求最新,追求和你的模拟器环境匹配,不然又搭进去半天环境问题。
2.3 Hook为什么是最后一块拼图
静态代码能告诉我们签名由哪些参数算出来,但算不出这次请求用到的具体值。时间戳、随机数、设备ID这些参数都是App运行时产生的,光看代码没法知道当前值是多少。Hook就是在这里介入:让App跑起来,在指定的方法入口挂一个回调,把入参和返回值打印出来,一次性拿到真实请求的完整上下文。
选型上,Frida是目前最常见的方案。它不需要重打包APK,命令行客户端负责连接设备,JS脚本描述拦截逻辑,改脚本比改代码重打包快得多。代价是要在一个可控的调试环境里运行,比如root过的模拟器。Hook不是替代抓包和反编译,而是给它们补上动态上下文:抓包确认目标接口,反编译还原签名结构,Hook拿到真实参数值。三条信息凑齐,才可能写出一份能主动控制的请求脚本。三个阶段的关系可以简化成下面这张表:
| 阶段 | 产出 | 典型工具 |
|---|---|---|
| 抓包 | 接口路径、参数名、请求头 | 任意中间人抓包工具 |
| 反编译 | 签名算法、拼接顺序 | jadx |
| Hook | 动态参数值、返回值 | Frida |
3. 从抓包到反编译:三步还原目标App请求的完整链路
这一章把静态分析和动态观察连起来:先让流量可见,再从代码里找算法入口,最后确认哪个方法对应哪一条请求。三步走完,你就有了一个明确的Hook目标,而不是对着反编译源码发呆。
3.1 打通HTTPS中间人:证书安装与端口转发的最小配置
先把抓包入口跑起来。常见做法是用带Web控制台的中间人工具,命令只需要一行:
# 启动带控制台的抓包入口,监听8080端口 mitmweb --listen-port 8080说明:默认监听8080端口,如果被占用,换成8888或任意空闲端口。mitmweb会打开一个浏览器控制台,所有经过的请求都显示在里面,适合边看边改。如果是长期采集,我更习惯用mitmdump把流量写到文件,命令是mitmdump --listen-port 8080 -w flows.mitm,抓完再慢慢分析。
接下来让模拟器流量经过这个入口。在模拟器的WiFi设置里找到当前网络,把HTTP转发指向电脑IP和刚才的端口。模拟器NAT模式下访问宿主机常用固定IP(通常是10.0.2.2),真机则填电脑的局域网IP。这里只是系统配置,不需要改App。配好之后打开浏览器访问任意网页,如果抓包控制台能看到这条请求,就说明链路通了。
然后是HTTPS证书。用浏览器访问抓包工具的证书下载页,安装证书。Android 7以上系统默认不信任用户证书,装完用户证书后HTTPS仍然解密不了。常见做法是把证书放到系统证书目录,第5章的排查里会给出具体命令。
提示:抓包环境只影响调试,不影响App本身。不要在生产环境机器上做这套调试,证书信任链会被污染。
3.2 用jadx按关键词反查:从字段名倒追加密入口
抓包拿到字段名之后,就该让jadx上场了。反编译命令很简单:
# 反编译目标APK,输出到output_dir目录 jadx -d output_dir target_app.apk说明:-d指定输出目录,最后一个参数是APK路径。反编译完成后,output_dir下是按包名组织的Java源码。如果只看代码不关心资源,加--no-res可以跳过资源解析,速度快不少;机器核数多的时候,用--threads-count调大并行线程数,也能明显提速。
然后在反编译结果里搜索抓包时看到的参数名。如果请求里有个字段叫x-token,直接搜x-token,命中点通常就是字符串拼接位置,签名算法就在附近。搜不到的时候换URL路径搜,比如/api/poi/list,从请求入口往上找参数构造逻辑。这里踩过的坑是:硬搜参数名搜不到,一搜URL路径立刻定位,因为App里参数名经常被动态拼接,而URL路径通常是完整字面量。
3.3 模拟器联调:把抓包结果和反编译结果对上
抓包和反编译的结果要对上,靠的是模拟器联调。先把设备连接确认一下:
# 查看当前连接的设备 adb devices # 将电脑的8080端口映射到模拟器本地 adb reverse tcp:8080 tcp:8080说明:adb reverse把模拟器里的localhost:8080转发到电脑的8080,适合宿主机和模拟器在同一台机器上的情况。如果转发不生效,回WiFi设置里检查HTTP转发入口是否指向正确的IP和端口。这条命令在部分模拟器上有兼容问题,不生效时不要死磕,直接在WiFi设置里手动填IP。
联调动作:启动App,手动点击一次搜索,立刻回抓包控制台找刚才那条请求,记下URL里的路径特征;再到jadx代码里搜这个路径,找到发请求的代码。这一步的价值在于把“网络侧看到的请求”和“代码侧生成的请求”对齐。对齐之后,你知道该Hook的类名、方法名,也知道它大概在哪个位置被调用,下一步就顺了。
4. Hook签名计算:用Frida把动态参数变成可复用的代码
签名算法在代码里了,但你看不到本次请求用到的值。这一章把Frida环境搭起来,第一次把动态参数打印出来。环境搭好之后,后面所有验证都是在模拟器里点几下屏幕、看几行输出的事情。
4.1 环境准备:模拟器、Frida与目标App的版本匹配
Frida是典型的C/S结构:宿主机装Python工具链,设备上跑一个frida-server。两边的版本必须一致,这是整个流程里最玄学也最频繁出问题的一环。
# 在宿主机安装Frida工具链 pip install frida frida-tools # 将frida-server推送到模拟器并启动 adb push frida-server /data/local/tmp/ adb shell "chmod 755 /data/local/tmp/frida-server" adb shell "/data/local/tmp/frida-server &"说明:frida-server是设备端的驻留服务,chmod 755给执行权限,最后一条命令在模拟器里后台启动。这个终端窗口不要关,关掉服务就停了。验证连接是否正常,在宿主机执行frida-ps -U,能列出设备进程就说明通了。如果报错,先看设备连没连上,再确认frida-server有没有起来,最后检查版本。我的习惯是固定模拟器和PC两端版本,升级任何一边之前都先确认另一边兼容性,省得定位问题定位到深夜。
4.2 定位签名函数:从反编译结果里找到Hook点
从jadx里定位到类似这样的方法:类名com.demo.business.network.SignHelper,方法public static String generateToken(Map params, String secret)。Hook目标就是这个方法。Frida脚本里类名用点分隔,方法名直接写,参数类型要写全。如果方法有多个重载,也就是同名方法参数不同,必须用overload指定具体重载,否则注入阶段就失败。
我一般会先在反编译代码里找到调用点,看业务代码是怎么调用的。比如调用是generateToken(params, "abc"),那么Hook脚本里参数类型就是java.util.Map和java.lang.String。不要凭猜,猜重载大概率翻车。反编译代码里调用点就在业务方法旁边,顺着点进去看比对着源码猜快得多。
4.3 写第一个Frida脚本:把参数和返回值打印出来
第一次写Hook脚本,目标不要定太高,先把参数和返回值打出来就行:
// hook_token.js Java.perform(function () { // 请替换成jadx里定位到的实际类名 var SignHelper = Java.use("com.demo.business.network.SignHelper"); // 单个实现直接赋值;多实现需要用overload区分 SignHelper.generateToken.implementation = function (params, secret) { var result = this.generateToken(params, secret); console.log("params=" + params.toString()); console.log("secret=" + secret); console.log("result=" + result); // 不修改返回值,让App原有逻辑照常执行 return result; }; });运行命令:
# -U连接USB设备,-f冷启动App,-l加载脚本 frida -U -f com.demo.package -l hook_token.js --no-pause说明:-U是连接设备的方式,-f指定要启动的App包名并等待注入,-l指定脚本路径,--no-pause让App启动后不暂停在入口。跑起来之后,手动触发一次App里的搜索,终端就会打印出签名参数。打印出来的params可能不止一条,手动找包含目标接口特征的那条,比如有city_id就是列表请求,有order_id就是详情请求。这个脚本只是观察,不改返回值,App运行逻辑不受影响。
5. 高频问题排查:抓不到包、Hook闪退、复放即被拦
把这条链路完整走一遍,下面几个问题几乎人人都会遇到。每一条按“现象→原因→解决”写清楚,你可以直接对照自己的报错信息找到对应的处理方式。
5.1 HTTPS流量全是Tunnel,抓不到任何请求内容
现象:抓包里只能看到目标域名的连接记录,点开只有Tunnel,看不到请求路径和参数。原因:App检测到证书不受信任,TLS握手失败,抓包工具只能显示建立连接的状态,请求内容没有解密。解决:把证书放进系统证书目录,而不是用户证书目录:
# openssl把证书转成DER格式,文件名需要按Android证书主题的哈希规则命名 openssl x509 -inform PEM -in mitm.pem -outform DER -out 12345678.0 adb root adb remount adb push 12345678.0 /system/etc/security/cacerts/ adb shell chmod 644 /system/etc/security/cacerts/12345678.0 adb reboot说明:12345678.0是示意文件名,实际要把证书主题算出的哈希值作为文件名,否则系统不加载。adb root和adb remount是为了拿到可写系统分区权限,这两个命令在部分模拟器上不支持,那就换一个可写系统分区的模拟器镜像,别在权限上死磕。
5.2 Frida注入后App闪退
现象:运行frida命令后,App启动就闪退;或者注入后没有任何输出,过几秒进程消失。原因:最常见的是frida-server与宿主机版本不一致,其次是方法签名写错导致注入异常,少数情况是App自身带调试检测。解决:先用空脚本验证连接是否正常:
// empty.js Java.perform(function () { console.log("frida inject ok"); });如果空脚本也闪退,问题在环境:版本不匹配、root权限不足、包名错误。如果空脚本正常、换成正式脚本才闪退,就是方法签名错了,回jadx里核对参数类型是不是java.util.Map和java.lang.String,或者是不是有多个重载没区分。大多数“秒退”其实是签名错误,不一定是反调试。
5.3 浏览器里能打开的接口,脚本复放却被拦截
现象:在抓包工具里手动改参数能正常返回,用Python脚本发送同样的请求却被拦,响应里出现验证码或风控提示。原因:脚本的请求头太“干净”,App真实请求里带的User-Agent、Accept、Cookie等,Python默认请求头都没有,服务端很容易识别这不是客户端发出的。解决:先把抓包里那条成功请求的请求头完整照抄进代码,再逐步精简,找出哪几个是必须的。同时控制频率,用随机间隔而不是固定sleep。我一般会在每次请求后等待2到5秒之间的随机值,这个优先级比签名还高。签名算对了但节奏不对,照样被断。
5.4 反编译结果里搜不到目标参数名
现象:jadx反编译结果里搜token、sign,搜不到有效业务代码。原因:字符串被拆开拼接过,或者做了混淆,字段名在代码里不以完整形式出现。解决:搜URL路径特征,比如/api/poi/list,定位到请求入口后再往上找参数组装逻辑。也可以搜算法常量,比如MD5、SHA1,从算法类反查封装位置。搜索粒度要从字段名切换到路径特征,这是我在这条路上换过的最有效的一个思路。
6. 把签名模块搬进Python:从手工验证到批量复跑的采集脚本
走完前面几步,你手里已经有一份能看懂的签名逻辑和一次真实请求的完整参数。最后要做的是把手工验证变成代码里的可复用模块。最常见做法是让签名函数独立成一个模块,采集主流程里只负责传参:
# sign_client.py:签名逻辑独立成模块,方便采集脚本复用 import hashlib def generate_token(params: dict, secret: str) -> str: # 拼接顺序必须和App端一致,错了签名就失效 raw = "" for key in sorted(params.keys()): raw += f"{key}={params[key]}&" raw = raw.rstrip("&") + secret return hashlib.md5(raw.encode("utf-8")).hexdigest()主调用侧保持简单,把频率控制和请求头都放在循环外面:
# collect.py:按可控节奏请求,避免固定频率 import time import requests from sign_client import generate_token session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 ..." # 照抄抓包里的UA }) for page in range(1, 101): params = {"page": page, "city_id": "110000"} params["token"] = generate_token(params, "secret_value") resp = session.get("https://api.example.com/poi/list", params=params) print(page, resp.status_code) time.sleep(2 + (page % 3) * 0.5)说明:generate_token保持和App源码一致的拼接顺序,这是最容易错的一环。如果App源码里是先拼时间戳再拼密钥,这里也必须一模一样。请求头里的User-Agent一定要照抄真实抓包结果,不要自己编一个。最后强调一下边界:这套流程只用于学习和自用,采集公开的商户或POI信息时控制频率,不碰个人隐私字段,商用前拿到明确授权。
我在这条路上吃过亏的地方,是只盯着签名算法,忽略了请求头和节奏。签名算对了,请求头缺失,照样返回风控页面;把脚本间隔改成随机值之后,数据才真正能持续跑起来。希望帮到你。
本文还有配套的精品资源,点击获取