1. Mitmproxy 基础安装与报错解决

1.1 安装命令

在准备好 Python 运行环境的电脑上,直接使用 pip 安装:

pip install mitmproxy

1.2 首次运行与文件生成

在终端运行以下命令以初始化并生成根证书:

mitmdump

运行后应当看到如下输出,代表代理服务和 Web 服务已正常启动:

Web server listening at http://127.0.0.1:8081/
Proxy server listening at http://*:8080

首次运行后,mitmproxy 会在用户家目录的隐藏文件夹中自动生成一套证书:

  • Linux/WSL 路径/home/您的用户名/.mitmproxy/
  • Windows 路径C:\Users\您的用户名\.mitmproxy\
    (注:如果是在 Win10 Ubuntu 子系统 WSL 下,其实际 Windows 映射路径通常为 C:\Users\您的用户名\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu...[哈希值]\LocalState\rootfs\home\您的用户名\.mitmproxy)

1.3 证书文件角色说明

在该目录下,您会看到以下文件:

  • mitmproxy-ca.pem:PEM 格式证书私钥。
  • mitmproxy-ca-cert.pem:PEM 格式根证书,适用于大多数 LinuxmacOS 等非 Windows 平台。
  • mitmproxy-ca-cert.cer:PEM 格式证书,内容与 .pem 相同,仅后缀不同,适用于部分 Android 设备。
  • mitmproxy-ca-cert.p12:PKCS12 格式证书,适用于 Windows 平台。
  • mitmproxy-ca.p12:PKCS12 格式证书私钥。
  • mitmproxy-dhparam.pem:PEM 格式秘钥文件,用于增强 SSL 安全性。

1.4 常见初始化报错及解决

在一些 Linux 环境(或高版本 Python)中运行 mitmdump 可能遇到以下报错:

(trapped) error reading bcrypt version
Traceback (most recent call last):
  File ".../passlib/handlers/bcrypt.py" line 620 in _load_backend_mixin
    version = _bcrypt.about.version
AttributeError: module 'bcrypt' has no attribute 'about'

原因:这是由于最新版的 bcrypt 库移除了 about 属性,而 passlib 依赖该属性,导致兼容性冲突。
解决办法:在终端运行以下命令,将 bcrypt 降级到兼容版本即可:

pip install bcrypt==3.2.0

2. 本地证书安装与信任(Windows/Linux)

2.1 Windows 系统安装

双击 .mitmproxy 目录下的 mitmproxy-ca-cert.p12 证书,按照导入向导进行安装,并确保将证书放入**“受信任的根证书颁发机构”**中。

2.2 Linux (Ubuntu / WSL) 信任证书

如果在 Ubuntu 上遇到 .pem 文件,需要先将其转换为系统易识别的 .crt 格式:

openssl x509 -in mitmproxy-ca-cert.pem -inform PEM -out mitmproxy-ca-cert.crt

创建额外的证书存放目录,并将证书拷贝进去:

sudo mkdir /usr/share/ca-certificates/extra
sudo cp mitmproxy-ca-cert.crt /usr/share/ca-certificates/extra/mitmproxy-ca-cert.crt

让 Ubuntu 重新配置和加载新证书:

sudo dpkg-reconfigure ca-certificates

在弹出的交互式安装界面中,使用空格键选中 extra/mitmproxy-ca-cert.crt,然后直接回车即可完成系统受信任 CA 的更新。


3. 手机与安卓模拟器代理配置

要想抓取移动端的数据包,必须将手机与运行 mitmproxy 的电脑连接到**同一个局域网(Wi-Fi)**下。

3.1 配置手机/模拟器代理

打开手机的“设置” $\rightarrow$ “WLAN” $\rightarrow$ 长按当前连接的 Wi-Fi 名字 $\rightarrow$ “修改网络”(或“高级选项”):

  • 代理:选择 手动
  • 服务器主机名:输入运行 mitmproxy 电脑的局域网 IP(例如 192.168.31.139)。
  • 服务器端口:输入 8080mitmproxy 的默认代理监听端口)。
  • 保存

3.2 基础证书安装方法(Android 7.0 以下或部分浏览器环境)

