如果想在 V2EX 获得更好的推广效果,欢迎了解 PRO 会员机制:
https://www.v2ex.com/pro/about

如果你经常使用铜币置顶主题,持有 V2EX Solana Token 会在每日签到时获得额外铜币:
https://www.v2ex.com/solana
BifrostNetwork
V2EX  ›  推广

Scraper Proxy 实战:用 Playwright 接入住宅代理,做好会话、退避与数据验收

  •  
  •   BifrostNetwork ·
    PRO
    · 6h 8m ago · 182 views

    爬虫代理接入与验证指南:Python + Playwright + BifrostNetwork 实战

    爬虫接上代理后,出口 IP 已经改变,为什么价格仍是错误地区的价格?为什么页面返回 200 ,数据库里却没有有效商品?为什么屏蔽图片以后,代理流量反而没有下降?

    这些问题发生在不同层:网络出口、浏览器状态、页面就绪条件和采集验收。可用于生产的 scraper proxy 接入,应把代理会话与浏览器上下文一起管理,并用业务数据判断成功。本文用 Python + Playwright + BifrostNetwork 说明具体做法。

    资料核查日期:2026 年 9 月 14 日。本文依据当前官方文档和协议标准;配置示例用于接入与验证,不包含商业代理成功率、延迟或节省比例的实测承诺。


    1. 先确定代理负责哪一层

    Scraper proxy 是爬虫访问目标站时使用的代理出口。Proxy scraper 则通常指搜集代理地址的工具,两者不是同一类需求。

    在本文架构中,任务队列决定何时访问,Playwright 执行页面 JavaScript ,代理网关选择出口,解析器提取字段,校验器决定能否入库。代理不会替你维护选择器,也不会让缺失的访问权限自动出现。

    任务条件 建议起点 验证重点
    官方 API 或导出已经满足字段要求 优先使用该数据接口 配额、数据授权、更新时效
    HTML 内已有完整数据 普通 HTTP 客户端,按需加代理 解析正确性、出口要求
    数据依赖 JavaScript 或页面交互 Playwright ,按任务配置代理 就绪条件、上下文状态、浏览器资源
    必须观测特定市场的住宅网络访问结果 测试住宅出口 实际国家、页面市场、会话连续性

    这些是工程选型建议,不是住宅代理优于其他方案的性能结论。不同代理类型的成本比较可参考 [Scraper Proxy 选型指南]。

    Playwright 支持在浏览器启动时设置代理,也支持按 BrowserContext 设置代理。独立上下文不共享 Cookie 和缓存,适合隔离不同采集任务。参考 Playwright 网络文档、Browser.new_context 文档。


    2. 先解决协议与认证,别从修改请求头开始

    Chromium 的 SOCKS5 支持不等于支持 SOCKS5 密码认证

    Chromium 官方代理文档明确说明:Chrome 的 SOCKSv5 不支持认证。因此,把一条可用于 requests 的带密码 SOCKS5 连接串直接搬进 Playwright Chromium ,可能失败。本文使用带用户名密码的 HTTP 代理。参考 [Chromium 代理实现说明]。

    Playwright 的 proxy.usernameproxy.password 是代理认证参数;http_credentials 是网站 HTTP 认证参数。不要把代理密码填进网站认证,也不要把 Proxy-Authorization 当作普通页面请求头发送。参考 [Playwright HTTP 认证与代理设置]。

    代理地址以 http:// 开头,并不要求目标网址也是 HTTP 。访问 HTTPS 目标时,HTTP 代理可以使用 CONNECT 建立隧道,目标 TLS 在隧道内进行;但这不等于客户端到代理的外层连接也经过 TLS 。若要加密代理认证这一跳,应使用服务商明确提供且客户端支持的 HTTPS 代理端点,不能只改协议前缀。参考 [RFC 9110:CONNECT]、[Chromium HTTP/HTTPS 代理说明]。

    从控制面板复制凭证,路由参数放在用户名中

    BifrostNetwork 当前文档列出的网关是 gate.bifrostnetwork.cc:9521,支持 HTTP/HTTPS CONNECT 接入;用户名可携带国家、会话 ID 和 TTL 参数。实际基础用户名请从订单复制,不要根据示例猜测套餐代码。参考 [BifrostNetwork 接入文档]。

    基础用户名-country-us-session-唯一任务标识-ttl-300
    
    • -country-us 请求美国出口;
    • -session-… 请求粘性会话;
    • -ttl-300 指定 300 秒。

    当前文档给出的省略 TTL 默认值是 600 秒。文档也说明,未覆盖的地区或 ASN 组合存在回退行为,因此不能仅凭用户名判定实际国家。参考 [会话与定位参数]。


    3. 一个业务流程,绑定一个上下文和一个代理会话

    商品列表 → 选择市场 → 打开详情 → 读取价格,是同一个有状态流程。推荐把以下信息绑定到任务 ID:

    • task_id
      • country + proxy_session_id
      • BrowserContext( Cookie 、站点存储、语言设置)
      • 起止时间 + 内容校验结果

    在流程结束时关闭上下文;下一个独立任务生成新的会话 ID 。不要在同一页面的导航、脚本和 XHR 之间主动更换代理配置,也不要只换出口却继续复用上个市场的 Cookie 。

    这是任务隔离策略,不是“新会话必定获得从未使用过的新 IP”的保证。连接复用、可用节点和代理路由都会影响实际结果;一个出口检查请求也无法证明随后所有子请求都走相同节点。长流程应在关键步骤记录出口并检查漂移,超过计划会话时长时重新安排任务。

    IP 国家、浏览器 locale 、站点选择的配送国家和币种也必须分别验收。locale="en-US" 会影响浏览器语言相关行为,但不会把出口变成美国 IP ,更不能替代页面里的市场选择。参考 [BrowserContext 的 locale 配置]。


    4. Python 示例:先验证一个页面,再扩展队列

    在独立 Python 环境中安装 Playwright 和匹配的 Chromium 。正式运行时记录并锁定 Python 、Playwright 与浏览器版本,确保回归可以复现。参考 [Playwright 安装文档]。

    python -m pip install playwright
    python -m playwright install chromium
    

    运行脚本前,用本地环境或密钥管理服务注入以下变量。密码不要提交到仓库或写进共享 shell 历史。

    变量 内容
    BIFROST_BASE_USERNAME 控制面板中的基础用户名,不含本文追加的路由参数
    BIFROST_PASSWORD 代理密码
    TARGET_URL 已确认允许采集的 HTTPS 页面
    READY_SELECTOR 能定位到唯一业务元素的 CSS 选择器
    EXPECTED_TEXT 该元素预期包含的非空文本,用于最小内容验收
    BIFROST_PROXY_SERVER 可选;默认 http://gate.bifrostnetwork.cc:9521

    下面保存为 scraper_proxy.py,然后执行 python scraper_proxy.py。示例只做单次页面访问:认证错误、HTTP 错误和内容校验错误直接交给上层调度器,不隐式切换 IP 重试。

    import asyncio
    import json
    import os
    import time
    import uuid
    from urllib.parse import urlsplit
    
    from playwright.async_api import async_playwright, expect
    
    
    async def main():
        target = os.environ["TARGET_URL"]
        selector = os.environ["READY_SELECTOR"]
        expected = os.environ["EXPECTED_TEXT"].strip()
        parsed = urlsplit(target)
        if parsed.scheme != "https" or not parsed.hostname or not expected:
            raise ValueError("需要有效的 HTTPS 目标地址和非空验收文本")
    
        task_id = uuid.uuid4().hex[:16]
        base = os.environ["BIFROST_BASE_USERNAME"]
        proxy = {
            "server": os.environ.get(
                "BIFROST_PROXY_SERVER", "http://gate.bifrostnetwork.cc:9521"
            ),
            "username": f"{base}-country-us-session-{task_id}-ttl-300",
            "password": os.environ["BIFROST_PASSWORD"],
        }
    
        async with async_playwright() as p:
            browser = await p.chromium.launch(headless=True)
            try:
                context = await browser.new_context(proxy=proxy, locale="en-US")
                try:
                    page = await context.new_page()
                    started = time.monotonic()
                    response = await page.goto(
                        target, wait_until="domcontentloaded", timeout=30_000
                    )
                    if response is None:
                        raise RuntimeError("导航未返回主文档响应")
                    if not 200 <= response.status < 300:
                        # 上层调度器可记录并解析该值;不要在这里立即重试。
                        raise RuntimeError(json.dumps({
                            "task_id": task_id,
                            "status": response.status,
                            "retry_after": response.headers.get("retry-after"),
                        }))
    
                    ready = page.locator(selector)
                    await expect(ready).to_have_count(1, timeout=10_000)
                    await expect(ready).to_be_visible(timeout=10_000)
                    await expect(ready).to_contain_text(expected, timeout=10_000)
                    print(json.dumps({
                        "task_id": task_id,
                        "status": response.status,
                        "elapsed_ms": round((time.monotonic() - started) * 1000),
                        "content_check": "passed",
                    }))
                finally:
                    await context.close()
            finally:
                await browser.close()
    
    
    if __name__ == "__main__":
        asyncio.run(main())
    

    选择器和预期文本需要按目标页面填写,不存在适用于所有电商网站的通用选择器。生产版本还应校验最终 URL 、商品 ID 、价格、币种、市场和采集时间,并按业务键去重;这里只演示一个可明确失败的就绪条件。不要把配置选择器为空、匹配多个元素或定位超时统称为“代理失效”。Playwright 的 Locator assertions 会在超时内重复检查条件。参考 [Locator assertions 文档]。

    page.goto() 对 404 、500 等有效 HTTP 状态不会自动抛出异常,必须检查返回响应;它返回的是主文档响应,也不能代表后续商品 API 成功。domcontentloaded 只表示 DOM 事件已发生,业务就绪仍靠字段或响应断言。官方将 networkidle 标为不推荐,不能用“网络安静”代替数据完整性。参考 [Page.goto 文档]。


    5. 按错误类型重试:429 不应触发立即换 IP

    现象 优先定位 调度动作
    407 或代理认证失败异常 代理用户名、密码、协议和套餐状态 停止该配置,修正凭证
    CONNECT 失败或连接超时 网关可达性、代理协议、出口与目标连接 分层探测;确认暂态故障后有限重试
    401 / 403 目标认证、访问策略和响应内容 检查访问条件,避免无限重试
    429 目标的频率限制及作用范围 解析 Retry-After ,降低相应任务组负载
    502 / 503 / 504 响应来自网关还是目标、是否暂态 记录来源线索,按预算退避
    200 但字段为空或地区错误 页面状态、数据接口、解析和地区设置 判为内容失败,修复后重跑

    401 、403 、407 的协议含义分别涉及目标认证、拒绝处理和代理认证;浏览器隧道建立失败时,错误可能表现为异常而不是页面响应。参考 [RFC 9110 状态码定义]。

    RFC 6585 没有要求服务器只按 IP 计数;限制还可能关联账户、Cookie 或资源。收到 429 后换 IP 不一定解除限制,也无法控制系统对同一目标的总负载。参考 [RFC 6585 第 4 节]。

    Retry-After 既可以是非负整数秒,也可以是 HTTP 日期。调度器需要支持两种格式;日期形式应按当前 UTC 时间计算等待时长并考虑时钟偏差。参考 [RFC 9110:Retry-After]。

    建议的有限重试策略如下,参数应由目标规则和任务预算决定:

    1. 没有可解析的 Retry-After:使用带随机抖动的指数退避。
    2. 存在有效 Retry-After:等待至少服务端指定时长,再加入小幅抖动。
    3. 等待时间超出任务剩余预算:延后入队或结束任务,不截短后抢跑。
    4. 达到最大尝试次数:进入失败队列,保留错误类别。

    调度限速至少按目标域名聚合,涉及账户配额时还要跨域名合并账户预算。多个 worker 必须共享冷却状态,否则每个 worker 都“低并发”,总请求量仍可能超限。一次浏览器导航还会产生脚本、图片和 API 请求,不能把页面任务数直接当 HTTP 请求数。

    如果你的队列基于 Scrapy ,当前 RetryMiddleware 默认重试状态中包含 429 ,但“可重试”不等于完整实现服务端等待策略。AutoThrottle 会参考延迟,且不允许较快的非 200 响应使延迟下降;仍需结合实际配置验证冷却与重试行为。参考 [Scrapy RetryMiddleware]、[Scrapy AutoThrottle]。


    6. 流量优化:先保留基线,再测试资源拦截

    浏览器可能下载大量与目标字段无关的图片和媒体,但直接屏蔽所有非 HTML 请求会破坏页面。先跑正常加载的基线,再试验性阻止确认为无关的资源,比较字段完整率、任务耗时与实际计费流量。

    特别注意:Playwright 启用 context.route() 后会禁用 HTTP 缓存; Service Worker 接管的请求也可能绕过该路由拦截。官方建议在需要拦截时考虑设置 service_workers="block",但这会改变依赖 Service Worker 的页面行为。多页任务原本可以复用缓存,开启拦截后必须重新测量总成本。参考 [BrowserContext.route 文档]。

    分析请求体积时,可在请求完成后使用 request.sizes();其中 responseBodySize 是编码后的响应体字节数. 它适合定位大资源,但不等于代理账单:协议开销、失败请求及供应商计量边界需要另行核对。参考 [Playwright Request.sizes]。

    不要用 len(page.content()) 估算代理流量:它是渲染后的 HTML 字符串长度,既不是全部网络字节,也不包含所有页面资源。


    7. 用固定样本验证 BifrostNetwork 是否适合任务

    建议先选一组覆盖主要页面模板和目标市场的 URL ,固定浏览器版本、时间窗口、任务数量、重试预算和字段规则。样本规模由页面差异和波动决定;下表是验收方法,不是服务性能保证。

    指标 记录方法 能回答的问题
    出口与市场一致性 实际 IP 、定位来源、页面国家和币种 拿到的是否是目标市场数据
    内容通过率 合格任务数 ÷ 全部任务数 HTTP 成功之外,有多少结果能用
    会话漂移 关键步骤的出口与站点状态 长流程能否维持一致条件
    延迟分布 成功任务 P50/P95 ,另列失败和超时 是否满足采集截止时间
    重试放大 总尝试次数 ÷ 任务数 稳定性问题增加了多少工作量
    每千条有效记录成本 总成本 ÷ 合格且去重的记录数 × 1,000 成本是否支持业务规模

    “总成本”应包括代理账单、浏览器计算资源和可归属的维护成本;有效记录为零时,单位成本不可计算,应判定该轮测试失败。比较套餐时使用同一标准,不把某次最好结果当平均表现。

    对已有 Playwright 管线的团队,BifrostNetwork 的接入点就是 proxy 配置:从控制面板获取凭证,用开发者文档中的国家与会话参数组织任务,再按当前价格页和试跑账单核算预算。本文不引用未经实测的吞吐量,也不把动态住宅粘性会话当作独享静态 IP 的 SLA 。

    上线前先确认目标提供的 API 、访问规则与频率要求。robots.txt 是爬虫访问规则的一部分,RFC 9309 也明确它不构成访问授权;本文示例不会自动抓取或执行 robots 规则,需要由任务入口处理。参考 [RFC 9309]。


    常见问题

    为什么已经指定美国代理,页面还是其他币种?

    检查实际出口、配送国家、已有 Cookie 、账户市场设置和页面返回数据。IP 定位只是输入之一; BifrostNetwork 文档列出的路由回退也需要纳入验收。

    每个请求换 IP ,会不会更稳定?

    对于包含导航和 XHR 的关联流程,主动频繁更换出口会增加状态不一致的排查难度。先以业务任务为会话边界;独立任务需要轮换时,再创建新的上下文与会话。

    为什么 HTTP 200 不能算采集成功?

    主文档可能只是空壳,商品 API 可能失败,页面也可能是登录页或错误提示。只有业务字段、市场、时间和去重规则全部通过,才计入有效数据。

    能否直接把示例扩到几百并发?

    应先测单任务浏览器资源和目标负载,再加有限队列、共享冷却与失败预算。并发额度属于系统容量约束;增加 worker 不能代替目标允许的访问频率。

    No Comments Yet
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   5387 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 51ms · UTC 09:16 · PVG 17:16 · LAX 02:16 · JFK 05:16
    ♥ Do have faith in what you're doing.