圆海博客-探寻心灵的宁静

您现在的位置是:首页 > 博客 > 正文

博客

Python设置代理动态IP教程:urllib与requests双库适配指南

2026-09-11 08:55:30博客
做爬虫、做数据采集、做多账号运营的朋友,大概率都遇到过同一个问题:目标网站频繁封IP。你刚跑完一轮请求,下一条就返回 403 或者验证码弹窗,IP 被拉黑了。这时候,动态代理 IP……

做爬虫、做数据采集、做多账号运营的朋友,大概率都遇到过同一个问题:目标网站频繁封IP。你刚跑完一轮请求,下一条就返回 403 或者验证码弹窗,IP 被拉黑了。这时候,动态代理 IP 就成了刚需。

Python 生态里最常用的两个 HTTP 库——urllib(标准库)和 requests(第三方库)——设置代理的方式完全不同。一个靠 Handler 链式构建,一个靠字典传参。很多开发者只熟悉其中一种,换库就卡住。这篇文章把两个库的代理配置、动态 IP 轮换、异常重试全部讲透,代码可以直接复制运行。

一、为什么需要动态代理 IP

静态代理 IP 用完就废,一个 IP 被标记后短期内无法恢复。动态代理 IP 的核心优势在于:每次请求(或每隔一段时间)自动更换出口 IP,让目标网站始终”看到”不同的访问者,从而绕过频率限制和 IP 黑名单。

典型使用场景包括:

  • 大规模网页数据采集,需要持续稳定的出口
  • 多账号注册、养号、内容发布
  • 价格监控、库存追踪等高频轮询任务
  • 海外平台(如 TikTok、Amazon)的合规访问

这里要提醒一点:光络云的代理 IP 服务需要客户自身具备海外网络环境才能正常使用(TikTok 专线除外)。如果你的服务器部署在国内且没有海外出口,建议先确认网络链路再配置代理。

二、urllib 设置代理动态 IP

urllib 是 Python 标准库,无需额外安装。它设置代理的核心思路是:创建 ProxyHandler → 构建 Opener → 全局安装,三步走。

2.1 基础配置

import urllib.request
import urllib.error

# 代理地址(以光络云为例,替换为你自己的代理信息)
proxy_url = "http://user:password@proxy.guangluoyun.com:8080"

# 第一步:创建代理处理器
proxy_handler = urllib.request.ProxyHandler({
    'http':  proxy_url,
    'https': proxy_url
})

# 第二步:构建自定义 Opener
opener = urllib.request.build_opener(proxy_handler)

# 第三步:全局安装(之后所有 urllib.request.urlopen 都走代理)
urllib.request.install_opener(opener)

# 测试请求
try:
    resp = urllib.request.urlopen("https://httpbin.org/ip", timeout=10)
    print(resp.read().decode())
except urllib.error.URLError as e:
    print(f"请求失败: {e.reason}")

注意 install_opener全局生效的,一旦调用,当前进程中所有通过 urllib.request 发出的请求都会经过代理。如果你只想让部分请求走代理,不要调用 install_opener,而是直接用 opener.open(url)

2.2 动态 IP 轮换

光络云提供的动态代理,每次连接会分配不同的出口 IP。在 urllib 中实现轮换有两种方式:

方式一:每次请求重建 Opener(推荐)

import urllib.request
import random
import time

def make_opener(proxy_list):
    """每次调用生成新的 Opener,绑定随机代理"""
    proxy = random.choice(proxy_list)
    handler = urllib.request.ProxyHandler({
        'http': proxy,
        'https': proxy
    })
    return urllib.request.build_opener(handler)

# 你的代理池(从光络云控制台获取)
proxy_pool = [
    "http://user:pass@node1.guangluoyun.com:8080",
    "http://user:pass@node2.guangluoyun.com:8080",
    "http://user:pass@node3.guangluoyun.com:8080",
]

def fetch_with_rotation(url, proxy_pool, max_retries=3):
    for attempt in range(max_retries):
        opener = make_opener(proxy_pool)
        try:
            resp = opener.open(url, timeout=10)
            return resp.read().decode()
        except Exception as e:
            print(f"第{attempt+1}次失败: {e}")
            time.sleep(1)
    raise RuntimeError("所有代理均失败")

result = fetch_with_rotation("https://httpbin.org/ip", proxy_pool)
print(result)

方式二:利用光络云动态特性,单条代理地址自动轮换

光络云的动态代理支持在同一个入口地址上,每次 TCP 连接自动分配不同出口 IP。这意味着你甚至不需要维护代理池,每次新建连接就会拿到新 IP

import urllib.request
import time

proxy = "http://user:pass@dynamic.guangluoyun.com:8080"

for i in range(5):
    handler = urllib.request.ProxyHandler({'http': proxy, 'https': proxy})
    opener = urllib.request.build_opener(handler)
    resp = opener.open("https://httpbin.org/ip", timeout=10)
    print(f"第{i+1}次出口IP: {resp.read().decode()}")
    time.sleep(1)

