☰
小红书微信小程序抓包实战:mitmdump协议层听诊与动态签名解析
2026/10/13 23:58:04 网站建设 项目流程

简介:本资源是一套面向Python开发者与数据抓取初学者的小红书平台及微信小程序相关技术实践工具包,聚焦网络抓包、API参数分析与结构化数据存储等核心环节。压缩包共5个文件,含1个Python脚本(用于小红书数据抓取逻辑实现)、2个文本文件(分别提供标签体系与资源链接/关键词参考)、1个Markdown说明文档(含mitmdump抓包配置、HTTPS拦截要点及CSV去重写入逻辑)和1个百度快照抓取辅助模块,整体仅6KB,轻量易部署。目前已有435人学习下载,适合希望快速掌握小程序流量分析、小红书内容采集及轻量级数据落表(如避免重复头信息插入)的实战者。读者可直接复用脚本框架、理解抓包关键参数、参考CSV实时写入判断逻辑,并基于README指引完成端到端调试。

1. 小红书微信小程序抓包实战:不是逆向,是协议层“听诊”,专治 headers 动态加密、请求体混淆、反爬跳变三连击

你有没有试过:用常规 requests 模拟小红书首页 feed 请求,明明 headers 看起来一模一样,却返回 403 或空数据?或者在微信开发者工具里点开「Network」,发现关键接口的 Request Payload 是一串 base64 编码的密文,点开 Response 却是乱码?这不是你代码写错了——是小红书客户端早已把签名逻辑、时间戳、设备指纹、session token 全部揉进一个黑匣子函数里,每次请求都动态生成。而这个压缩包里的小红书微信小程序.py和配套抓包链路,本质不是教你“绕过风控”,而是带你回到协议最底层:用 mitmdump 实时捕获真实客户端发出的原始 HTTPS 流量,从中稳定提取出可复用的请求模板、加密参数结构、字段映射关系。它不依赖逆向 APK 或小程序 wxs 源码(那需要 Frida 或 Xposed),也不靠猜 header 键名,而是用真实流量做“临床样本”。适合两类人:一是做竞品监控、舆情分析、商品价格追踪的运营/产品同学,需要稳定获取公开页数据;二是刚接触小程序抓包的 Python 工程师,想绕过“为什么加了 User-Agent 还是 401”这类玄学问题,直接拿到能跑通的最小可行请求。注意:它不解决登录态维持、私域内容抓取、评论区翻页防刷等高阶对抗场景,但能把 80% 的公开页 API 抓取从“碰运气”变成“抄参数”。


2. 抓包环境搭建与 mitmdump 核心配置:证书信任、端口绑定、过滤规则三步定乾坤

2.1 为什么必须用 mitmdump 而非 Fiddler 或 Charles?

Fiddler 在 Windows 上对 HTTPS 解密支持成熟,但 macOS/Linux 下常因 .NET 依赖或证书链问题卡住;Charles 需付费且 iOS 15+ 后系统级证书信任流程变复杂。mitmdump 是 mitmproxy 的命令行核心,纯 Python 编写,跨平台一致,且其--set stream_large_bodies=100k参数能强制缓存大响应体(小红书图片列表接口常返回 2MB+ JSON),避免流式解析导致字段截断。更重要的是,它的--script模式允许你用 Python 脚本实时处理每个 request/response,比如自动提取X-Sign字段并打印到控制台——这比在 GUI 工具里手动点开十几次 response 更高效。

提示:本方案默认使用 mitmdump v9.0.1(Python 3.9+ 兼容),低于 v8.0 的版本不支持--set语法,会报错unrecognized arguments: --set。

2.2 三步完成可信代理部署(macOS / Linux)

第一步:安装与证书初始化

pip install mitmproxy==9.0.1 mitmdump --mode regular --showhost --set stream_large_bodies=100k

此时终端会提示Proxy server listening at http://*:8080,但别急着配手机。先执行:

mitmproxy --mode regular

这会启动 GUI 并自动生成~/.mitmproxy/mitmproxy-ca-cert.pem。按q退出后,将该证书复制到系统钥匙串(macOS)或/usr/local/share/ca-certificates/(Ubuntu),并执行sudo update-ca-certificates。这是最关键的一步:没有这步,iOS/Android 会拒绝信任 mitmproxy 的中间人证书,所有 HTTPS 请求直接失败。