方法 A:通过虚拟网站下载(最简便)

  1. 确保电脑上已启动 mitmdump,且手机已成功配置并连接上述代理。
  2. 打开手机浏览器,在地址栏输入:mitm.it
  3. 页面会识别当前的代理服务,点击对应的 Android 图标,即可下载 mitmproxy-ca-cert.pem.cer 证书。
  4. 下载完成后,到手机设置中进行安装:
    • 常规路径设置 $\rightarrow$ 安全(或凭据) $\rightarrow$ 从存储设备安装证书 $\rightarrow$ 选择刚下载的文件。
    • 部分模拟器/旧设备设置 $\rightarrow$ WLAN $\rightarrow$ 高级设置/偏好设置 $\rightarrow$ 安装证书

4. Android 7.0+ 证书突破限制(Magisk/手写入系统凭据)

Android 7.0 (API 24) 开始,系统默认不再信任用户自行安装的 CA 证书。因此,如果您想抓取现代 App 的 HTTPS 数据包,必须将证书安装到系统的根证书目录(即由“用户凭据”升级为“系统凭据”)。

4.1 方案一:使用 Magisk 模块(最推荐,支持 Android 10+ 及其最新版)

如果您的手机已刷入 Magisk (面具)KernelSU 或 APatch,推荐使用 AlwaysTrustUserCerts 模块。

  1. 下载模块:前往 GitHub 仓库 AlwaysTrustUserCerts Releases 下载最新的模块 .zip 压缩包。
  2. 正常安装用户证书:按照前文 3.2 节 的方法,把 mitmproxy-ca-cert.pem 常规安装到手机中。
  3. 刷入模块:打开手机上的 Magisk App $\rightarrow$ 点击右下角 模块 $\rightarrow$ 点击 从本地安装 $\rightarrow$ 选择下载的模块 .zip 文件并刷入。
  4. 重启生效:刷入成功后,重启手机。
  5. 工作原理:重启时,该模块会自动将所有用户安装的 CA 证书(包含 mitmproxy)挂载并移动到系统证书根目录(/system/etc/security/cacerts/)。您可以在手机系统的信任的凭据 $\rightarrow$ 系统中找到 mitmproxy,此时即可顺利抓取所有 App 的 HTTPS 流量。

4.2 方案二:手动通过 ADB 将证书写入系统根目录(需要 Root)

如果您不想安装 Magisk 模块,也可以通过命令行手动将证书重命名为 Android 识别的 Hash 格式并写入系统。

  1. 准备证书并生成证书的哈希值
    在电脑终端(已配置 OpenSSL)执行以下命令:
    # 1. 转换为 .crt 格式
    openssl x509 -in mitmproxy-ca-cert.pem -inform PEM -out mitmproxy-ca-cert.crt
    
    # 2. 获取老版本系统的 Hash 值(得到类似 269953fb 的 8 位字符串)
    openssl x509 -inform PEM -subject_hash_old -in mitmproxy-ca-cert.crt
    
  2. 重命名证书
    将得到的哈希值拼上 .0 后缀。例如哈希值为 269953fb,则执行:
    mv mitmproxy-ca-cert.crt 269953fb.0
    
  3. 通过 ADB 推送到系统目录
    # 获取 root 权限并解锁系统写保护
    adb root
    adb remount
    
    # 如果提示 "remount of /system failed: Read-only file system",可依次执行:
    # adb disable-verity
    # adb reboot
    # 重启后重新运行 adb root 和 adb remount
    
    # 将证书推送到手机系统证书目录
    adb push 269953fb.0 /sdcard/
    adb shell
    mv /sdcard/269953fb.0 /system/etc/security/cacerts/
    chmod 644 /system/etc/security/cacerts/269953fb.0
    reboot
    
    设备重启后,即可在系统证书列表里看到 mitmproxy 根证书。

5. Python 脚本联动拦截与 Web 端自动化抓取(含 Vivo 健康实战)

mitmproxy 的核心优势在于支持使用 Python 脚本,以可编程的方式实时分析过滤或修改请求和响应。

5.1 Python 处理脚本编写规范

在启动时,我们重写 request(flow) 处理发出的请求包,重写 response(flow) 处理服务器返回的响应包。

下面是一个标准的 Python 处理脚本示例,可打印关键请求项并捕获带有 Cookie 的响应头:

# filename: example_script.py
from mitmproxy import ctx
import re

def write_text(text):
    # 提取 headers 中含有 Cookie 或 Set-Cookie 的段落并保存至文件
    r_list = re.compile(r"\(b'cookie'.*?\)|\(b'Set-Cookie'.*?\)" re.S).findall(text)
    if r_list:
        for content in r_list:
            with open("cookie.txt" "a" encoding="utf-8") as file:
                file.write(content + "\n\n")

