简介这份PDF资料面向具备Python基础、对网络请求与爬虫开发感兴趣的技术爱好者与研发工程师围绕手机APP数据传输的抓取与解析展开重点讲解如何借助Fiddler抓包定位登录与话题接口再通过urllib2构造HTTP请求、携带Header与表单数据模拟客户端登录并对返回的JSON数据进行解析提取实现动态获取APP内用户发布话题信息。资源包共1个PDF文件大小约143KB内容紧凑涵盖抓包分析、登录代码、话题抓取与下拉加载等完整流程代码基于Python2.7并经过实际测试。已有366人学习适合希望掌握定制化信息采集系统设计、从数据捕获到解析处理全流程的读者参考同时提醒注意遵守相关法律法规与平台服务条款。1. 手机 APP 数据抓取为什么你的爬虫一上线就翻车做过 Web 爬虫的人第一次碰手机 APP 抓包大概率会经历同一个心理落差浏览器里 F12 一按请求响应看得清清楚楚换成 APP同样的接口却怎么都调不通。这不是玄学而是移动端的数据传输链路和 Web 有本质区别——APP 走的是私有协议封装、证书绑定、参数签名、设备指纹校验这一整套组合拳。标题里说的「Python 爬虫技术应用于手机 APP 传输数据的抓取与解析」核心要解决的就是三件事把 APP 发出的请求完整捕获下来、把加密或编码过的请求体还原成可读结构、用 Python 把整条链路自动化跑起来。适合谁看有 Python 基础、做过 requests 或 Scrapy、现在需要把数据源从网页扩展到移动端的开发者。下面按「抓得到 → 解得开 → 跑得稳」的顺序拆。2. 抓包环境怎么搭从手机流量到 Python 可读的请求2.1 三种抓包路径的选型对比移动端抓包不是只有一条路选错了后面全是坑。常见做法有三类代理抓包、模拟器内抓包、真机配合中间人证书。三者的适用场景差别很大先看对比。方式适用场景优点主要限制代理抓包真机代理工具大部分 HTTP/HTTPS 明文或可解密流量贴近真实设备环境需处理证书信任部分 APP 有证书绑定模拟器内抓包快速验证、批量复现环境可控、易重置部分 APP 检测模拟器环境直接拒绝服务真机中间人证书需要长期稳定采集流量完整证书绑定、双向校验会拦截我一般会先用代理抓包快速看一遍流量结构确认是明文还是加密再决定要不要上更重的方案。如果 APP 做了证书绑定SSL Pinning代理工具会直接报证书错误这时候要么换模拟器环境要么在合规前提下分析其校验逻辑——注意任何绕过安全机制的操作都必须在法律和授权范围内进行本文只讨论技术原理与合法采集场景。2.2 用 Python 起一个本地代理做流量镜像比起依赖图形化工具用 Python 自己起一个轻量代理能把抓包和后续解析串成一条流水线。下面是一个基于 mitmproxy 的最小可运行脚本把经过的请求落成 JSON 行方便后续用 pandas 处理。# proxy_capture.py # 依赖: pip install mitmproxy from mitmproxy import http import json import datetime LOG_FILE capture.jsonl def request(flow: http.HTTPFlow) - None: 拦截请求阶段记录 URL、方法、请求头、请求体 record { ts: datetime.datetime.now().isoformat(), method: flow.request.method, url: flow.request.pretty_url, headers: dict(flow.request.headers), # 请求体可能是 bytes转成可读字符串 body: flow.request.get_text(strictFalse), } with open(LOG_FILE, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n) def response(flow: http.HTTPFlow) - None: 拦截响应阶段记录状态码和响应体前 2000 字符 record { ts: datetime.datetime.now().isoformat(), url: flow.request.pretty_url, status: flow.response.status_code, resp_preview: flow.response.get_text(strictFalse)[:2000], } with open(LOG_FILE, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n)逻辑说明request钩子在请求发出前触发response在响应回来后触发两者都往同一个 jsonl 文件追加。参数上get_text(strictFalse)是关键——APP 的请求体经常是二进制或非 UTF-8 编码strictTrue会直接抛异常中断抓包设成 False 能容错。resp_preview只截前 2000 字符避免大文件把日志撑爆。启动命令mitmdump -s proxy_capture.py -p 8080手机端把代理指向这台机器的 8080 端口装好 mitmproxy 的 CA 证书并信任流量就会落到capture.jsonl。这一步跑通说明链路是通的接下来才是真正的难点解析。2.3 请求体编码的识别与还原抓到的 body 经常长这样a1bxxx是幸运的更多时候是一串 Base64、一段 gzip 压缩、或者一个带签名的 JSON。识别顺序建议是先看 Content-Type再看 body 前几个字节的魔数最后才尝试解码。# decode_body.py import base64 import gzip import json def sniff_and_decode(raw: str) - dict: 按常见编码顺序尝试还原请求体 # 1. 直接是 JSON try: return {type: json, data: json.loads(raw)} except Exception: pass # 2. Base64 解码后再试 JSON try: decoded base64.b64decode(raw).decode(utf-8) return {type: base64json, data: json.loads(decoded)} except Exception: pass # 3. gzip 压缩Base64 包裹 try: decoded base64.b64decode(raw) text gzip.decompress(decoded).decode(utf-8) return {type: base64gzipjson, data: json.loads(text)} except Exception: pass return {type: unknown, data: raw}逻辑说明这个函数按「明文 JSON → Base64 → Base64gzip」的优先级逐层尝试每层失败就往下走。参数上base64.b64decode对非法字符会抛异常正好用来做判断。实际项目里还会遇到 AES 加密的 body那就需要先定位密钥这属于更深的逆向范畴不在本文展开但识别思路是一样的先确定编码层次再逐层剥。3. 参数签名与请求还原让 Python 请求骗过服务端校验3.1 签名参数的常见构成APP 接口几乎不会让你裸调。最常见的防护是签名sign服务端拿同样的算法算一遍对不上就拒绝。签名的原料通常是请求参数按字典序拼接 时间戳 随机数 一个固定盐值再做 MD5 或 HMAC。抓包时你会看到请求里多了一个sign字段但不知道它怎么来的。定位签名逻辑的常规路径是把 APP 的安装包解出来找到计算 sign 的那段代码或者用动态调试跟踪。这里必须强调逆向分析他人 APP 涉及法律边界仅可在获得授权或分析自有应用时进行。对于合法采集场景更稳妥的做法是直接复用抓到的签名——如果签名不随时间变化或者变化周期很长可以缓存复用。3.2 用 Python 复现一个典型签名假设通过合法途径确认了签名算法是「参数排序拼接 盐值 MD5」Python 复现如下# sign_demo.py import hashlib import time import random SALT example_salt_value # 盐值实际以确认结果为准 def build_sign(params: dict) - str: 按 key 字典序拼接参数追加盐值后做 MD5 # 过滤掉 sign 本身和空值 items sorted( (k, v) for k, v in params.items() if k ! sign and v not in (None, ) ) raw .join(f{k}{v} for k, v in items) raw fsalt{SALT} return hashlib.md5(raw.encode(utf-8)).hexdigest() def build_request_params(base: dict) - dict: 补全时间戳、随机数并计算签名 params dict(base) params[timestamp] int(time.time() * 1000) params[nonce] random.randint(100000, 999999) params[sign] build_sign(params) return params if __name__ __main__: p build_request_params({page: 1, size: 20, type: feed}) print(p)逻辑说明build_sign先按 key 排序保证拼接顺序和服务端一致这是最容易翻车的地方——顺序错一位MD5 结果完全不同。timestamp用毫秒级nonce用六位随机数这两个字段通常也参与签名所以必须在算 sign 之前就填好。参数上SALT是核心机密实际值需要通过合法分析获得这里用占位符。3.3 把签名接进 requests 会话签名算对了还要处理 Cookie、User-Agent、设备标识这些请求头否则服务端照样拒绝。# session_client.py import requests from sign_demo import build_request_params BASE_URL https://example-api.invalid/app/v1/feed session requests.Session() session.headers.update({ User-Agent: ExampleApp/1.0 (Android 12; Pixel), Content-Type: application/json, X-Device-Id: fixed-device-id-for-test, }) def fetch_feed(page: int 1) - dict: params build_request_params({page: page, size: 20}) resp session.get(BASE_URL, paramsparams, timeout10) resp.raise_for_status() return resp.json() if __name__ __main__: data fetch_feed(1) print(data)逻辑说明用Session保持 Cookie 和连接复用X-Device-Id这类设备标识如果服务端校验就必须固定成抓包时看到的值。参数上timeout10防止请求卡死raise_for_status让非 200 直接抛异常便于定位问题。注意 URL 用了.invalid域名这是保留域名不会真实解析替换成你自己的合法目标即可。4. 数据解析与落库从响应 JSON 到结构化表4.1 响应结构的容错解析APP 的响应经常嵌套很深而且字段可能缺失。直接data[list][0][title]这种写法一旦某个字段没有就整个脚本崩掉。稳妥做法是用.get()链式取值或者上jsonpath。# parse_resp.py from typing import Any def safe_get(obj: Any, path: str, defaultNone): 按 a.b.c 路径安全取值任一层缺失返回 default cur obj for key in path.split(.): if isinstance(cur, dict): cur cur.get(key, default) elif isinstance(cur, list) and key.isdigit(): idx int(key) cur cur[idx] if idx len(cur) else default else: return default if cur is default: return default return cur def extract_items(resp: dict) - list: 从响应中抽取列表项兼容多种嵌套结构 raw safe_get(resp, data.list) or safe_get(resp, data.items) or [] items [] for it in raw: items.append({ id: safe_get(it, id), title: safe_get(it, title, ), author: safe_get(it, author.name, ), ts: safe_get(it, create_time), }) return items逻辑说明safe_get支持字典和列表两种容器路径里数字表示列表下标。extract_items用or兜底兼容服务端可能返回的两种字段名。参数上default保证 title 缺失时是空串而不是 None方便后续入库。4.2 落库与去重解析出来的数据要落库最怕重复。用 SQLite 做本地存储时给业务主键加唯一索引配合INSERT OR IGNORE就能天然去重。# store.py import sqlite3 def init_db(path: str app_data.db) - sqlite3.Connection: conn sqlite3.connect(path) conn.execute( CREATE TABLE IF NOT EXISTS feed ( id TEXT PRIMARY KEY, title TEXT, author TEXT, ts INTEGER, fetched_at INTEGER DEFAULT (strftime(%s,now)) ) ) conn.commit() return conn def save_items(conn: sqlite3.Connection, items: list) - int: cur conn.executemany( INSERT OR IGNORE INTO feed (id, title, author, ts) VALUES (?,?,?,?), [(i[id], i[title], i[author], i[ts]) for i in items] ) conn.commit() return cur.rowcount # 实际插入的行数逻辑说明id设为主键INSERT OR IGNORE遇到重复主键直接跳过rowcount返回真正写入的行数可以用来监控增量。参数上fetched_at用 SQLite 内置函数自动填当前时间戳省去 Python 侧处理。5. 避坑与排查那些让脚本半夜挂掉的细节5.1 现象抓包全是 CONNECT看不到明文原因APP 走了 HTTPS代理工具只看到隧道建立没解密。解决确认手机已安装并信任代理 CA 证书且证书装在「系统级」而非仅浏览器级部分 APP 只信任系统证书需要把证书装到系统分区需 root 或使用模拟器。5.2 现象签名怎么算都对不上原因拼接顺序、是否包含空值、时间戳精度秒 vs 毫秒、大小写MD5 输出 hex 还是大写任一不同都会导致失败。解决把抓到的原始参数和你的拼接结果逐字符对比重点看排序规则和分隔符用同一组参数在 APP 里触发一次抓下它的 sign反推算法。5.3 现象跑几十次后接口返回 403 或空数据原因触发风控。常见触发点是请求频率过高、设备标识固定不变、缺少必要的埋点参数。解决加随机延时1~3 秒、轮换 User-Agent、补全抓包时看到的所有请求头如果服务端有滑块或验证码说明已进入强风控区应降低频率或停止。5.4 现象响应体是乱码或解析报错原因响应被 gzip/br 压缩或编码不是 UTF-8。解决requests 一般会自动解压但自定义 header 时可能覆盖Accept-Encoding显式设置resp.encoding或对resp.content手动解压。5.5 现象脚本在本地跑通部署到服务器就失败原因服务器 IP 被目标服务端标记或服务器缺少证书链。解决检查服务器出口 IP 是否被限制确认 CA 证书路径正确用curl -v对比本地和服务器的 TLS 握手差异。6. 进阶把抓取链路做成可维护的采集管道单次抓取能跑通只是起点真正要长期用得把「抓包 → 解析 → 签名 → 请求 → 落库」串成一条可观测的管道。我一般会加三个东西结构化日志、失败重试、增量水位线。结构化日志用logging输出 JSON方便后续用 ELK 或简单 grep 排查# pipeline_log.py import logging import json class JsonFormatter(logging.Formatter): def format(self, record): return json.dumps({ level: record.levelname, msg: record.getMessage(), ts: self.formatTime(record), }, ensure_asciiFalse) logger logging.getLogger(pipeline) handler logging.StreamHandler() handler.setFormatter(JsonFormatter()) logger.addHandler(handler) logger.setLevel(logging.INFO)失败重试用tenacity对网络类异常做指数退避# retry_demo.py from tenacity import retry, stop_after_attempt, wait_exponential import requests retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min1, max10)) def robust_get(url, params): resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.json()参数说明stop_after_attempt(3)最多试 3 次wait_exponential让等待时间按 1、2、4 秒递增避免密集重试加剧风控。增量水位线则是把每次抓到的最大时间戳存下来下次从该时间戳之后开始拉既省流量又避免重复。验证管道是否健康我习惯看三个指标单次抓取的成功率、去重后的新增条数、平均响应耗时。成功率低于 90% 就要查风控新增条数突然归零要查接口是否改版耗时飙升要查网络或服务端限流。这套东西搭起来之后换一个 APP 只需要替换签名模块和解析规则主干不用动。最后说个血泪经验别在没确认授权的情况下对任何 APP 下手技术能力越强越要守住边界。我早期图快直接拿一个没授权的接口练手结果不仅白干还惹了麻烦。现在我的习惯是动手前先确认数据归属和采集许可再谈技术方案。希望帮到你。本文还有配套的精品资源点击获取