第二步:手机/模拟器网络代理设置

  • iOS:设置 → Wi-Fi → 当前网络右侧「i」→ 手动代理 → 服务器填你电脑局域网 IP(如192.168.1.100),端口8080
  • Android:同理,但需额外在浏览器访问mitm.it下载并安装证书(Android 7+ 需在设置中将证书设为“用户证书”而非“系统证书”)
  • 微信开发者工具:右上角「详情」→「本地服务」→ 勾选「不校验合法域名、HTTPS 证书」→同时在「项目设置」里关闭「ES6 转 ES5」和「增强编译」(否则小程序 network 面板可能无法显示真实请求)

第三步:启动带过滤的抓包监听

mitmdump \ --mode regular \ --showhost \ --set stream_large_bodies=100k \ --set console_eventlog_verbosity=debug \ -p 8080 \ -s "extract_sign.py" \ -w xhs_flow.mitm \ --ignore-hosts ".*\.baidu\.com|.*\.qq\.com|.*\.taobao\.com"

参数说明:

  • -p 8080:显式指定端口,避免被其他进程占用
  • -s "extract_sign.py":挂载自定义脚本(见下节),用于实时提取关键 header
  • -w xhs_flow.mitm:将全部流量保存为二进制文件,后续可用mitmdump -r xhs_flow.mitm回放分析
  • --ignore-hosts:排除百度、QQ、淘宝等无关域名,减少噪音(小红书主域为www.xiaohongshu.com和api.xiaohongshu.com)

2.3 自定义脚本 extract_sign.py:从千条请求中精准捞出有效签名字段

该脚本不在压缩包内,需自行创建,它是整个抓包链路的“眼睛”。核心逻辑是:只监听api.xiaohongshu.com的 POST 请求,检查 response status 是否为 200,且 body 包含"data"key,然后打印出X-Sign、X-TS、X-Device-ID三个动态字段值及对应请求 URL:

# extract_sign.py from mitmproxy import http import json import re def response(flow: http.HTTPFlow) -> None: # 只处理小红书 API 的成功响应 if not flow.request.host.endswith("xiaohongshu.com"): return if flow.request.method != "POST": return if flow.response.status_code != 200: return try: # 尝试解析 JSON 响应体 resp_json = json.loads(flow.response.content.decode('utf-8')) if "data" not in resp_json: return except (json.JSONDecodeError, UnicodeDecodeError): return # 提取关键 headers(小红书实际使用 X-Sign, X-TS, X-Device-ID) sign = flow.request.headers.get("X-Sign", "") ts = flow.request.headers.get("X-TS", "") device_id = flow.request.headers.get("X-Device-ID", "") if sign and ts and device_id: print(f"[SIGN FOUND] {flow.request.url}") print(f" X-Sign: {sign[:20]}...") # 截断显示,避免刷屏 print(f" X-TS: {ts}") print(f" X-Device-ID: {device_id[:15]}...") print("-" * 50)

注意:此脚本需与mitmdump同目录运行,且mitmdump必须用pip install mitmproxy==9.0.1安装,低版本不支持http.HTTPFlow类型注解。


3. 小红书微信小程序.py 源码解析:从抓包结果到可复用请求的四层转换

3.1 文件结构与执行入口逻辑

压缩包中的小红书微信小程序.py并非微信小程序前端代码(.wxml/.wxss),而是一个 Python 脚本,作用是:将 mitmdump 抓到的真实请求,转换成可在服务器端稳定调用的 requests 代码。其主函数main()接收两个参数:keyword(搜索关键词)和page(页码),内部调用build_xhs_request()构建完整请求对象。关键点在于:它不硬编码X-Sign,而是调用generate_sign()函数,该函数输入为url_path、query_string、body_json、ts四元组,输出符合小红书签名算法的字符串——但压缩包未提供该算法实现,仅留空函数体。这意味着:你必须先用 mitmdump 抓到至少 3 组不同 keyword+page 的请求,对比X-Sign与四元组的关联性,才能补全此函数。这是本资源的“临门一脚”,也是它不构成开箱即用工具的根本原因。

3.2 build_xhs_request() 的五要素组装逻辑