运行后你会看到每次返回的 IP 都不一样——这就是动态代理的效果。

三、requests 设置代理动态 IP

requests 库的代理配置比 urllib 简洁得多,核心就是一个 proxies 字典。它支持三种传入方式:单次请求参数、Session 级别、环境变量

3.1 单次请求传参

import requests

proxies = {
    'http':  'http://user:pass@dynamic.guangluoyun.com:8080',
    'https': 'https://user:pass@dynamic.guangluoyun.com:8080'
}

resp = requests.get(
    "https://httpbin.org/ip",
    proxies=proxies,
    timeout=10
)
print(resp.json())

这是最灵活的方式——每次请求可以传不同的 proxies,天然适配动态 IP 轮换。

3.2 Session 级别复用

如果你需要连续请求同一个站点(比如翻页采集),用 Session 可以复用 TCP 连接,减少握手开销:

import requests
import random
import time

session = requests.Session()
session.proxies = {
    'http':  'http://user:pass@dynamic.guangluoyun.com:8080',
    'https': 'https://user:pass@dynamic.guangluoyun.com:8080'
}

# 连续翻页
for page in range(1, 11):
    url = f"https://example.com/list?page={page}"
    try:
        resp = session.get(url, timeout=10)
        resp.raise_for_status()
        print(f"第{page}页: {resp.status_code}, {len(resp.text)} 字节")
    except requests.RequestException as e:
        print(f"第{page}页失败: {e}")
        # 失败时重置 Session,强制新连接(新 IP)
        session.close()
        session = requests.Session()
        session.proxies = {
            'http':  'http://user:pass@dynamic.guangluoyun.com:8080',
            'https': 'https://user:pass@dynamic.guangluoyun.com:8080'
        }
    time.sleep(random.uniform(1, 3))

关键点:当请求失败时,session.close() 会断开底层 TCP 连接,下次请求会建立新连接,从而触发动态代理分配新 IP。

3.3 环境变量方式(不推荐用于生产)

import os
os.environ['http_proxy']  = 'http://user:pass@dynamic.guangluoyun.com:8080'
os.environ['https_proxy'] = 'https://user:pass@dynamic.guangluoyun.com:8080'

# 之后所有 requests 请求自动走代理
resp = requests.get("https://httpbin.org/ip", timeout=10)
print(resp.json())

这种方式会污染整个进程的环境变量,在多线程或多代理场景下容易出 bug,生产环境建议用显式传参

四、动态 IP 轮换策略与异常处理

光有代理地址还不够,真正让爬虫稳定跑起来的是轮换策略异常兜底。下面给出一套通用的轮换框架,urllib 和 requests 都能套用。

4.1 代理池管理

import random
import time
import threading

class ProxyPool:
    """轻量级代理池,支持失败标记与冷却恢复"""

    def __init__(self, proxies: list, cooldown=60):
        self._lock = threading.Lock()
        self._pool = {p: {'status': 'ok', 'fail_time': 0} for p in proxies}
        self._cooldown = cooldown  # 冷却秒数

    def get(self):
        """随机取一个可用代理"""
        with self._lock:
            available = [
                p for p, info in self._pool.items()
                if info['status'] == 'ok'
            ]
            if not available:
                # 全部失败,强制等待冷却
                time.sleep(5)
                self._recover()
                available = list(self._pool.keys())
            return random.choice(available)

    def mark_fail(self, proxy):
        with self._lock:
            self._pool[proxy]['status'] = 'fail'
            self._pool[proxy]['fail_time'] = time.time()

    def mark_ok(self, proxy):
        with self._lock:
            self._pool[proxy]['status'] = 'ok'

    def _recover(self):
        now = time.time()
        for p, info in self._pool.items():
            if info['status'] == 'fail' and (now - info['fail_time']) >= self._cooldown:
                info['status'] = 'ok'

4.2 通用请求函数(requests 版)

import requests

def safe_request(url, pool: ProxyPool, max_retries=3, **kwargs):
    """带代理轮换的安全请求"""
    last_err = None
    for i in range(max_retries):
        proxy = pool.get()
        proxies = {
            'http':  proxy,
            'https': proxy.replace('http://', 'https://')
        }
        try:
            resp = requests.get(url, proxies=proxies, timeout=10, **kwargs)
            if resp.status_code == 200:
                pool.mark_ok(proxy)
                return resp
            elif resp.status_code in (403, 429):
                # 被风控,标记失败换 IP
                pool.mark_fail(proxy)
                last_err = f"HTTP {resp.status_code}"
            else:
                return resp  # 其他状态码直接返回
        except requests.RequestException as e:
            pool.mark_fail(proxy)
            last_err = str(e)
        time.sleep(1)
    raise RuntimeError(f"重试{max_retries}次仍失败: {last_err}")

4.3 通用请求函数(urllib 版)

import urllib.request
import urllib.error

