# Python中的住宅代理轮换：requests，httpx和aiohttp

> Python中的住宅代理轮换：使用requests重试，httpx同步与异步，aiohttp，保持会话和字节计数。语法检查代码。

- URL: https://crawlrelay.com/zh/docs/python/
- 最后更新: 2026-10-13
- Publisher: CrawlRelay (https://crawlrelay.com)

生产环境模式，非单行代码：尊重Retry-After的重试，每次请求使用新的IP或一个流程用一个IP，asyncio中的每主机并发，以及每个页面实际消耗的流量字节。

**快速回答 — 如何在Python中使用旋转住宅代理？**

在requests中将`http://USERNAME:PASSWORD@gw.crawlrelay.com:7777`作为`http`和`https`的代理，在httpx 0.26+中作为`proxy=`，在aiohttp中每次请求作为`proxy=`。**CrawlRelay**住宅代理每次新连接都会轮换，且用户名中带有`-session-<id>`保持会话。价格是US$1.50/1GB，50GB起价US$0.50/GB。支持加密货币支付，无需 KYC。

## Python快速设置

| 属性 | 值 |
| --- | --- |
| requests | `proxies={"http": url, "https": url}`；通过urllib3的`Retry`实现重试 |
| httpx | `httpx.Client(proxy=url)`；httpx 0.28删除了旧的`proxies=`参数 |
| aiohttp | `session.get(url, proxy="http://USER:PASS@host:port")`，凭据写在代理URL中 |
| SOCKS5 | `pip install "requests[socks]"`或`httpx[socks]`，端口为7778的`socks5h://` |
| 每次请求使用新 IP | 关闭keep-alive：`Connection: close`，`max_keepalive_connections=0`或`force_close=True` |
| 保持一个流程使用同一IP | 用户名中写`-session-<id>-ttl-<minutes>`，最长支持 30 分钟持续时长 |
| 代理URL方案 | HTTP端口始终是`http://`，即使是HTTPS网站也一样 |

## 如何用requests配合Respect Retry-After的重试使用代理？

在`Session`上挂载带有urllib3`Retry`的`HTTPAdapter`。它会重试连接错误和你列出的状态码，采用指数退避，尊重网站在429和503返回的`Retry-After`头。设置`trust_env = False`，避免残留的`HTTPS_PROXY`变量覆盖代理设置。

*requests + urllib3 Retry*

```python
import os

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

USER = os.environ.get("CRAWLRELAY_USER", "USERNAME-country-us")
PASSWORD = os.environ.get("CRAWLRELAY_PASSWORD", "PASSWORD")
PROXY = "http://%s:%s@gw.crawlrelay.com:7777" % (USER, PASSWORD)

retry = Retry(
    total=4,
    connect=2,
    backoff_factor=1.0,  # waits of 0 s, 2 s, 4 s, 8 s (Retry-After wins on 429/503)
    status_forcelist=(429, 502, 503, 504),
    allowed_methods=frozenset({"GET", "HEAD"}),
    respect_retry_after_header=True,
)

session = requests.Session()
session.trust_env = False  # ignore HTTP(S)_PROXY from the environment
session.mount("http://", HTTPAdapter(max_retries=retry))
session.mount("https://", HTTPAdapter(max_retries=retry))
session.proxies = {"http": PROXY, "https": PROXY}
session.headers["User-Agent"] = "ExampleCrawler/1.0 (+https://example.com/crawler-info)"
session.headers["Connection"] = "close"  # new connection per request, so usually a new exit IP

r = session.get("https://api.ipify.org", timeout=(10, 30))
print(r.status_code, r.text)
```

## 如何安全生成定向代理URL？

在一个函数中生成用户名并校验。国家码拼写错误或密码中未编码字符会导致[407](https://crawlrelay.com/zh/docs/errors/407-proxy-authentication-required/)，看上去像是凭据问题。下面的助手保持文档顺序（`USERNAME-country-{cc}-state-{state}-city-{city}-zip-{zip}-asn-{asn}-session-{id}-ttl-{minutes}`），拒绝未知键，并对凭据进行URL编码。

*用户名生成器和代理URL*

```python
import re
from urllib.parse import quote

ORDER = ("country", "state", "city", "zip", "asn", "session", "ttl")
VALUE = re.compile(r"^[a-z0-9_]+$")


def build_username(base, **params):
    """build_username("USERNAME", country="us", city="new_york", session="job1", ttl=30)"""
    parts = [base]
    for key in ORDER:
        value = params.pop(key, None)
        if value is None:
            continue
        value = str(value).lower()
        if not VALUE.match(value):
            raise ValueError("bad value for %s: %r" % (key, value))
        if key == "ttl" and not 1 <= int(value) <= 30:
            raise ValueError("ttl must be 1 to 30 minutes")
        parts.append("%s-%s" % (key, value))
    if params:
        raise ValueError("unknown parameters: %s" % ", ".join(sorted(params)))
    return "-".join(parts)


def proxy_url(username, password, socks=False):
    scheme, port = ("socks5h", 7778) if socks else ("http", 7777)
    return "%s://%s:%s@gw.crawlrelay.com:%d" % (
        scheme, quote(username, safe=""), quote(password, safe=""), port)


print(proxy_url(build_username("USERNAME", country="us", state="ny", city="new_york", session="job1", ttl=20), "p@ss:word"))
```

## 如何用httpx每次请求获取新IP？

关闭keep-alive。httpx默认连接池复用，连接复用会保持相同出口IP。关闭后（`max_keepalive_connections=0`），每次请求都会新建连接，通常通过不同住宅IP出口。`num_bytes_downloaded`显示实际传输字节，这最接近流量计费。

*httpx：旋转客户端*

```python
import httpx

PROXY = "http://USERNAME-country-gb:PASSWORD@gw.crawlrelay.com:7777"

# No keep-alive: every request opens a new connection, so it usually gets a new exit IP
with httpx.Client(
    proxy=PROXY,
    timeout=httpx.Timeout(30.0, connect=10.0),
    limits=httpx.Limits(max_keepalive_connections=0),
    follow_redirects=True,
) as client:
    for _ in range(3):
        r = client.get("https://api.ipify.org")
        print(r.text, r.num_bytes_downloaded, "bytes on the wire")
```

## 如何用asyncio并发爬取而不过度请求单个站点？

**每个主机**限制并发，而非仅全局限制。全局限制50会导致50个请求都访问同一域名（当URL按域排序时）。对每主机使用信号量，保证每个网站访问量合理，同时整体爬取依然快速。初始限制2到4，提升前请阅读[并发指南](https://crawlrelay.com/zh/docs/concurrency/)。

*httpx.AsyncClient加每主机信号量*

```python
import asyncio
from collections import defaultdict
from urllib.parse import urlsplit

import httpx

PROXY = "http://USERNAME-country-us:PASSWORD@gw.crawlrelay.com:7777"
PER_HOST = 4  # parallel requests per target host
host_slots = defaultdict(lambda: asyncio.Semaphore(PER_HOST))


async def fetch(client, url):
    async with host_slots[urlsplit(url).hostname]:
        try:
            r = await client.get(url)
            return url, r.status_code, r.num_bytes_downloaded
        except httpx.HTTPError as exc:
            return url, type(exc).__name__, 0


async def main(urls):
    limits = httpx.Limits(max_connections=50, max_keepalive_connections=0)
    timeout = httpx.Timeout(30.0, connect=10.0)
    async with httpx.AsyncClient(proxy=PROXY, limits=limits, timeout=timeout) as client:
        for result in await asyncio.gather(*(fetch(client, u) for u in urls)):
            print(*result)


asyncio.run(main(["https://example.com/", "https://example.org/", "https://example.net/"]))
```

## 如何使用aiohttp代理？

每次请求传递网关作为`proxy=`，凭据写入代理URL。此方法适用于所有aiohttp 3.x版本且无警告；`BasicAuth`和`proxy_auth=`参数自aiohttp 3.14起废弃，4.0将移除。`TCPConnector(force_close=True)`确保响应后关闭连接，每次请求通常获得新出口。否则aiohttp会复用连接，IP会保持一段时间。

*aiohttp凭证写入代理URL*

```python
import asyncio

import aiohttp

# Credentials go in the proxy URL; percent-encode a password that contains @ or :
PROXY = "http://USERNAME-country-fr:PASSWORD@gw.crawlrelay.com:7777"


async def main():
    # force_close=True: one connection per request, so each request usually gets a new exit IP
    connector = aiohttp.TCPConnector(limit=20, force_close=True)
    timeout = aiohttp.ClientTimeout(total=60, sock_connect=10)
    async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
        for _ in range(3):
            async with session.get("https://api.ipify.org", proxy=PROXY) as resp:
                print(resp.status, await resp.text())


asyncio.run(main())
```

## 如何保持多步骤流程使用同一个IP？

每个流程生成一个会话ID，写入用户名，TTL应长于流程时间。此处keep-alive可用，目的是保持同一出口IP。若会话对应住宅设备掉线，TTL到期前IP可能变化，保证流程可从上次检查点重启。

*requests中的保持会话*

```python
import uuid

import requests

session_id = uuid.uuid4().hex[:10]  # one ID per flow, lowercase letters and digits
user = "USERNAME-country-us-state-ca-session-%s-ttl-20" % session_id
proxy = "http://%s:PASSWORD@gw.crawlrelay.com:7777" % user

with requests.Session() as s:
    s.proxies = {"http": proxy, "https": proxy}
    first = s.get("https://api.ipify.org", timeout=(10, 30)).text
    second = s.get("https://api.ipify.org", timeout=(10, 30)).text
    print(first, second, "same exit" if first == second else "exit changed")
```

## Python爬虫每页流量多少？

购买大流量前，测量100个真实页面流量。在httpx中记录`num_bytes_downloaded`，或在中间件统计压缩内容长度，乘以页面数。HTML页面通常几十KB；[带宽指南](https://crawlrelay.com/zh/guides/bandwidth-calculator/)给出成本和1,000页价格，基于CrawlRelay定价。

三种习惯大幅降低流量，效果不变：

- 发送`Accept-Encoding: gzip, br`（httpx和requests默认支持gzip），页面传输压缩。
- 获取页面加载的JSON接口（前提是网站支持或开放此行为），而非完整HTML。
- 使用`If-None-Match`或`If-Modified-Since`跳过未变更页面：`304 Not Modified`只需几百字节。

## Python 和代理：常见问题

### requests、httpx 还是 aiohttp：我该用哪个来爬虫？

简单脚本和小规模爬取用 requests。想同时支持同步和异步代码、并通过 `httpx[http2]` 使用 HTTP/2 就用 httpx。纯 asyncio 中高并发用 aiohttp。需要调度、去重和管道的爬虫，建议用 [Scrapy](https://crawlrelay.com/zh/docs/scrapy/)，别自己写这些功能。

### 为什么在 Python 里用代理时会遇到 SSL: WRONG_VERSION_NUMBER 错误？

大概率是你在代理地址前写了 `https://`。网关端口 7777 是纯 HTTP，用来建立 HTTPS 的 CONNECT 隧道，所以代理 URL 在 `http` 和 `https` 键里都必须以 `http://` 开头。详见 [错误页面](https://crawlrelay.com/zh/docs/errors/ssl-wrong-version-number/)。

### 如何在 Python 中使用 SOCKS5 代理？

安装额外组件（`pip install "requests[socks]"` 或 `pip install "httpx[socks]"`），然后使用 `socks5h://USERNAME:PASSWORD@gw.crawlrelay.com:7778`。`h` 会把主机名发送给代理，DNS 解析在出口进行，可以获得跟本地用户一样的区域结果。

### 为什么我的轮换代理会返回相同的 IP？

你的客户端重复使用了“keep-alive”连接，而轮换是针对新连接才执行的。在 requests 里发送 `Connection: close`，httpx 里设置 `max_keepalive_connections=0`，aiohttp 中用 `force_close=True`。还要检查用户名是否含有 `session-` 部分，那是故意保留一个 IP 的。

### 如何设置代理请求的超时？

要显式设置：requests 默认无超时。连接超时一般设为 10 秒，读取超时 30 秒适合 HTML 页；移动出口或慢速目标建议 45 到 60 秒。超时后最好重试一次，且新建连接，别一直等。参考[读取超时](https://crawlrelay.com/zh/docs/errors/read-timeout/)。

### 我可以在 Jupyter 或 pandas 中使用这些代理吗？

可以。只要用到 requests 或 httpx 的工具都能用相同的代理设置。对于 `pandas.read_html` 或 `read_csv` 形式的 URL，先用带代理的 requests 下载内容，再把文本或字节传给 pandas。

**通过住宅代理运行你的 Python 爬虫** US$1.50，1 GB起，US$0.50/GB从50 GB起。 [查看价格](https://crawlrelay.com/zh/pricing/) · [Scrapy 指南](https://crawlrelay.com/zh/docs/scrapy/)

---

CrawlRelay: CrawlRelay 是为开发者设计的住宅代理和 4G/5G 移动代理轮换服务，适用于爬虫和 AI 数据管道：按需付费且流量永不过期，最低流量从 US$0.50/GB 起，支持加密货币支付，无需上传身份证明。无需 KYC —— 无需身份证、自拍和任何证件。支持加密货币支付。
