Skip to content
Advanced / 4 min read

如何在 macOS 上通过 mitmproxy 路由 Dataimpulse 代理以进行网页抓取

单个本地 mitmproxy 监听器可将 macOS 上的每个抓取器、浏览器和模拟器通过 HTTP 或 SOCKS5 转发到 Dataimpulse 住宅或移动代理,轮换、限流以及可选的 TLS 检查都由你控制。

macOS SOCKS5 网页抓取 隐私/匿名性

概述

mitmproxy 是一个可编写脚本的开源拦截代理,作为普通 macOS 进程运行。在上游模式下,它在一个端口(127.0.0.1:8080)上接受本地客户端的连接,并将该流量转发到第二个代理——在本例中,是你的 Dataimpulse 住宅或移动端点。

优势在于集中化:你只需配置一次代理,然后机器上的任何浏览器、CLI 工具、Python 脚本或移动模拟器都可以通过 Dataimpulse 路由,而无需编辑该工具自身的代理设置。所有内容都经过一个单一的瓶颈点,在这里你可以限制并发、观察故障,并确认你实际使用的出口 IP。

Dataimpulse 很适合这种模式,因为它同时通过 HTTP 和 SOCKS5 提供住宅与移动端点,采用按需付费计费,并拥有庞大的 IP 池——适用于突发性、高吞吐量的数据采集,无需按月承诺。你可以在决定之前将其与其他代理提供商进行比较。

这种方法何时适用

方法 你要配置的内容 最适合
mitmproxy 上游模式(本指南) 为所有客户端配置一个本地端口 同时使用多个工具、集中限流与日志记录
每个工具的代理标志(curl -x、requests proxies=) 分别配置每个工具 组件最少的单个脚本
桌面代理管理器(例如 Proxifier) 按应用程序规则 忽略系统代理设置的 GUI 应用

你将构建什么

  • 位于 127.0.0.1:8080 的本地 mitmproxy 监听器
  • 通过 HTTP 或 SOCKS5 连接到 Dataimpulse 的上游连接
  • 针对你希望检查的特定主机可选的 TLS 拦截
  • 一个并发门控插件,使请求压力保持可预测
  • 适用于 curl、requests、Playwright 和 Android 模拟器的可运行示例

先决条件

  • 已安装 Homebrew 的 macOS 13 或更高版本
  • 一个 Dataimpulse 账户,以及仪表板中显示的用户名、密码、网关主机和 HTTP/SOCKS5 端口
  • 用于客户端示例的 Python 3.9 或更高版本(python3 --version)
  • 熟悉终端,并对 HTTP 代理和 TLS 有基本了解

设置的工作原理

  1. mitmproxy 监听 127.0.0.1:8080。
  2. 你的客户端将请求发送到 mitmproxy,而不是直接发送到目标站点。
  3. mitmproxy 打开到 Dataimpulse 网关的连接。
  4. Dataimpulse 从其池中分配一个出口 IP。
  5. 响应通过 mitmproxy 返回到你的客户端。

有两种模式很重要,尽早选择其中一种可以节省时间:

  • 直通(隧道): mitmproxy 转发 TLS 字节而不解密它们。任何地方都不需要更改证书。使用 --ignore-hosts '.*' 启用。
  • 拦截: mitmproxy 终止 TLS,以便读取或重写请求和响应。需要在 macOS 上以及每个客户端运行时中信任 mitmproxy CA 证书。

对于只需要不同出口 IP 的抓取,直通更简单、更快速,并且完全避免证书相关工作。仅对你真正需要检查的主机开启拦截。