def request(flow):
    req = flow.request
    info = ctx.log.info
    # 打印请求的各项基本信息
    info(f"URL: {req.url}")
    info(f"Method: {req.method}")
    info(f"Host: {req.host}")
    info(f"Port: {str(req.port)}")
    info(f"Headers: {str(req.headers)}")
    info(f"Cookies: {str(req.cookies)}")
    
    # 自定义分析请求头保存 Cookie
    write_text(str(req.headers))

def response(flow):
    res = flow.response
    info = ctx.log.info
    info(f"Status Code: {str(res.status_code)}")
    info(f"Headers: {str(res.headers)}")
    info(f"Cookies: {str(res.cookies)}")
    info(f"Body Text: {str(res.text)}")
    
    # 从响应头捕获 Set-Cookie
    write_text(str(res.headers))

运行命令

mitmdump -s example_script.py

5.2 过滤与定位特定内容的响应 (Response)

如果只想打印含有特定关键字(如商品名称字段 "tiledTagName")的响应包,可以像下面这样写:

# filename: get_text.py
def response(flow):
    response = flow.response
    # 过滤包含指定字词的网页响应内容
    if "tiledTagName" in str(response.text):
        print(str(response.text))

纯净输出运行命令-q 参数将屏蔽 mitmproxy 默认的连接日志,仅显示您 print 出来的文字):

mitmdump -s get_text.py -q

5.3 进阶实战:联动 mitmweb 提取并同步睡眠数据

mitmweb 是带网页可视化界面的代理终端。下面是一个高级联动案例,当您使用 mitmweb -s save_health_data.py 启动时,它不仅能充当代理,还能在后台静默拦截 vivo 运动健康的睡眠数据,对其进行深度解析,并同步写到您的私有云笔记中

🐍 自动化抓取并同步脚本 save_health_data.py

# filename: save_health_data.py
import requests
import time
import random
import json
from datetime import datetime timezone timedelta

# ─────────────────────────────────────────────────────────────────────────────
# 笔记同步基础配置
# ─────────────────────────────────────────────────────────────────────────────
BASE_URL = "https://note.yysresume.work"
NOTE_ID = "3141f6e3-8d2c-477b-8a57-9bfb36991c4b"
AUTH_TOKEN = "a_secret_fixed_token"

note_headers = {
    "Cookie": f"auth_token={AUTH_TOKEN}"
    "Content-Type": "application/json"
}

def write_record_with_dict(key data_dict):
    """
    [PATCH] 结构化写入:通过直接传递标准的 Python 字典对象进行多列对齐更新
    """
    url = f"{BASE_URL}/api/note-op"
    payload = {
        "noteId": NOTE_ID
        "date": key
        "data": data_dict  # 直接将 dict 传给 data 参数
        "token": AUTH_TOKEN
    }
    # 使用独立的 note_headers 发送请求,防止与代理 Host 冲突引发 403 错误
    response = requests.patch(url json=payload headers=note_headers)
    if response.status_code == 200:
        print(f"✅ [PATCH 写入成功] 键: '{key}'")
        return True
    print(f"❌ [PATCH 写入失败]: {response.status_code} - {response.text}")
    return False

