代理

hs-net 内置代理归一化服务(ProxyService),统一处理 HTTP/SOCKS/认证代理,对外暴露纯 HTTP 本地代理。

章节概览

章节说明示例文件
简单代理字符串方式指定代理fixed_proxy.py
代理认证HTTP Basic / SOCKS 认证
列表轮换多个代理轮询或随机switch_proxy.py
自定义代理源实现 ProxyProvider 接口custom_provider.py
代理池 APIApiProxyProvider 对接代理池api_proxy.py
代理链通过中转代理连接上游transit_proxy.py
域名路由按域名走不同代理或直连domain_rules.py
身份路由按请求身份自动分配并绑定代理identity_routing.py
异步代理异步客户端搭配代理async_proxy.py

简单代理

直接传字符串,支持 HTTP、HTTPS、SOCKS4、SOCKS5 及认证格式:

simple_proxy.py
from hs_net import SyncNet

# HTTP 代理
with SyncNet(proxy="http://proxy-host:8080") as net:
    resp = net.get("https://example.com")

# HTTPS 代理
with SyncNet(proxy="https://proxy-host:8443") as net:
    resp = net.get("https://example.com")

# SOCKS5 代理
with SyncNet(proxy="socks5://proxy-host:1080") as net:
    resp = net.get("https://example.com")

# SOCKS4 代理
with SyncNet(proxy="socks4://proxy-host:1080") as net:
    resp = net.get("https://example.com")

# 带认证的代理
with SyncNet(proxy="http://user:password@proxy-host:8080") as net:
    resp = net.get("https://example.com")

# 带认证的 SOCKS5
with SyncNet(proxy="socks5://user:password@proxy-host:1080") as net:
    resp = net.get("https://example.com")

完整示例见 examples/proxy/fixed_proxy.py

支持的代理格式
格式示例
HTTPhttp://host:port
HTTPShttps://host:port
SOCKS4socks4://host:port
SOCKS4asocks4a://host:port
SOCKS5socks5://host:port
SOCKS5hsocks5h://host:port
带认证http://user:pass@host:port
带认证 SOCKS5socks5://user:pass@host:port
:::

代理认证

代理认证通过 URL 中的 user:password@ 部分传递,不同协议的认证方式由 hs-net 自动处理:

HTTP/HTTPS 代理认证

使用 HTTP Basic Auth,hs-net 自动编码为 Proxy-Authorization 头:

http_auth.py
# 用户名 + 密码
with SyncNet(proxy="http://admin:secret123@proxy-host:8080") as net:
    resp = net.get("https://example.com")

# HTTPS 代理同理
with SyncNet(proxy="https://admin:secret123@proxy-host:8443") as net:
    resp = net.get("https://example.com")

SOCKS5 代理认证

SOCKS5 支持用户名/密码认证(RFC 1929),hs-net 自动完成认证握手:

socks5_auth.py
with SyncNet(proxy="socks5://myuser:mypass@proxy-host:1080") as net:
    resp = net.get("https://example.com")

SOCKS4 代理认证

SOCKS4 仅支持 user_id(无密码),通过用户名字段传递:

socks4_auth.py
with SyncNet(proxy="socks4://myuser@proxy-host:1080") as net:
    resp = net.get("https://example.com")

认证格式汇总

协议认证方式URL 格式
HTTP/HTTPSBasic Auth(自动 Base64 编码)http://user:pass@host:port
SOCKS5用户名/密码认证(RFC 1929)socks5://user:pass@host:port
SOCKS4User ID(仅用户名)socks4://user@host:port

:::warning 特殊字符 密码中包含 @:/ 等特殊字符时,需要进行 URL 编码:

from urllib.parse import quote

password = quote("p@ss:word/123", safe="")
proxy = f"http://user:{password}@proxy-host:8080"
# => http://user:p%40ss%3Aword%2F123@proxy-host:8080

ProxyService

对于更复杂的代理需求,使用 ProxyService

列表轮换

list_proxy.py
from hs_net import Net, ProxyService

# 轮询模式(默认)
proxy = ProxyService([
    "http://proxy1:8080",
    "socks5://proxy2:1080",
    "http://user:pass@proxy3:8080",
], strategy="round_robin")

async with Net(proxy=proxy) as net:
    # 每次 switch() 切换到下一个代理
    resp = await net.get("https://example.com")
    net.proxy_service.switch()  # 切换到下一个
    resp = await net.get("https://example.com")
random_proxy.py
# 随机模式
proxy = ProxyService([
    "http://proxy1:8080",
    "socks5://proxy2:1080",
], strategy="random")

with SyncNet(proxy=proxy) as net:
    resp = net.get("https://example.com")
    net.proxy_service.switch()  # 随机切换

完整示例见 examples/proxy/switch_proxy.py

自定义代理源

实现 ProxyProvider 接口,对接自己的代理源:

custom_provider.py
from hs_net import Net, ProxyService, ProxyProvider