步骤

  1. 安装 mitmproxy。

    brew install mitmproxy
    mitmdump --version
    
  2. 将你的 Dataimpulse 凭据导出为环境变量,这样它们就不会出现在 shell 历史记录或已提交的文件中。

    export DI_USER='your-dataimpulse-username'
    export DI_PASS='your-dataimpulse-password'
    export DI_HOST='gateway-host-from-your-dashboard'
    export DI_HTTP_PORT='http-port-from-your-dashboard'
    export DI_SOCKS_PORT='socks5-port-from-your-dashboard'
    export DI_SESSION='scrape-macos-01'
    

    如果希望它们在每个新 shell 中都可用,请将相同的行添加到 ~/.zshrc,并在不回显密码的情况下确认主机和端口:

    echo "${DI_HOST}:${DI_HTTP_PORT}"
    
  3. 对包含保留字符的凭据进行百分号编码。 密码中的 @、:、/ 或 # 等字符会破坏上游 URL。只编码你放入 URL 的值。

    python3 -c "import os, urllib.parse; print(urllib.parse.quote(os.environ['DI_PASS'], safe=''))"
    
  4. 通过 HTTP 以直通上游模式启动 mitmproxy。 在直通模式下,mitmproxy 隧道传输流量而不解密它,因此无需更改客户端证书。

    mitmdump \
      --mode "upstream:http://${DI_USER}:${DI_PASS}@${DI_HOST}:${DI_HTTP_PORT}" \
      --listen-port 8080 \
      --ignore-hosts '.*' \
      --set flow_detail=1
    
  5. 验证出口 IP。 让 mitmproxy 在其自己的终端窗口中保持运行,并从第二个窗口进行测试。

    curl -s -x http://127.0.0.1:8080 https://api.ipify.org
    echo
    curl -s https://api.ipify.org
    echo
    

    两个地址应该不同。如果它们相同,则请求没有到达 Dataimpulse——请参阅故障排除部分。

  6. 将上游切换为 SOCKS5,当你希望代理远程解析 DNS 时,这对于地理敏感目标通常更可取。

    mitmdump \
      --mode "upstream:socks5://${DI_USER}:${DI_PASS}@${DI_HOST}:${DI_SOCKS_PORT}" \
      --listen-port 8080 \
      --ignore-hosts '.*'
    
  7. 仅为你需要的主机启用拦截。 --allow-hosts 将解密限制为匹配的主机;其他所有内容保持隧道传输且不受影响。

    mitmdump \
      --mode "upstream:http://${DI_USER}:${DI_PASS}@${DI_HOST}:${DI_HTTP_PORT}" \
      --listen-port 8080 \
      --allow-hosts 'example\.com' \
      -s ./gate.py
    
  8. 信任 mitmproxy CA 证书,然后才能通过拦截抓取 HTTPS 主机。证书文件在 mitmproxy 首次启动时创建。

    # First run generates ~/.mitmproxy/mitmproxy-ca-cert.pem
    mitmdump --listen-port 8081 &
    sleep 2
    kill %1
    
    sudo security add-trusted-cert -d -r trustRoot \
      -k /Library/Keychains/System.keychain \
      ~/.mitmproxy/mitmproxy-ca-cert.pem
    

    命令行运行时通常会忽略 macOS 钥匙串,因此请将它们指向一个显式捆绑包:

    cat "$(python3 -c 'import certifi; print(certifi.where())')" \
      ~/.mitmproxy/mitmproxy-ca-cert.pem > ~/.mitmproxy/ca-bundle.pem
    
    export REQUESTS_CA_BUNDLE=~/.mitmproxy/ca-bundle.pem
    export NODE_EXTRA_CA_CERTS=~/.mitmproxy/mitmproxy-ca-cert.pem
    
  9. 添加一个并发门控插件,这样来自快速抓取器的突发请求就不会淹没网关。将此文件保存为 gate.py,放在你启动 mitmproxy 的目录旁边。

    # gate.py — cap in-flight requests passing through mitmproxy
    import asyncio
    
    from mitmproxy import http
    
    
    class Gate:
        def __init__(self, limit: int = 8) -> None:
            self._limit = limit
            self._sem: asyncio.Semaphore | None = None
    
        def running(self) -> None:
            self._sem = asyncio.Semaphore(self._limit)
    
        async def request(self, flow: http.HTTPFlow) -> None:
            await self._sem.acquire()
            flow.metadata["gate_held"] = True
    
        def _release(self, flow: http.HTTPFlow) -> None:
            if flow.metadata.pop("gate_held", False):
                self._sem.release()
    
        def response(self, flow: http.HTTPFlow) -> None:
            self._release(flow)
    
        def error(self, flow: http.HTTPFlow) -> None:
            self._release(flow)
    
    
    addons = [Gate(limit=8)]
    

    使用 -s ./gate.py 加载它。请注意,插件只能看到被拦截的流:如果你使用 --ignore-hosts '.*' 运行,连接会被隧道传输,这些钩子永远不会触发。在这种情况下,请改为在抓取器内部限制并发。

  10. 将你的工具指向本地监听器。 对于 shell 工具,在你要抓取的终端中导出标准变量——但不要在运行 mitmproxy 的终端中导出,以避免代理循环。

    export HTTP_PROXY=http://127.0.0.1:8080
    export HTTPS_PROXY=http://127.0.0.1:8080
    export NO_PROXY=localhost,127.0.0.1
    curl -s https://httpbin.org/ip
    

    对于 Python 脚本,在你保持直通模式时无需 CA 配置:

    import requests
    
    LOCAL_PROXY = "http://127.0.0.1:8080"
    proxies = {"http": LOCAL_PROXY, "https": LOCAL_PROXY}
    
    response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=30)
    response.raise_for_status()
    print(response.json())
    

    对于无头浏览器,将相同的本地代理传递给浏览器上下文:

    from playwright.sync_api import sync_playwright
    
    LOCAL_PROXY = "http://127.0.0.1:8080"
    
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True, proxy={"server": LOCAL_PROXY})
        page = browser.new_page()
        page.goto("https://httpbin.org/ip", wait_until="domcontentloaded")
        print(page.inner_text("body"))
        browser.close()
    

    如果你还需要抓取运行在自己机器上的服务,Chromium 默认会绕过环回地址。在启动调用中添加 args=["--proxy-bypass-list=<-loopback>"] 可移除此豁免。

  11. 捕获模拟器流量(可选)。 Dataimpulse 移动端点提供运营商级出口 IP;它们本身不会捕获设备流量。要通过该池路由模拟器,请将模拟器指向主机监听器。

    # Android emulator: 10.0.2.2 is the host machine
    adb shell settings put global http_proxy 10.0.2.2:8080
    adb shell settings get global http_proxy
    
    # Reset when finished
    adb shell settings put global http_proxy :0
    

    对于 iOS 模拟器,打开“设置”,选择 Wi-Fi,然后选择“配置代理”、“手动”,并将服务器设置为 127.0.0.1,端口为 8080。模拟器流量共享 Mac 的网络堆栈,因此拦截还需要在模拟器中安装 CA。

  12. 完成后进行清理。 移除拦截证书并重置任何客户端设置,以便无关流量停止通过 mitmproxy 流动。

    sudo security delete-certificate -c mitmproxy /Library/Keychains/System.keychain
    unset HTTP_PROXY HTTPS_PROXY
    

