Python 代理抓取实战:Requests、HTTPX、Playwright 与可靠重试

发布时间
阅读时长约 5 分钟

Key Takeaways

代码优先的中文 Python 代理抓取指南,覆盖同步、异步和浏览器任务,并强调有效业务结果而非单纯 HTTP 成功。

🐍
可靠的 Python 代理抓取需要同时控制:代理身份、连接池、超时、重试、页面分类、地域验证和资源清理。仅把代理 URL 填进 proxies 参数并不等于生产可用。

本文分别展示 Requests、HTTPX 和 Playwright 的实现边界。所有示例只应用于获得授权的公开数据、QA、监控或自有系统。

环境变量

bash
PROXY_URL=http://USER:PASSWORD@PROXY_HOST:PORT
IP_CHECK_URL=https://iprobe.io/json
TARGET_URL=https://example.com

生产环境中应把主机、用户名和密码分开存储,避免异常堆栈输出完整 URL。

Requests 同步示例

python
import os
import requests

proxy = os.environ['PROXY_URL']
proxies = {
    'http': proxy,
    'https': proxy,
}

response = requests.get(
    os.getenv('IP_CHECK_URL', 'https://iprobe.io/json'),
    proxies=proxies,
    timeout=(10, 20),
)
response.raise_for_status()
print(response.text)

timeout=(10, 20) 分别限制连接和读取等待。不要省略超时,否则故障路由可能长期占用工作线程。

使用 Session 复用连接

python
from contextlib import closing
import requests

with closing(requests.Session()) as session:
    session.proxies.update(proxies)
    session.headers.update({
        'User-Agent': 'AuthorizedDataCollector/1.0',
        'Accept-Language': 'en-US,en;q=0.9',
    })
    response = session.get('https://example.com', timeout=(10, 20))

连接复用会影响代理轮换。即使凭证表示轮换,持续连接也可能继续使用原有出口。需要每个任务新身份时,应明确连接和 Session 生命周期。

HTTPX 异步示例

python
import asyncio
import os
import httpx

async def fetch(url: str) -> dict:
    timeout = httpx.Timeout(connect=10, read=20, write=10, pool=10)
    limits = httpx.Limits(max_connections=20, max_keepalive_connections=10)

    async with httpx.AsyncClient(
        proxy=os.environ['PROXY_URL'],
        timeout=timeout,
        limits=limits,
        follow_redirects=True,
    ) as client:
        response = await client.get(url)
        return {
            'status': response.status_code,
            'final_url': str(response.url),
            'body': response.text,
        }

print(asyncio.run(fetch('https://iprobe.io/json')))

异步并不意味着无限并发。应通过队列或信号量限制同一目标的并发。

并发控制

python
import asyncio

semaphore = asyncio.Semaphore(5)

async def bounded_fetch(url: str):
    async with semaphore:
        return await fetch(url)

并发值应依据目标允许速率、代理账户限制、CPU、连接数和有效输出率逐步调整。

轮换与粘性代理

代理服务通常通过用户名中的会话参数或不同端点实现轮换。

python
from urllib.parse import quote


def proxy_url(host: str, user: str, password: str) -> str:
    return f"http://{quote(user)}:{quote(password)}@{host}"

独立任务: 每个 URL 或任务使用新的轮换身份。

状态任务: 登录、分页、表单和购物车应使用同一粘性会话,并保持同一个客户端或浏览器 Context。

不要在重试函数内部无条件生成新会话,否则状态任务会在中途换 IP。

SOCKS 与远程 DNS

安装 SOCKS 支持:

bash
pip install 'httpx[socks]'

不同库对 SOCKS5 本地或代理端 DNS 的配置方式不同。需要远程解析时,验证客户端是否支持 socks5h 语义,不要只根据 URL 字符串推测。

Playwright 浏览器回退

python
import os
from playwright.async_api import async_playwright

async def browser_fetch(url: str) -> str:
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            headless=True,
            proxy={
                'server': os.environ['PROXY_SERVER'],
                'username': os.environ.get('PROXY_USERNAME'),
                'password': os.environ.get('PROXY_PASSWORD'),
            },
        )
        try:
            context = await browser.new_context(
                locale='en-US',
                timezone_id='America/New_York',
            )
            try:
                page = await context.new_page()
                await page.goto(url, wait_until='domcontentloaded', timeout=45_000)
                return await page.locator('body').inner_text()
            finally:
                await context.close()
        finally:
            await browser.close()

只有在数据依赖 JavaScript 或交互时才使用浏览器。静态 HTTP 更便宜、更容易调试。

页面验证

不要只判断 response.status_code == 200。还要检查:

  • 最终 URL
  • Content-Type
  • 页面标题
  • 业务字段
  • 挑战、登录或同意页标记
  • 地区、语言和币种
  • 响应长度异常
python
from dataclasses import dataclass

@dataclass
class Validation:
    valid: bool
    page_class: str
    reason: str | None = None


def validate(status: int, body: str) -> Validation:
    lower = body.lower()
    if status == 407:
        return Validation(False, 'proxy_auth', '代理凭证被拒绝')
    if 'captcha' in lower:
        return Validation(False, 'challenge', '发现挑战页面')
    if '<title>example' not in lower:
        return Validation(False, 'unexpected', '缺少预期页面标记')
    return Validation(True, 'expected')

实际项目不要只依赖一个字符串,应结合 URL、结构和业务 Schema。

错误分类与重试

python
from enum import Enum

class RetryClass(str, Enum):
    STOP = 'stop'
    BACKOFF = 'backoff'
    NEW_ROUTE = 'new_route'
    PARSER_REVIEW = 'parser_review'

建议规则:

  • 407:停止,修复凭证
  • 401/明确拒绝:停止,复核授权
  • 429:退避并降低频率
  • 连接重置或临时 5xx:有限重试
  • 错地区:检查市场配置,不直接当成成功
  • 解析失败:保存证据并更新解析器

指数退避与抖动

python
import asyncio
import random

async def sleep_before_retry(attempt: int) -> None:
    base = min(2 ** attempt, 30)
    await asyncio.sleep(random.uniform(0, base))

每个任务还应限制最大尝试次数、总耗时和最大字节数。

结构化日志

json
{
  "jobId": "job-001",
  "attempt": 1,
  "proxyMode": "sticky",
  "requestedCountry": "US",
  "observedCountry": "US",
  "status": 200,
  "pageClass": "expected",
  "durationMs": 1840,
  "downloadBytes": 182220,
  "businessOutput": "valid"
}

不要记录完整密码、可复用会话 ID、敏感页面内容或不必要的个人数据。

生产指标

  • 有效业务记录率
  • 每条有效记录的字节数
  • 每条有效记录的成本
  • 407、连接失败和 429 比例
  • 错地区率
  • 挑战页比例
  • 中位数和 p95 时延
  • 重试膨胀系数

上线检查清单

  1. 所有客户端都有超时。
  2. 并发有上限。
  3. Session 生命周期与代理会话一致。
  4. 浏览器只作为必要回退。
  5. HTTP 状态和页面分类分别记录。
  6. 重试按错误类型决策。
  7. 凭证已脱敏。
  8. 有原始证据与内容哈希。
  9. 指标以有效结果为中心。
  10. 已确认数据访问权限。

相关内容

AV
技术团队背书同行架构师审查 & 实测校验

Alex Vance

核心基础设施架构团队

本文由 BytesFlows 工程团队审查。代码示例适用于合规的公开网页数据采集、QA 自动化、SEO 监测与市场研究工作流。实际基准表现可能因目标站点防爬策略、地理位置、客户端运行环境及请求频率而异。