class MyProvider(ProxyProvider):
    """从代理 API 获取代理地址。"""

    def get_proxy(self) -> str:
        # 同步获取代理
        return "socks5://1.2.3.4:1080"

    async def async_get_proxy(self) -> str:
        # 异步获取代理(可选,默认回退到 get_proxy)
        return "socks5://1.2.3.4:1080"

proxy = ProxyService(provider=MyProvider())

async with Net(proxy=proxy) as net:
    resp = await net.get("https://example.com")
    await net.proxy_service.async_switch()  # 异步切换代理

完整示例见 examples/proxy/custom_provider.py

同步 vs 异步

ProxyProvider 提供两个方法:

  • get_proxy() — 同步版本,必须实现
  • async_get_proxy() — 异步版本,可选,默认回退到 get_proxy()

Net(异步客户端)启动和切换代理时调用 async_get_proxy()SyncNet 调用 get_proxy()。 对于纯计算的 Provider(如 FixedProxyProviderListProxyProvider),只实现 get_proxy() 即可。 需要异步 I/O 时(如调用代理池 API),应覆写 async_get_proxy()

代理池 API

使用内置的 ApiProxyProvider 对接代理池 API,自动获取代理地址:

api_proxy_simple.py
from hs_net import ApiProxyProvider, Net, ProxyService

# 最简单 — API 直接返回代理地址文本
provider = ApiProxyProvider("https://api.pool.com/get")

svc = ProxyService(provider=provider)
async with Net(proxy=svc) as net:
    resp = await net.get("https://example.com")

从 JSON 响应中提取代理地址:

api_proxy_json.py
from httpx import Response as HttpxResponse
from hs_net import ApiProxyProvider, Net, ProxyService

def parse_proxy(resp: HttpxResponse) -> str:
    data = resp.json()
    item = data["data"][0]
    return f"http://{item['ip']}:{item['port']}"

provider = ApiProxyProvider(
    "https://api.pool.com/get?num=1",
    parser=parse_proxy,
)

svc = ProxyService(provider=provider)
async with Net(proxy=svc) as net:
    resp = await net.get("https://example.com")
    await svc.async_switch()  # 从 API 获取新代理并切换

访问代理池 API 需要翻墙时,指定 proxy 参数:

api_proxy_with_proxy.py
provider = ApiProxyProvider(
    "https://api.overseas-pool.com/get",
    proxy="http://127.0.0.1:7897",  # 通过本地 Clash 访问 API
    parser=parse_proxy,
)

ApiProxyProvider 参数:

参数类型说明
api_urlstr代理池 API 地址
proxystr | None访问 API 用的代理(如本地 VPN)
parserCallable[[httpx.Response], str] | None自定义响应解析函数,默认 resp.text.strip()
timeoutfloat请求超时(秒),默认 10

完整示例见 examples/proxy/api_proxy.py

代理链(中转代理)

通过中转代理访问上游代理,适用于上游代理无法直连的场景(如通过本地 Clash 中转访问海外代理):

transit_proxy.py
from hs_net import Net, ProxyService

# 通过本地 Clash (127.0.0.1:7897) 中转访问海外 SOCKS5 代理
proxy = ProxyService(
    "socks5://overseas-proxy:1080",
    transit="http://127.0.0.1:7897",
)

async with Net(proxy=proxy) as net:
    resp = await net.get("https://example.com")

流量路径:客户端 → 本地 Clash → 海外代理 → 目标网站

完整示例见 examples/proxy/transit_proxy.py

域名路由

通过 rules 参数按域名路由到不同代理,支持通配符匹配和 "direct" 直连:

domain_rules.py
from hs_net import Net, ProxyService

svc = ProxyService(
    "http://default-proxy:8080",   # 未匹配域名走默认代理
    rules={
        "*.cn": "direct",               # 国内网站直连
        "*.google.com": "socks5://proxy1:1080",  # Google 走 SOCKS5
        "github.com": "http://proxy2:8080",      # GitHub 走 HTTP 代理
    },
)

async with Net(proxy=svc) as net:
    await net.get("https://baidu.cn")        # → 直连
    await net.get("https://www.google.com")  # → proxy1
    await net.get("https://github.com")      # → proxy2
    await net.get("https://example.com")     # → default-proxy

同步客户端同理:

domain_rules_sync.py
from hs_net import ProxyService, SyncNet

svc = ProxyService(
    "http://default-proxy:8080",
    rules={"*.cn": "direct"},
)

with SyncNet(proxy=svc) as net:
    net.get("https://baidu.cn")     # → 直连
    net.get("https://example.com")  # → default-proxy

域名匹配规则:

模式说明匹配示例
github.com精确匹配github.com
*.google.com通配符(子域名)www.google.commail.google.com
*.cnTLD 通配符baidu.cnwww.baidu.cn
"direct"直连,不走代理
匹配优先级

规则按 dict 插入顺序匹配,先匹配先命中。如果多个规则都能匹配同一个域名,只有第一个生效。

switch() / async_switch() 在 rules 模式下只切换未匹配域名的默认代理,不影响已匹配的规则。

仅域名匹配

