Python 中的 Selenium 轮换代理:ChromeOptions、认证与按会话轮换
Selenium 将代理绑定到浏览器启动,而非绑定到请求——这让轮换变得棘手,而带认证的轮换则以另一种方式变得棘手。以下是 Selenium 4 中真正可行的三种方案,包含 ChromeOptions 的 Python 代码、一
Selenium 没有轮换代理的概念。浏览器会话与出口 IP 在启动时就绑定在一起:你把 --proxy-server= 传入 ChromeOptions,驱动随之启动,此后该浏览器发出的每个请求都会走这个端点,直到你退出它。
正是这一设计事实,解释了为什么搜索 selenium rotating proxy 会返回如此多相互矛盾的建议。有些指南为每个代理重启一次浏览器,另一些则求助于封装库。两者都可行——但它们出问题的地方不同,而让大多数初次尝试失败的是认证,而不是轮换本身。
本指南讲解在 Python 中由 Selenium 4 驱动 Chrome 和 Chromium 时的具体机制:代理标志实际接受什么、三种可行的轮换方式、如何验证出口 IP,以及最耗时间的那些坑。
为什么 Selenium 的轮换与 requests、httpx 或 Scrapy 不同
requests和httpx把代理作为每次调用的参数。轮换只是改一个字典。- Scrapy 可以在下载器中间件中于请求之间切换代理,无需重启会话。
- Selenium 把代理设置在浏览器进程上。没有受支持的按请求代理参数,因此会话中途轮换需要诸如 selenium-wire 之类的拦截层,或借助 DevTools Protocol。
第二个限制来自 Chrome 自身的标志。--proxy-server=socks5://host:port 只接受一个协议方案、一个主机和一个端口——仅此而已。没有凭据字段。如果代理要求认证,Chrome 会弹出 HTTP 认证对话框,Selenium 无法填充,而无头模式则根本不会渲染它。某些 SOCKS5 端点同样会在没有凭据时拒绝连接,而这个标志无处承载这些凭据。
所以实际上有两个需要分别解决的问题:轮换出口 IP,以及把凭据传给代理。
三种方案对比
| 方案 | 轮换粒度 | 是否处理凭据 | 痛点 |
|---|---|---|---|
| 每个代理重启一次驱动 | 每个浏览器会话 | 否——需配合 IP 白名单 | 每次轮换都要付出一次浏览器启动的代价 |
通过 --load-extension 加载认证扩展 |
每个浏览器会话 | 是 | 扩展插装与无头模式的怪癖 |
| selenium-wire | 每个请求 | 是 | 额外依赖外加一个本地拦截层 |
大多数浏览器自动化并不需要按请求轮换。如果你的流程是打开页面、等待 JavaScript、提取数据、继续下一步,那么按会话轮换通常就足够了,而且调试起来容易得多。
方案 1:每个代理重启一次驱动
这是最不玄乎的选项,也是在生产环境中最容易推理的。
import random
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
# 文档示例取自 RFC 5737 的地址段——请替换为你自己的端点。
PROXIES = [
'http://198.51.100.10:8080',
'http://198.51.100.11:8080',
'socks5://203.0.113.7:1080',
]
def build_driver(proxy_url: str) -> webdriver.Chrome:
options = Options()
options.add_argument(f'--proxy-server={proxy_url}')
options.add_argument('--headless=new')
options.add_argument('--disable-dev-shm-usage')
return webdriver.Chrome(options=options)
def exit_ip(driver: webdriver.Chrome) -> str:
driver.get('https://api.ipify.org')
return driver.find_element(By.TAG_NAME, 'body').text.strip()
for proxy in random.sample(PROXIES, len(PROXIES)):
driver = build_driver(proxy)
try:
print(f'{proxy} -> {exit_ip(driver)}')
finally:
driver.quit()
有三个习惯能把一个能跑通的脚本和一个会莫名其妙在一夜之间挂掉的脚本区分开来:
- 一定要在
finally块中退出。游离的 chromedriver 进程会占用内存和文件锁。 - 用
--user-data-dir=/tmp/chrome-worker-1给每个并行工作进程分配自己的配置文件目录。两个 Chrome 实例共享同一配置文件会争抢锁。 - 仅在你掌控的容器中使用
--no-sandbox,绝不要在普通桌面会话中使用。
这种方案与无需认证的代理配合得干净利落,而 IP 白名单正是为此而来。
方案 2:把凭据送达浏览器
选项 A:将自己的出口 IP 加入白名单
许多服务商允许你授权自己发起调用所在的 IP,此后代理无需用户名或密码就会接受你,方案 1 可以原样使用。有两点需要注意:你的出口 IP 必须稳定,这在 CI 中或会重新拨号的家庭宽带上很麻烦;而列入白名单的 IP 是一种长期有效的授权,你需要记得撤销。
选项 B:用扩展应答认证提示
Chrome 扩展可以通过 chrome.webRequest.onAuthRequired 提供代理凭据。一个最小的 Manifest V3 组合如下。
{
"manifest_version": 3,
"name": "proxy-auth",
"version": "1.0",
"permissions": ["webRequest", "webRequestAuthProvider"],
"host_permissions": ["<all_urls>"],
"background": { "service_worker": "background.js" }
}
chrome.webRequest.onAuthRequired.addListener(
(details, callback) => {
callback({ authCredentials: { username: 'USER', password: 'PASS' } });
},
{ urls: ['<all_urls>'] },
['asyncBlocking']
);
在驱动启动时加载它:
options.add_argument('--disable-extensions-except=/opt/proxy-auth')
options.add_argument('--load-extension=/opt/proxy-auth')
在基于此做开发之前,有两点需要知道。认证回调的处理方式在不同 Chrome 版本间有所变化;如果回调形式被拒绝,请改为从监听器返回凭据对象,而不是调用 callback()。另外,无头模式下的扩展加载是最容易让你意外的地方——请使用 --headless=new,而不是旧版无头模式。
选项 C:selenium-wire
如果你确实需要按请求或会话中途轮换,selenium-wire 会在浏览器前面放一个小型本地代理,并让你在 Python 中设置上游代理。
from selenium.webdriver.common.by import By
from seleniumwire import webdriver
proxy = 'http://USER:[email protected]:8080'
wire_options = {
'proxy': {
'http': proxy,
'https': proxy,
'no_proxy': 'localhost,127.0.0.1',
}
}
driver = webdriver.Chrome(seleniumwire_options=wire_options)
try:
driver.get('https://api.ipify.org')
print(driver.find_element(By.TAG_NAME, 'body').text)
finally:
driver.quit()
由于涉及本地拦截层,请将 selenium-wire 与 Selenium 的版本一并锁定,并在其中任一升级后重新测试。
Selenium 是否特别需要 SOCKS5?
通常不需要,但它是受支持的。Chrome 在 --proxy-server 中接受 socks5://host:port,而 SOCKS5 在套接字层面转交流量,当你的代理池只有 SOCKS 时这很有用。凭据方面的限制完全相同:这个标志无处放置用户名,所以你还是要回到白名单或扩展的方案。
有一点澄清对验证很重要:让浏览器走 SOCKS5 控制的是 TCP 流量。它本身并不能阻止 WebRTC 打开一条不经代理的 UDP 路径。参见下面的检查清单。
为浏览器自动化选择轮换策略
| 策略 | 轮换触发条件 | 适用场景 | 取舍 |
|---|---|---|---|
| 每个任务粘性会话 | 任务开始时 | 登录、多步骤流程、分页列表 | 一个 IP 承担整个任务 |
| 每 N 个页面或每个时间窗口 | 计数器或定时器 | 大范围抓取、列表页 | 流程中途轮换可能破坏状态 |
| 每个浏览器会话 | 每次启动驱动 | 直接的抓取与截图任务 | 启动开销主导运行时间 |
| 每个工作进程一个代理 | 工作进程启动时 | 并行抓取 | 需要跟踪每个 IP 的负载 |
由于浏览器会话开销高昂,按请求轮换通常是 Selenium 的错误默认选项——你会为每个 URL 付出一次进程重启的代价。对于依赖地理位置的结果,位置一致性也比单纯的 IP 数量更重要。如果你在针对某个特定市场采样本地化页面、店铺页面或价格,请在整个会话期间固定出口地区,否则你收集到的数字在下一次运行时将无法复现。
验证出口 IP、DNS 与 WebRTC
对于无头运行来说,验证不是可选项,因为没有窗口可以瞥一眼。检查三件事。
1. 出口 IP 确实变了。 复用上面的 exit_ip 辅助函数,并断言该值与你自己的地址不同。偶尔再对比第二个回显端点,以免被缓存的响应所蒙蔽。
2. DNS 解析行为。 使用 HTTP 代理时,主机名通常由代理解析,这正是你想要的结果。SOCKS5 的处理方式值得确认而非想当然:在一次非无头运行中打开公开的 DNS 泄漏测试页面,将显示的解析器与你的 ISP 进行对比。
3. WebRTC。 即便已配置代理,Chrome 仍可能使用不经代理的 UDP 路径。下面的标志会强制 WebRTC 避开不经代理的 UDP。
options.add_argument('--force-webrtc-ip-handling-policy=disable_non_proxied_udp')
最后,为每个会话记录所用的代理和观测到的出口 IP。当页面返回验证挑战、错误的语言或你没预料到的价格时,那行日志会是你最先想看到的东西。
最浪费时间的一些坑
- 驱动泄漏。 每个游离的 chromedriver 都会占用内存。在容器中,这通常表现为莫名其妙的崩溃,而不是看起来像泄漏。到处都要用
try/finally。 - 共享配置文件目录。 共享同一用户数据目录的并行实例会互相阻塞。每个工作进程一个目录。
- 把失败都当成封禁。 403、空响应和验证挑战页是三个不同的问题。在归咎于代理之前,先记录状态码、页面标题和正文长度。
- 从一个页面就外推结论。 在自动化一百个页面之前先验证出口 IP,而不是之后。
- 忘了非技术层面的规则。 轮换 IP 并不会改变网站的服务条款、它的 robots.txt 或适用的数据保护法律所允许的范围——尤其是在 GDPR 司法辖区内收集个人数据时。放慢速度,遇到错误就退避,并保持适度的并发。
简要总结
- Selenium 把代理绑定到浏览器启动上,所以除非你加一个拦截层,轮换就意味着重启。
- Chrome 的
--proxy-server标志无法携带凭据。请选择 IP 白名单、认证扩展或 selenium-wire。 - 对于浏览器自动化,优先选择按会话轮换或粘性轮换,而不是按请求轮换。
- 在会话内部验证出口 IP,禁用不经代理的 WebRTC UDP,并记录哪个代理服务了哪个请求。
- 在
finally中退出驱动,为每个工作进程隔离配置文件目录,并在扩大规模之前先在一个 URL 上验证整套配置。