python代理ip配置教程,2026年爬虫老手都在用的写法

写爬虫这行干了好几年,我越来越觉得,代理ip配置这件事,90%的人第一步就写歪了。不是代码跑不通,而是你用的写法在2026年已经明显不够用了——要么ip用着不稳定,要么并发一上来就全崩,要么每次换个项目还得重新改一遍代理配置,烦得很。
这篇文章不聊什么高大上的架构设计,就踏踏实实讲一件事:python里代理ip到底怎么配、怎么管、怎么在真实项目里跑得稳。都是踩坑踩出来的经验,你照着改,基本能少走很多弯路。
先把协议搞明白,别一上来就抄代码
很多人写代理配置,上来就是 proxies = {"http": "http://ip:port"},然后发现有些站点走不通,又不知道为啥。根源在于你没搞清楚自己拿到的代理ip支持什么协议。
目前国内代理ip服务商提供的,基本就三种协议,区别还挺大的:
| 协议类型 | 适用场景 | 配置前缀 | 注意事项 |
|---|---|---|---|
| HTTP | 普通网页采集、API调用 | http:// | 最通用,绝大多数场景够用 |
| HTTPS | 目标站点是加密页面 | https:// | 代理本身要支持TLS透传,否则握手会失败 |
| SOCKS5 | 对协议层要求更高的场景 | socks5:// | 需要额外装 pysocks 库,requests原生不支持 |
说白了,你从服务商那边拿到的ip,先确认它支持哪种协议。我见过有人拿SOCKS5的ip硬塞进HTTP配置里,调了半天以为是代码bug,其实协议对不上,根本连不通。
requests配代理ip,别再硬编码了
最基础的写法大家都熟,但我想说的是——把ip地址直接写在代码里,是2026年最不该干的事。你换个ip就得改代码,部署到服务器上还得改一遍,团队协作的时候更是灾难。
我现在的习惯是,代理ip信息一律走环境变量或者配置文件,代码里只读变量:
import os
import requests
# 从环境变量读取,部署时改环境变量就行,代码不用动
PROXY_HOST = os.getenv("PROXY_HOST", "")
PROXY_PORT = os.getenv("PROXY_PORT", "")
PROXY_USER = os.getenv("PROXY_USER", "")
PROXY_PASS = os.getenv("PROXY_PASS", "")
def build_proxies():
if not PROXY_HOST:
return None
auth = f"{PROXY_USER}:{PROXY_PASS}@" if PROXY_USER else ""
proxy_url = f"http://{auth}{PROXY_HOST}:{PROXY_PORT}"
return {
"http": proxy_url,
"https": proxy_url
}
session = requests.Session()
session.proxies = build_proxies()
session.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"})
resp = session.get("https://httpbin.org/ip", timeout=10)
print(resp.json()) 看看出口ip是不是代理的那个
如果你用的是SOCKS5协议,记得先装一下 pip install pysocks,然后把前缀改成 socks5:// 就行,其他逻辑不变。
还有一种更省事的方式:如果你的代理ip服务商提供了隧道代理入口(一个固定地址,背后自动帮你轮换ip),那你连ip池都不用维护了,代码里就写一个固定地址,剩下的交给服务端调度。这种模式对开发来说最省心,后面我会细说。
代理ip池管理:老手真正在意的部分
单条代理ip配置谁都会,但真正的项目里,你面对的是几十上百个ip,有的能用有的不能用,有的用着用着就失效了。这时候你需要一套简单的池子管理逻辑。
核心就三件事:获取、验证、淘汰。
import random
import time
import requests
class ProxyPool:
def __init__(self, api_url, max_retries=3):
"""
api_url: 你的代理ip服务商提供的提取接口
每次调用返回一个可用的ip:port
"""
self.api_url = api_url
self.max_retries = max_retries
self.pool = []
self.failed = set()
def fetch_one(self):
"""从服务商接口拿一个ip"""
try:
r = requests.get(self.api_url, timeout=5)
ip = r.text.strip()
if ip and ip not in self.failed:
return ip
except Exception:
pass
return None
def get_proxy(self):
"""优先从池里取,池空了就去拉新的"""
if self.pool:
return self.pool.pop()
return self.fetch_one()
def mark_failed(self, ip):
self.failed.add(ip)
def request_with_proxy(self, url, kwargs):
"""带重试的代理请求"""
for attempt in range(self.max_retries):
ip = self.get_proxy()
if not ip:
time.sleep(1)
continue
proxies = {
"http": f"http://{ip}",
"https": f"http://{ip}"
}
try:
resp = requests.get(url, proxies=proxies, timeout=8, kwargs)
if resp.status_code == 200:
用完放回池里(短效ip在存活期内还能再用)
self.pool.append(ip)
return resp
else:
self.mark_failed(ip)
except (requests.ProxyError, requests.ConnectionError, requests.Timeout):
self.mark_failed(ip)
raise Exception("所有代理ip均不可用,请检查ip池或服务商状态")
# 使用示例
pool = ProxyPool(api_url="你的ip提取接口地址")
resp = pool.request_with_proxy("https://httpbin.org/html")
print(resp.status_code)
这段代码看着有点长,但逻辑其实很直白:每次请求先拿一个ip,失败了就标记掉,换下一个,重试次数到了就报错。你不需要搞什么复杂的队列,一个列表加一个失败集合,日常项目完全够用。
这里有个细节很多人忽略:短效ip是有存活时长的。比如你拿到的ip只能活5分钟,那你把它放回池子里之前,最好记一下获取时间,超时的就别再用了。不然你以为ip还在,实际上人家已经下线了,白白浪费一次重试。
多线程和异步场景,代理ip别乱用
一上多线程,代理ip的问题就来了。最典型的错误:所有线程共用一个ip,并发一高,那个ip直接被目标站点限流,然后全部线程一起挂。
正确做法是每个线程(或每个协程)绑定自己的ip,或者至少是从小池子里各取各的:
import threading
import requests
def worker(thread_id, proxy_ip, url, results, lock):
proxies = {"http": f"http://{proxy_ip}", "https": f"http://{proxy_ip}"}
try:
resp = requests.get(url, proxies=proxies, timeout=10)
with lock:
results[thread_id] = resp.status_code
except Exception as e:
with lock:
results[thread_id] = str(e)
# 假设你从服务商拿到了5个ip
ips = ["ip1:port1", "ip2:port2", "ip3:port3", "ip4:port4", "ip5:port5"]
url = "https://httpbin.org/ip"
results = {}
lock = threading.Lock()
threads = []
for i, ip in enumerate(ips):
t = threading.Thread(target=worker, args=(i, ip, url, results, lock))
threads.append(t)
t.start()
for t in threads:
t.join()
print(results)
如果是用 asyncio + aiohttp 的异步写法,思路一样,每个协程任务分配不同的ip就行。aiohttp配代理稍微注意一下,它用的是 proxy 参数而不是 proxies 字典:
import aiohttp
import asyncio
async def fetch_with_proxy(session, url, proxy_ip):
proxy = f"http://{proxy_ip}"
async with session.get(url, proxy=proxy, timeout=aiohttp.ClientTimeout(total=10)) as resp:
return await resp.text()
async def main():
ips = ["ip1:port1", "ip2:port2", "ip3:port3"]
url = "https://httpbin.org/ip"
async with aiohttp.ClientSession() as session:
tasks = [fetch_with_proxy(session, url, ip) for ip in ips]
results = await asyncio.gather(tasks, return_exceptions=True)
for r in results:
print(r[:100] if isinstance(r, str) else r)
asyncio.run(main())
几个我反复踩过的坑,你大概率也会遇到
超时设置太短。 代理ip本身多了一跳网络,延迟比直连高。你设个 timeout=3,稍微网络波动一下就直接超时了。我的经验是,连接超时给5秒,读取超时给10到15秒,别太抠。
HTTPS站点配HTTP代理。 有些目标站点是强制HTTPS的,你代理配的是 http:// 前缀,requests会尝试通过HTTP代理去连HTTPS目标,这个流程本身没问题,但你的代理服务商必须支持TLS透传。如果不支持,你会看到 SSLError 或者 ConnectionReset。这时候要么让服务商开HTTPS支持,要么把代理前缀也改成 https://。
ip用完了不知道去哪补。 短效ip用完就没了,你得有自动补货的机制。如果手动去后台一个个提,效率太低。靠谱的做法是,你的服务商提供API提取接口,代码里直接调接口拿新ip,不用人盯着。我用的网帆代理在这块做得比较顺,短效动态ip支持按量提取,接口返回就是ip:port,拿来就能用,不用额外解析。
忘了设User-Agent。 代理ip帮你换了出口地址,但你的UA还是python-requests/2.x,目标站点一看就知道是脚本。这个跟代理ip本身没关系,但经常一起出问题,顺手改一下。
选代理ip服务商,我实际在意的几个点
代理ip这个东西,代码写得再漂亮,ip本身质量不行也白搭。我选服务商主要看三样东西:
第一,ip纯净度和来源。是不是运营商正规线路出来的,IP池子够不够大。ip太”脏”的话,目标站点风控一开你就全挂。我目前用的网帆代理,ip是三大运营商合规线路直供的,纯净度标称99.8%以上,动态ip储备量在3000万级别,覆盖全国300多个省市,日常采集够用了。
第二,存活时长和提取方式。短效ip你能自定义存活时间(3分钟到30分钟都有),提取接口响应快,不用排队等。网帆这块支持1到30分钟自由定制,提取是毫秒级的,没有并发上限,我跑多线程采集的时候没遇到过”提ip要等”的情况。
第三,计费是否透明。有的服务商看着单价低,但并发限制、提取冷却、超时扣费这些隐形条款一堆。网帆是包量和包月两种模式,包量最低到0.0023元一个ip,包月长期用有折扣,没有那种”用超了才告诉你”的坑。新注册还能领2000个免费测试ip,够你跑通整个流程再决定要不要正式用。
另外他们有个隧道代理产品,如果你不想自己维护ip池,接入一个固定隧道地址就行,后端自动帮你轮换ip,还能在后台实时看到ip消耗和运行状态。对开发来说确实省了不少运维精力,尤其是项目周期短、不想搭ip管理逻辑的时候,直接用隧道最省事。
常见问题
Q1:我配了代理ip,但打印出口ip还是我自己的,怎么回事?
大概率是三个原因之一:一是代理地址格式写错了,比如多了个空格、端口号漏了、协议前缀不对;二是你的代码里某处用了 requests.get() 而不是你配了代理的那个 session 对象,等于绕过了代理;三是代理ip本身已经失效了,请求直接超时后走了直连(如果你代码里有fallback逻辑的话)。排查顺序:先单独测一下 requests.get("https://httpbin.org/ip", proxies=你的代理配置),看返回的ip对不对,再往回查。
Q2:多线程采集时,代理ip怎么分配最合理?
原则就一条:一个线程一个ip,别共用。如果你线程数是20,那就同时持有20个ip,每个线程绑定一个。ip失效了就从池子里补一个新的给那个线程。不要搞”所有线程从同一个池子里随机取”,那样两个线程可能取到同一个ip,并发一高就触发限流。线程数也不要盲目拉高,ip池子就那么多,线程数超过可用ip数没有意义。
Q3:短效ip和长效ip,我该怎么选?
看你的业务节奏。如果你的采集是高频、短周期的——比如每隔几分钟跑一轮,每轮请求量不大,跑完就停——短效动态ip就够了,成本低,用完即弃。但如果你有一个服务需要持续在线运行,比如一个定时任务每小时跑一次、每次要访问几十个页面,用短效ip的话每次都得重新提取,ip频繁更换也容易触发目标站点的风控。这种场景用长效动态ip更合适,ip能稳定在线1到24小时,环境一致,不容易被标记。网帆的长效ip支持精确到区县级地域筛选,如果你需要固定某个城市的出口,这个功能比较实用。
Q4:代理ip突然全部不可用了,代码层面怎么兜底?
别慌,先确认是ip的问题还是网络的问题。代码层面建议加两层保护:一是重试机制,单个ip失败后换下一个,连续失败超过阈值(比如5个)就暂停请求,等30秒再恢复,避免疯狂打一个已经挂了的ip池;二是降级策略,如果代理全部不可用,可以临时走直连(如果业务允许的话),同时触发告警通知你去检查服务商状态。千万别让程序在代理全挂的情况下死循环重试,把目标站点打得更严,反而更麻烦。
代理ip配置这件事,说复杂也复杂,说简单也就是”拿ip、配进去、失败了换下一个”这么个循环。但真正让代码跑得稳的,是那些细节——超时怎么设、ip存活时间怎么管、多线程怎么分配、失效了怎么兜底。把这些理顺了,你写出来的采集脚本才能长期稳定地跑,而不是今天能跑明天就崩。