rules 只支持域名级别匹配,不支持路径(如 example.com/api)。原因是 HTTPS 流量走 CONNECT 隧道,路径在 TLS 加密内,代理层无法看到。

完整示例见 examples/proxy/domain_rules.py

身份路由

通过 identity_extractor 参数,根据请求中的身份信息(cookies、headers、URL 参数等)自动为每个身份分配并绑定代理。同一身份始终使用同一个代理(sticky),不同身份使用不同代理。

典型场景:多账号并发采集,每个账号需要独立的代理 IP。

identity_routing.py
from hs_net import Net, ProxyService, RequestModel
from hs_net.proxy import ListProxyProvider

# 身份提取器:从请求中提取身份标识
def my_extractor(request: RequestModel) -> str | None:
    if request.cookies and "session" in request.cookies:
        return request.cookies["session"]
    return None

# 代理池 + 身份提取器
svc = ProxyService(
    provider=ListProxyProvider([
        "http://proxy1:8080",
        "http://proxy2:8080",
        "http://proxy3:8080",
    ]),
    identity_extractor=my_extractor,
)

async with Net(proxy=svc) as net:
    # 同一身份 → 同一代理(sticky)
    await net.get(url, cookies={"session": "user_a"})  # → proxy1
    await net.get(url, cookies={"session": "user_a"})  # → proxy1

    # 不同身份 → 不同代理
    await net.get(url, cookies={"session": "user_b"})  # → proxy2

    # 无身份 → 默认代理
    await net.get(url)

身份提取器的签名为 Callable[[RequestModel], str | None],可以从请求的任意字段提取身份:

extractor_examples.py
# 从 headers 提取
def from_header(req: RequestModel) -> str | None:
    return (req.headers or {}).get("Authorization")

# 从 URL 参数提取
def from_params(req: RequestModel) -> str | None:
    return (req.url_params or {}).get("token")

# 从 cookies 提取
def from_cookie(req: RequestModel) -> str | None:
    return (req.cookies or {}).get("session")

身份路由可以和域名路由、代理链一起使用:

identity_with_rules.py
svc = ProxyService(
    provider=ListProxyProvider(proxies),
    identity_extractor=my_extractor,
    transit="http://127.0.0.1:7897",     # 代理链
    rules={"*.cn": "direct"},            # 国内直连
)
代理选择优先级

域名路由(rules)> 身份路由(identity_extractor)> 默认上游代理。

即使请求带有身份标识,如果域名命中了 rules 规则,仍然走规则指定的代理或直连。

完整示例见 examples/proxy/identity_routing.py

异步代理

异步客户端搭配 ProxyService 使用,循环验证所有异步引擎均支持代理:

async_proxy.py
import asyncio
from hs_net import EngineEnum, Net, ProxyService

PROXY = "http://127.0.0.1:8080"
TEST_URL = "http://ip-api.com/json/"

ASYNC_ENGINES = [
    EngineEnum.HTTPX,
    EngineEnum.AIOHTTP,
    EngineEnum.CURL_CFFI,
    EngineEnum.REQUESTS_GO,
]

async def main():
    svc = ProxyService(PROXY)

    for engine in ASYNC_ENGINES:
        async with Net(proxy=svc, engine=engine, retries=0, timeout=30) as net:
            resp = await net.get(TEST_URL)
            ip = resp.json_data["query"]
            print(f"{engine.value:12s} -> IP: {ip}")

if __name__ == "__main__":
    asyncio.run(main())

完整示例见 examples/proxy/async_proxy.py

代理工作原理

当使用 ProxyService 时,hs-net 会在本地启动一个 HTTP 代理服务器(127.0.0.1 随机端口),自动处理:

  1. 协议转换:将 SOCKS4/SOCKS5 代理统一为 HTTP 代理
  2. 认证处理:自动注入代理认证头
  3. 代理链:支持通过中转代理连接上游
  4. 代理池 API:内置 ApiProxyProvider,支持同步/异步获取代理
  5. 域名路由:通过 rules 参数按域名匹配不同代理或直连
  6. 身份路由:通过 identity_extractor 按请求身份自动分配并 sticky 绑定代理
  7. 热切换:调用 switch() / async_switch() 即可切换上游代理,无需重建连接

所有引擎只需连接本地 HTTP 代理,无需各自处理 SOCKS 协议和认证。

ProxyService 生命周期

当你传入 proxy="http://..." 字符串时,Net/SyncNet 内部创建 ProxyService 并在 close() 时自动停止。

当你传入 proxy=svc(ProxyService 实例)时,close() 不会停止它,你可以跨多个客户端复用同一个 ProxyService:

reuse_proxy_service.py
from hs_net import EngineEnum, ProxyService, SyncNet

svc = ProxyService("socks5://proxy:1080")

# 同一个 ProxyService 跨多个引擎复用
for engine in [EngineEnum.HTTPX, EngineEnum.CURL_CFFI]:
    with SyncNet(proxy=svc, engine=engine) as net:
        resp = net.get("https://example.com")
    svc.switch()  # close() 不会停止 svc,switch 正常工作

# ProxyService 出作用域后自动清理