市场研究、价格监控、广告验证这类业务任务,很多页面是 JS 动态渲染的,接口拿不到完整内容,只能让真实浏览器去渲染,Selenium 也因此成了这类任务的标配。但任务一旦跨区域执行,就会遇到一个很现实的问题:全部请求从单一出口发出,区域不对、链路不稳,成功率随规模明显下降。这时候就需要代理,让浏览器从目标市场的网络出口发起请求,把负载分散到多个出口上,任务才能长期稳定地跑。
这篇文章从 Selenium 的代理配置原理讲起,覆盖无认证与带认证两种配置方式、代理生效验证、轮换与粘性会话的策略选择,最后给出常见问题排查清单,读完可以直接套用到自己的项目里。
一、Selenium 代理配置的基础
(一)无认证代理
Chrome 系浏览器支持通过启动参数直接指定代理服务器,是最省事的配置方式:
python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('--proxy-server=http://gate.b2proxy.com:7777')
driver = webdriver.Chrome(options=options)
Firefox 的配置方式略有不同,通过 preferences 设置:
python
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
options = Options()
options.set_preference('network.proxy.type', 1)
options.set_preference('network.proxy.http', 'gate.b2proxy.com')
options.set_preference('network.proxy.http_port', 7777)
options.set_preference('network.proxy.ssl', 'gate.b2proxy.com')
options.set_preference('network.proxy.ssl_port', 7777)
driver = webdriver.Firefox(options=options)
一个容易忽略的点:--proxy-server 只能配置不带认证的代理。网关式代理服务普遍需要用户名密码,直接把 http://user:pass@host:port 写进去通常不会生效,需要下面的方案。
(二)带认证代理
Selenium 本身没有暴露"为代理自动填写认证信息"的接口,社区通行做法是给浏览器装一个本地扩展:扩展负责两件事,把网关地址写入 Chrome 的代理设置,并在请求需要认证时自动填入凭据。
需要注意,Chrome 139 起已不支持 Manifest V2 扩展,所以扩展要按 V3 规范写,权限声明里必须有 webRequestAuthProvider:
python
manifest.json:
{
"name": "proxy-auth",
"version": "1.0.0",
"manifest_version": 3,
"permissions": ["proxy", "webRequest", "webRequestAuthProvider"],
"host_permissions": ["<all_urls>"],
"background": { "service_worker": "background.js" }
}
background.js:
chrome.runtime.onInstalled.addListener(() => {
const config = {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: "http",
host: "gate.b2proxy.com",
port: 7777
},
bypassList: ["localhost", "127.0.0.1"]
}
};
chrome.proxy.settings.set({ value: config, scope: "regular" }, () => {});
});
chrome.webRequest.onAuthRequired.addListener(
function (details) {
return {
authCredentials: { username: "your_username", password: "your_password" }
};
},
{ urls: ["<all_urls>"] },
["blocking"]
);
用 Python 把这两个文件打包成 zip,通过 add_extension 加载:
python
import json
import zipfile
import tempfile
import os
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def build_auth_extension(host, port, username, password):
manifest = {
"name": "proxy-auth",
"version": "1.0.0",
"manifest_version": 3,
"permissions": ["proxy", "webRequest", "webRequestAuthProvider"],
"host_permissions": ["<all_urls>"],
"background": {"service_worker": "background.js"},
}
background = """
chrome.runtime.onInstalled.addListener(() => {
const config = {
mode: "fixed_servers",
rules: {
singleProxy: { scheme: "http", host: "%s", port: %d },
bypassList: ["localhost", "127.0.0.1"]
}
};
chrome.proxy.settings.set({ value: config, scope: "regular" }, () => {});
});
chrome.webRequest.onAuthRequired.addListener(
function (details) {
return {
authCredentials: { username: "%s", password: "%s" }
};
},
{ urls: ["<all_urls>"] },
["blocking"]
);
""" % (host, port, username, password)
fd, path = tempfile.mkstemp(suffix=".zip")
with zipfile.ZipFile(path, "w") as z:
z.writestr("manifest.json", json.dumps(manifest))
z.writestr("background.js", background)
os.close(fd)
return path
ext_path = build_auth_extension(
host="gate.b2proxy.com",
port=7777,
username="your_username",
password="your_password",
)
options = Options()
options.add_extension(ext_path)
driver = webdriver.Chrome(options=options)
几个要点:
凭据建议从环境变量或配置文件读取,不要硬编码进代码;
密码含 @、#、: 等特殊字符时,先做 URL 编码再填入,这是 407 报错的高发原因;
onAuthRequired 里填错凭据时浏览器会反复重试,表现为像卡住一样,可在回调里按请求 ID 计数,失败两次就停止,方便定位问题;
部分服务商支持 IP 白名单方式(把任务机出口 IP 登记到服务端),可以免去认证环节,直接 --proxy-server 即可,适合出口 IP 固定不变的任务机。
二、验证代理是否真的生效
配置完不等于生效。最直接的办法是让浏览器访问一个回显出口信息的接口(如 ipinfo.io/json 或 httpbin.org/ip),断言出口 IP 和区域是否符合预期:
python
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import json
driver.get("https://ipinfo.io/json")
body = WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.TAG_NAME, "body"))
).text
info = json.loads(body)
print(info.get("ip"), info.get("country"), info.get("city"))
把这段放到任务启动时跑一次,出口区域不对就立刻中止,比跑完整个任务才发现数据全部来自错误区域要划算得多。
三、轮换与粘性会话:按任务类型选择出口策略
代理配置只是第一步,真正影响任务稳定性的,是出口策略的选择。
网关式代理服务(以 B2Proxy 为例)的接入方式很统一:分配一个固定的网关地址(host:port)和用户名密码,出口 IP 由服务端管理,会话类型在凭据层面配置:
Rotating:每次请求自动更换出口,适合大批量、对出口连续性没有要求的任务,比如列表页遍历;
Sticky:时间窗口内出口保持不变,适合需要会话连续性的流程,比如分步表单、多页面衔接的查询流程。
Selenium 场景有个特殊性:浏览器打开一个页面,会同时发出几十上百个请求(HTML、JS、CSS、图片、接口),这些请求最好来自同一出口。所以 Selenium 任务里更常见的是 Sticky 模式,按会话窗口分配出口,而不是每个请求都换。Sticky 模式下同一凭据默认数分钟内出口不变,到期自动更换。
另外一个和 requests 场景不同的点:requests 可以在同一个请求周期内逐个换代理,Selenium 不行,代理配置是会话级的,在 driver 创建时生效。想换出口,就要结束当前会话、用新配置重建 driver。所以轮换的粒度通常按"任务批次"来:一批页面用一个出口,跑完换下一批,而不是每个页面都重启浏览器。并发多个批次时,每个实例独立创建 driver 和 options,互不影响:
python
def build_driver(ext_path):
options = Options()
options.add_extension(ext_path)
return webdriver.Chrome(options=options)
# 每台 worker 一个 driver,出口由服务端凭据分配
driver_a = build_driver(ext_path_a)
driver_b = build_driver(ext_path_b)
需要按地区定向时,多数服务商支持在凭据配置里指定国家/州/城市,生成的凭据即绑定到对应地区出口,代码里不需要额外处理。
四、常见问题排查
1. --proxy-server 配置了但请求没走代理
检查目标页面是 http 还是 https,个别环境下 https 请求需要同时配好代理协议对应的入口;
Chrome 对 localhost、127.0.0.1 等地址有隐式直连规则,排查时别用本机地址做验证目标;
确认浏览器没有加载其他代理插件造成冲突。
2. 407 Proxy Authentication Required
用户名密码错误,或密码中的特殊字符没有做 URL 编码;
认证扩展未加载成功,或扩展缺少 webRequestAuthProvider 权限(MV3 必填)。
3. 认证扩展加载失败
确认 manifest_version 是 3,Chrome 139 起已不支持 V2 扩展,网上很多旧教程的 V2 写法现在已经不生效;
扩展有报错可以在 chrome://extensions 打开开发者模式查看 service worker 日志;
打包的 zip 里只放 manifest.json 和 background.js 两个文件,避免多余目录层级导致加载失败。
4. 出口区域与预期不符
区域定向在服务端凭据层面配置,先确认当前凭据绑定的区域;
部分服务需要在凭据或请求参数里指定地区,检查网关配置对应的区域设置。
5. 会话中断
Sticky 模式下时间窗口到期出口会更换,长任务按窗口切分成多个批次,或者在窗口内完成需要连续性的部分;
浏览器自动更新或崩溃后,driver 会话失效,出口可能变化,任务重跑前重新验证一次出口。
五、小结
Selenium 里的代理配置,本质上是在回答"浏览器从哪里看网页"这个问题。单出口时链路单一、区域受限;接入代理后,出口由服务端管理,区域、会话、轮换都变成了配置项。
具体选型时建议:先明确任务的区域要求和会话连续性要求,再选择 Rotating 还是 Sticky;用回显接口验证出口区域;最后拿小规模流量跑通整条链路再放量。B2Proxy 这类网关式服务支持在官网申请免费测试流量,先用真实任务验证区域覆盖和会话稳定性,比看任何宣传都靠谱。