# ─────────────────────────────────────────────────────────────────────────────
# Mitmproxy 响应拦截与解析
# ─────────────────────────────────────────────────────────────────────────────
def response(flow):
    # 只处理去往 health.vivo.com 的睡眠数据接口
    if "health.vivo.com" in flow.request.pretty_host and "/daily/sleep/original" in flow.request.path:
        try:
            json_data = json.loads(flow.response.text)
            if not json_data.get("data") or not json_data["data"].get("detail"):
                return

            # 获取最新的睡眠记录
            details = json_data["data"]["detail"]
            latest_sleep = max(details key=lambda x: x["total"]["startTime"])
            
            # 时区换算 (GMT+8)
            offset = latest_sleep.get("zoneOffset" 480)
            tz = timezone(timedelta(minutes=offset))
            
            start_dt = datetime.fromtimestamp(latest_sleep["total"]["startTime"] / 1000 tz)
            end_dt = datetime.fromtimestamp(latest_sleep["total"]["endTime"] / 1000 tz)
            now_dt = datetime.fromtimestamp(time.time() tz)
            
            bedtime_str = start_dt.strftime("%H:%M")
            waketime_str = end_dt.strftime("%H:%M")
            
            days_ago_start = (now_dt.date() - start_dt.date()).days
            days_ago_end = (now_dt.date() - end_dt.date()).days
            
            def get_day_label(days_ago is_bedtime=False):
                if days_ago == 0:
                    return "今天凌晨" if is_bedtime else "今晨"
                elif days_ago == 1:
                    return "昨晚" if is_bedtime else "昨日"
                else:
                    return start_dt.strftime("%m-%d")
                    
            bedtime_label = f"{bedtime_str} ({get_day_label(days_ago_start True)})"
            waketime_label = f"{waketime_str} ({get_day_label(days_ago_end False)})"
            
            # 睡眠时长计算
            duration_ms = latest_sleep["total"]["duration"]
            hours = duration_ms // 3600000
            minutes = (duration_ms % 3600000) // 60000
            duration_str = f"{hours} 小时 {minutes} 分钟"
            
            # 深度睡眠时长计算 (累加各分段时长)
            deep_sleep_segments = latest_sleep.get("deepSleep" [])
            deep_sleep_ms = sum(segment.get("duration" 0) for segment in deep_sleep_segments)
            deep_hours = deep_sleep_ms // 3600000
            deep_minutes = (deep_sleep_ms % 3600000) // 60000
            deep_duration_str = f"{deep_hours} 小时 {deep_minutes} 分钟" if deep_hours > 0 else f"{deep_minutes} 分钟"
            
            score = latest_sleep.get("score" 0)

            # 结构化数据组装
            device_data = {
                "入睡时间": bedtime_label
                "醒来时间": waketime_label
                "总睡眠时长": duration_str
                "深度睡眠时长": deep_duration_str
                "睡眠综合得分监测数值": f"{score} 分"
            }
            
            # 自动异步上传至云笔记
            print("\n发现最新睡眠数据,正在同步到您的笔记系统中...")
            sync_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            write_record_with_dict(sync_time device_data)
            
        except Exception as e:
            print(f"[!] 解析或同步健康数据时发生错误: {e}")

启动命令

mitmweb -s save_health_data.py

运行后,当您在手机的“vivo运动健康” App 中下拉刷新睡眠历史时,脚本会自动提取您今天早上的睡眠状态并推送至远程笔记中,并在运行目录本地保存完整的格式化数据。


6. 经典实战案例

6.1 实战案例:JD 农场水滴批量提取与浇水模拟

下面是利用拦截到的水滴接口及其请求参数进行自动化操作的案例。

JD 批量提取 Cookie 配置

使用 mitmproxy 抓取并提取到请求头(Headers)后,可利用 Python 将 Cookie 快速拆分成字典:

cookie_str = "您的抓包Cookie文本"
cookie = {}

for i in cookie_str.split(""):
    item = i.split("=")
    if len(item) == 2:
        cookie["name"] = item[0].strip()
        cookie["value"] = item[1].strip()
        cookie['path'] = "/"
        cookie['domain'] = ".m.jd.com"
        print(f"self.driver.add_cookie(cookie_dict={str(cookie)})")

JD 农场任务自动点击模拟

  1. 领取农场新用户水滴任务

    • 请求 URLhttps://api.m.jd.com/client.action?functionId=gotNewUserTaskForFarm&body=%7B%22version%22%3A10%2C%22channel%22%3A1%7D&appid=wh5
    • 接口成功返回的样例(JSON):其中 addEnergy: 128 表示成功领取水滴 128g。
    {
      "code": "0"
      "farmUserPro": {}
      "totalEnergy": 1730
      "addEnergy": 128
      "sysTime": 1613863094672
      "message": null
    }
    
  2. 农场自动浇水接口

    • 请求 URLhttps://api.m.jd.com/client.action?functionId=waterGoodForFarm&body=%7B%22type%22%3A%22%22%2C%22version%22%3A10%2C%22channel%22%3A1%7D&appid=wh5
    • 接口成功返回的样例(JSON):其中 totalEnergy: 109 代表浇水后账户剩余的水滴总数。
    {
      "code": "0"
      "totalEnergy": 109
      "totalWaterTimes": 21
      "sysTime": 1613863291160
      "waterStatus": 0
      "sendAmount": 10
      "treeEnergy": 1750
    }
    

当您掌握了接口的 URL 规则与有效的 token 后,就可以使用 Python 的 requests.get 进行模拟发送,从而摆脱手动操作。