故障排除

407 需要代理身份验证

上游 URL 缺少凭据,或者密码中的保留字符破坏了 URL。请重新运行步骤 3 中的百分号编码命令,并使用编码后的值重建 --mode 字符串。

SSLCertVerificationError、CERT_AUTHORITY_INVALID 或 unable to get local issuer certificate

你启用了拦截,但客户端不信任 mitmproxy CA。请按步骤 8 所示安装证书,然后设置特定运行时的捆绑包变量(REQUESTS_CA_BUNDLE、NODE_EXTRA_CA_CERTS)。或者,如果你不需要读取流量,可以使用 --ignore-hosts '.*' 退回到直通模式。

地址已在使用

另一个进程已占用该端口。找到它,然后更改 --listen-port,而不是终止无关服务。

lsof -nP -iTCP:8080 -sTCP:LISTEN

所有请求挂起或超时

你很可能陷入了代理循环:HTTP_PROXY 在启动 mitmproxy 的同一个 shell 中导出,因此 mitmproxy 自己的上游连接被发送回自身。请在一个干净的终端中启动 mitmproxy,或在启动前 unset HTTP_PROXY HTTPS_PROXY。

请求成功但出口 IP 从不改变

直通模式本身不会轮换任何内容。轮换行为取决于你在仪表板中配置的会话类型以及客户端打开连接的方式。确认你没有在整个运行过程中固定使用单个粘性会话,并使用步骤 5 中的命令检查出口 IP。

模拟器忽略代理

在 Android 模拟器上,127.0.0.1 指的是模拟器自身,而不是 Mac。请使用 10.0.2.2,并用 adb shell settings get global http_proxy 确认该值。

目标返回 403 或 429

这是目标端的速率限制,而不是代理故障。请降低 gate.py 中的门控限制,在请求之间添加延迟,并确保你的抓取遵守网站条款和 robots.txt。

总结

现在你在 macOS 上拥有一个本地 mitmproxy 监听器,它可通过 HTTP 或 SOCKS5 将任何客户端转发到 Dataimpulse 住宅或移动代理;对于简单的 IP 轮换可使用直通模式,对于更深入的检查可使用选择性拦截。凭据保存在环境变量中,并发由插件限制,当你不再需要读取加密流量时,可以干净地添加或移除证书信任。