def safe_request_urllib(url, pool: ProxyPool, max_retries=3):
    last_err = None
    for i in range(max_retries):
        proxy = pool.get()
        handler = urllib.request.ProxyHandler({
            'http': proxy,
            'https': proxy
        })
        opener = urllib.request.build_opener(handler)
        try:
            resp = opener.open(url, timeout=10)
            if resp.status == 200:
                pool.mark_ok(proxy)
                return resp.read().decode()
            elif resp.status in (403, 429):
                pool.mark_fail(proxy)
                last_err = f"HTTP {resp.status}"
            else:
                return resp.read().decode()
        except urllib.error.HTTPError as e:
            pool.mark_fail(proxy)
            last_err = f"HTTPError {e.code}"
        except urllib.error.URLError as e:
            pool.mark_fail(proxy)
            last_err = str(e.reason)
        time.sleep(1)
    raise RuntimeError(f"重试{max_retries}次仍失败: {last_err}")

4.4 轮换策略选择

策略 适用场景 实现要点
每请求换 IP 高频采集、反爬严格的站点 每次新建连接 / 新建 Opener
每 N 秒换 IP 中频轮询(如价格监控) 记录上次换 IP 时间,超时则重连
失败才换 IP 低频、对 IP 连续性有要求 捕获 403/429/超时后触发换 IP
固定 IP 绑定 需要同一地区出口(如海外电商) 指定光络云特定地区节点

五、urllib 与 requests 代理配置对比

两个库的代理机制差异总结:

对比项 urllib requests
配置方式 ProxyHandler + build_opener proxies 字典
生效范围 install_opener 全局 / opener.open 局部 单次参数 / Session / 环境变量
动态切换 需重建 Opener 对象 每次传不同 proxies 即可
连接复用 需手动管理(或配合 http.client) Session 自动管理连接池
依赖 标准库,零依赖 需 pip install requests
推荐场景 轻量脚本、不想装第三方包 生产项目、高频请求、需要 Session

简单说:写一次性脚本用 urllib 够了,做正经项目用 requests。如果项目里两个库都在用(比如某些 SDK 底层依赖 urllib),就需要同时配置,注意别冲突。

六、常见坑与排错

坑 1:HTTPS 代理用了 http:// 前缀

在 requests 中,https 键对应的值建议用 https:// 前缀(如果代理支持 HTTPS CONNECT)。urllib 中两者可以都写 http://,urllib 会自动处理隧道。

坑 2:代理地址中的特殊字符

如果用户名或密码包含 @:# 等字符,必须做 URL 编码:

from urllib.parse import quote
password = "p@ss:word"
encoded = quote(password, safe='')
proxy = f"http://user:{encoded}@host:port"

坑 3:urllib 的 install_opener 污染全局

调用过 install_opener 后,想”取消”代理并没有直接的 API。正确做法是:不要调用 install_opener,始终用 opener.open() 局部指定。

坑 4:超时设置缺失

代理链路多了一跳,默认超时可能不够。urllib 的 urlopen 和 requests 的 get 都要显式传 timeout,建议 10~15 秒。

坑 5:光络云代理需要海外网络环境

再次强调:光络云代理 IP(TikTok 专线除外)需要你的服务器或本地机器具备海外网络出口才能正常连通代理节点。如果在国内直连代理节点不通,请先解决网络环境问题,再配置代码。

常见问题

Q: urllib 和 requests 可以同时使用不同的代理吗?
可以。urllib 的代理通过 Opener 控制,requests 的代理通过 proxies 参数控制,两者互不干扰。只要你不调用 urllib.request.install_opener,就不会影响 requests 的行为。

Q: 光络云的动态代理,一个入口地址能同时支持多少并发?
具体并发上限取决于你选择的套餐规格。建议在光络云控制台查看当前套餐的并发连接数限制,超出后新连接会排队或报错。如果业务并发较高,可以联系光络云客服确认是否需要升级。

Q: 用 requests 的 Session 时,怎么确保每次请求都是新 IP?
Session 会复用底层 TCP 连接,而动态代理的 IP 分配是在 TCP 连接建立时完成的。所以需要在每次请求前调用 session.close() 断开连接,下次请求时会自动建立新连接、拿到新 IP。或者干脆不用 Session,每次直接 requests.get(url, proxies=...)

Q: 代理 IP 被目标网站标记了怎么办?
动态代理的优势就在于”换”。被标记后,该 IP 在光络云侧会被标记为异常,短期内不再分配给你。你的代码只需做好失败重试 + 换 IP 的逻辑(如本文第四节的 ProxyPool),系统会自动切换到干净 IP 继续工作。

Q: 我在国内服务器,能直接用光络云代理吗?
光络云的代理 IP 产品(TikTok 专线除外)需要客户自身具备海外网络环境才能使用。如果你的服务器在国内且没有海外出口线路,需要先解决网络连通性问题。TikTok 专线产品则专门针对国内访问 TikTok 场景设计,可直接使用。