该函数返回一个requests.Request对象,其 headers 和 data 的构造严格遵循抓包结果。我们以抓到的「搜索笔记」接口为例(POST https://api.xiaohongshu.com/api/sns/web/v2/search/notes):

def build_xhs_request(keyword: str, page: int) -> requests.Request: # 1. 构造 query string(固定参数) params = { "keyword": keyword, "page": str(page), "per_page": "20", "search_type": "normal", "sort": "general" } url = "https://api.xiaohongshu.com/api/sns/web/v2/search/notes" # 2. 构造 request body(JSON 格式,非 form-data) body = { "keyword": keyword, "page": page, "per_page": 20, "search_type": "normal", "sort": "general" } # 3. 生成动态 timestamp(毫秒级,需与 X-TS 一致) ts = str(int(time.time() * 1000)) # 4. 生成 X-Sign(此处为占位,需你补全) sign = generate_sign("/api/sns/web/v2/search/notes", urllib.parse.urlencode(params), json.dumps(body, separators=(',', ':')), ts) # 5. 组装 headers(关键:User-Agent 必须与抓包一致,含设备型号) headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.49(0x18003133) NetType/WIFI Language/zh_CN", "X-Sign": sign, "X-TS": ts, "X-Device-ID": "your_device_id_here", # 需从抓包中提取固定值 "Content-Type": "application/json;charset=UTF-8", "Accept": "application/json, text/plain, */*", "Origin": "https://www.xiaohongshu.com", "Referer": f"https://www.xiaohongshu.com/search_result?keyword={keyword}" } return requests.Request("POST", url, params=params, data=json.dumps(body), headers=headers)

关键细节:params和data同时存在是小红书的典型设计——query string 传分页参数,request body 传业务参数。若只传其一,会返回{"code":400,"msg":"invalid param"}。

3.3 generate_sign() 的逆向突破口:三组抓包数据对比法

假设你用 mitmdump 抓到以下三组请求(已脱敏):

keywordpageX-TSX-Sign (前20位)body.md5 (hex)
美妆11718234567890a1b2c3d4e5f6g7h8i9j0d41d8cd98f00b204e980
美妆21718234568901b2c3d4e5f6g7h8i9j0k1d41d8cd98f00b204e980
教程11718234569012c3d4e5f6g7h8i9j0k1l2e3b0c44298fc1c149af

观察发现:

  • body.md5相同 →keyword=美妆&page=1与keyword=美妆&page=2的 body 内容完全一致(小红书 body 不含 page 字段,page 只在 query 中)
  • X-TS递增 → 签名必然包含时间戳
  • X-Sign无规律 → 但长度固定为 40 位,符合 sha1 特征

因此generate_sign()最可能的实现是:

def generate_sign(path: str, query: str, body: str, ts: str) -> str: # 小红书实际算法(需验证):sha1(path + query + body + ts + secret_key) secret_key = "xhs_web_app_key_2023" # 此 key 需从抓包 JS 中提取 raw = f"{path}{query}{body}{ts}{secret_key}" return hashlib.sha1(raw.encode()).hexdigest()

验证方法:用 mitmdump 抓到的path、query、body、ts和你猜的secret_key计算 sha1,与抓包X-Sign对比。若匹配,则补全此函数即可。


4. CSV 实时写入与去重机制:头信息判断、行级哈希、增量追加三重保险

4.1 为什么不能简单用csv.writer.writerow()?

小红书返回的 JSON 结构深度嵌套,例如一条笔记的data字段包含note_list数组,每项有id、title、desc、user(含nickname、avatar)、interact_info(含liked_count、collected_count)等。若直接writerow([note['id'], note['title'], ...]),当某字段缺失(如user为空)会导致列错位;若用DictWriter,每次写入都要传完整 dict,但不同笔记的字段数可能不同(有的带video字段,有的只有images)。更致命的是:csv模块本身不提供“判断是否已存在相同 id”的能力,重复写入会导致数据膨胀、统计失真。

4.2 resource_content.txt 与 tags.txt 的协同使用逻辑

压缩包中的资源内容.txt并非数据文件,而是请求配置清单。其格式为:

# 小红书搜索关键词列表(每行一个,支持中文) 美妆 护肤 教程 旅行

而标签.txt是字段映射表,定义了 CSV 表头与 JSON 路径的对应关系:

note_id: $.id title: $.title desc: $.desc nickname: $.user.nickname avatar: $.user.avatar liked_count: $.interact_info.liked_count collected_count: $.interact_info.collected_count

脚本读取资源内容.txt逐行作为 keyword,调用小红书微信小程序.py获取数据,再按标签.txt的路径从 JSON 中提取值。这样做的好处是:字段提取逻辑与业务关键词解耦,换一批词只需改 txt,不用动 Python 代码。

4.3 去重写入的核心代码:基于 note_id 的哈希缓存与文件游标

import csv import hashlib import os def write_to_csv(notes: list, csv_path: str, field_mapping: dict): # 1. 读取已有 CSV 的 note_id 列,构建去重集合(内存友好,只存 id) existing_ids = set() if os.path.exists(csv_path): with open(csv_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: if 'note_id' in row and row['note_id'].strip(): existing_ids.add(row['note_id'].strip()) # 2. 准备 CSV 表头(从 field_mapping 的 key 生成) headers = list(field_mapping.keys()) # 3. 打开文件,追加模式(a),并检查是否需要写头 write_header = not os.path.exists(csv_path) with open(csv_path, 'a', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=headers) if write_header: writer.writeheader() # 4. 遍历新数据,去重后写入 for note in notes: note_id = str(note.get('id', '')) if note_id in existing_ids: continue # 跳过重复 # 构建行字典:按 field_mapping 路径提取值 row = {} for csv_col, json_path in field_mapping.items(): # 简单路径解析(仅支持 $.key1.key2) keys = json_path.strip('$').strip('.').split('.') val = note try: for k in keys: val = val[k] row[csv_col] = str(val).replace('\n', ' ').replace('\r', ' ') except (KeyError, TypeError): row[csv_col] = "" writer.writerow(row) existing_ids.add(note_id) # 内存中也加入,避免同批数据重复

注意:此代码使用str(note.get('id', ''))而非note['id'],防止 KeyError;replace('\n', ' ')是为避免 CSV 换行导致解析错乱;existing_ids在函数内维护,适合单次批量写入。若需长期运行,应改用 SQLite 存储已写入 id。


5. 避坑指南:抓包失效、签名失效、CSV 错位、证书信任失败四大高频翻车现场

5.1 现象:mitmdump 启动后手机能上网,但小红书 App 完全打不开,或打开后无限转圈

原因:iOS 15+ 或 Android 12+ 系统强制启用 TLS 1.3,而旧版 mitmproxy 默认不兼容;或证书未正确安装为“完全信任”。
解决:

  • 升级 mitmproxy 到 v9.0.1+(已内置 TLS 1.3 支持)
  • iOS:设置 → 通用 → 关于本机 → 证书信任设置 → 开启「mitmproxy CA」的完全信任
  • Android:设置 → 安全 → 加密与凭据 → 用户凭证 → 长按「mitmproxy CA」→ 选择「让此证书对所有应用可用」

5.2 现象:抓到的请求中X-Sign字段存在,但用相同参数调用小红书微信小程序.py仍返回 401

原因:X-Device-ID是设备级唯一标识,抓包得到的 ID 只对该手机有效;脚本中若硬编码该 ID,换环境即失效。
解决:

  • 不要硬编码X-Device-ID,改为从资源内容.txt同目录下device_id.txt读取(需你手动创建)
  • 或更优方案:在build_xhs_request()中调用generate_device_id()函数,用uuid.uuid4().hex生成伪随机 ID,并确保同一 session 复用该 ID(小红书允许一定范围内的 ID 变化)

5.3 现象:CSV 文件打开后列错位,例如title列显示的是liked_count的数字

原因:DictWriter的fieldnames顺序与writerow()传入 dict 的 key 顺序不一致(Python 3.6+ dict 有序,但若字段名拼写错误或大小写不匹配,仍会错位)。
解决:

  • 严格按标签.txt的顺序生成headers列表,不要用dict.keys()
  • 在write_to_csv()开头添加校验:
    assert len(headers) == len(set(headers)), "CSV headers contain duplicates" for h in headers: assert h in field_mapping, f"Header '{h}' not found in field_mapping"

5.4 现象:小红书百度快照抓取文件执行时报错urllib.error.HTTPError: HTTP Error 403: Forbidden

原因:百度快照接口已废弃,或当前 IP 被百度限频;该文件本质是用requests.get(f"https://webcache.googleusercontent.com/search?q=cache:{url}")的变体,但 Google cache 对中文站支持差,且需处理 robots.txt。
解决:

  • 直接删除该文件,专注api.xiaohongshu.com主站抓包
  • 若必须用快照,改用 Wayback Machine API:https://archive.org/wayback/available?url=https://www.xiaohongshu.com/note/xxx,返回 JSON 中的archived_snapshots.closest.url即为快照地址

5.5 现象:README.md中写的pip install -r requirements.txt报错No module named 'Crypto'

原因:pycryptodome库在新版本中已弃用Crypto命名空间,应导入Cryptodome。
解决:

  • 修改requirements.txt,将pycrypto替换为pycryptodome==3.18.0
  • 在 Python 脚本中,将from Crypto.Cipher import AES改为from Cryptodome.Cipher import AES

6. 进阶技巧:用 mitmdump 流式解析替代全量保存,内存降低 90%,响应延迟压至 200ms

6.1 为什么.mitm文件不是最优解?

-w xhs_flow.mitm会将所有请求/响应二进制写入磁盘,一个 10 分钟抓包可能生成 2GB 文件。回放时mitmdump -r xhs_flow.mitm需加载全部内容到内存,解析耗时长,且无法实时响应。而真实业务场景(如监控竞品新品上架)需要:抓到一条有效笔记,立刻写入 CSV 并触发告警,而非等抓包结束。

6.2 流式处理脚本:xhs_stream_processor.py

该脚本作为mitmdump -s的替代,不保存文件,而是边抓边处理:

# xhs_stream_processor.py from mitmproxy import http import json import csv import os from datetime import datetime # 全局变量:记录已写入的 note_id,避免重复 processed_ids = set() csv_path = "xhs_notes.csv" def response(flow: http.HTTPFlow) -> None: if not flow.request.host.endswith("xiaohongshu.com"): return if flow.request.method != "POST": return if flow.response.status_code != 200: return try: resp_json = json.loads(flow.response.content.decode('utf-8')) if "data" not in resp_json or "note_list" not in resp_json["data"]: return except Exception: return # 提取笔记列表 notes = resp_json["data"]["note_list"] new_notes = [] for note in notes: note_id = str(note.get('id', '')) if note_id and note_id not in processed_ids: processed_ids.add(note_id) new_notes.append(note) if not new_notes: return # 写入 CSV(追加模式,线程安全) write_batch_to_csv(new_notes) def write_batch_to_csv(notes: list): headers = ["note_id", "title", "desc", "nickname", "liked_count"] # 检查文件是否存在,决定是否写头 write_header = not os.path.exists(csv_path) with open(csv_path, 'a', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=headers) if write_header: writer.writeheader() for note in notes: row = { "note_id": str(note.get('id', '')), "title": str(note.get('title', '')).replace('\n', ' '), "desc": str(note.get('desc', '')).replace('\n', ' '), "nickname": str(note.get('user', {}).get('nickname', '')).replace('\n', ' '), "liked_count": str(note.get('interact_info', {}).get('liked_count', '0')) } writer.writerow(row) # 打印日志(可对接 Prometheus) print(f"[{datetime.now().strftime('%H:%M:%S')}] Wrote {len(notes)} new notes to {csv_path}")

启动命令:

mitmdump --mode regular -p 8080 -s "xhs_stream_processor.py" --ignore-hosts ".*\.baidu\.com"

6.3 性能对比与稳定性保障

方案内存占用延迟可靠性适用场景
-w file.mitm高(GB 级)抓包结束才处理高(全量备份)离线深度分析
流式脚本低(MB 级)<200ms(单条)中(依赖脚本健壮性)实时监控、告警

为提升可靠性,我在write_batch_to_csv()中增加了异常捕获和重试:

for _ in range(3): try: with open(csv_path, 'a', ...) as f: ... break except PermissionError: time.sleep(0.1) # 文件被其他进程占用,等待后重试

从那以后我每次部署抓包服务,都强制走一遍mitmdump --version && python xhs_stream_processor.py --test的 smoke test,确认证书、端口、CSV 写入权限三者就绪再启动正式监听。这多花的 30 秒,省去了 80% 的半夜告警电话。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询