一行参数就能跑:先把代理挂上去
Selenium 起 Chrome 的时候,浏览器进程是带着启动参数起来的。代理这种东西,Chrome 本身就认 --proxy-server 这个参数,所以你不用装任何插件,也不用改系统设置,只要在 Options 里塞一行,整台浏览器的流量就走到代理上去了。
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
opts = Options()
# HTTP 代理
opts.add_argument("--proxy-server=http://12.34.56.78:8080")
driver = webdriver.Chrome(options=opts)
driver.get("https://你的IP查询接口/") # 换成任意会返回访问者IP的地址
print(driver.page_source)
driver.quit()
注意两个细节。一是协议头要跟代理类型对上,SOCKS5 的代理就写 --proxy-server=socks5://12.34.56.78:1080,别把 SOCKS5 的地址写成 http://,Chrome 不会报错,只会一直转圈然后超时,很难查。二是这个参数是浏览器进程级别的,一旦启动就改不了了,想换个地区的 IP,只能 driver.quit() 之后重新起一个。很多人卡在这一步,以为改一下 Options 对象就能热切换,其实不行。
另外顺手把 --headless=new、--no-sandbox、--disable-gpu 加上,服务器上跑基本都要。别嫌参数多,能省掉一半莫名其妙的启动失败。
带账号密码的代理,Chrome 不认参数怎么办
真正麻烦的是认证代理。你手上那批 IP 是 用户名:密码@域名:端口 的格式,直接拼到 --proxy-server 里,Chrome 会把整串当成主机名,然后连接失败。这是浏览器本身的限制,不是 Selenium 的锅。
能走通的路就三条,你按自己的情况挑:
| 方案 | 上手难度 | 稳定性 | 适合谁 |
|---|---|---|---|
| 启动参数直连 | 最低 | 高 | IP 已绑定本机白名单,不需要账号密码 |
| 认证插件注入 | 中等 | 高 | 纯 Selenium 环境,不想引入新库 |
| selenium-wire / 本地转发 | 较低 | 中高 | 想顺便抓请求包,或插件加载报错 |
方案一:认证插件。核心思路是写一个极小的 Chrome 扩展,在里面把代理地址和账号密码写死,浏览器一启动就自动接管代理设置,遇到认证弹窗直接回填。整个扩展就两个文件,用 Python 生成即可,不用手写。
import os, zipfile
def build_auth_ext(host, port, user, pwd, ext_dir="auth_ext"):
manifest = """{
"version": "1.0.0",
"manifest_version": 2,
"name": "proxy auth",
"permissions": ["proxy", "tabs", "storage", "webRequest",
"webRequestBlocking", "*://*/*"],
"background": {"scripts": ["background.js"]},
"minimum_chrome_version": "22.0.0"
}"""
background = """
var cfg = {
mode: "fixed_servers",
rules: {
singleProxy: {scheme: "http", host: "%s", port: parseInt(%s)},
bypassList: ["localhost"]
}
};
chrome.proxy.settings.set({value: cfg, scope: "regular"}, function() {});
chrome.webRequest.onAuthRequired.addListener(
function(details) {
return {authCredentials: {username: "%s", password: "%s"}};
},
{urls: ["*://*/*"]},
["blocking"]
);
""" % (host, port, user, pwd)
os.makedirs(ext_dir, exist_ok=True)
with open(os.path.join(ext_dir, "manifest.json"), "w") as f:
f.write(manifest)
with open(os.path.join(ext_dir, "background.js"), "w") as f:
f.write(background)
zip_path = os.path.abspath(ext_dir + ".zip")
with zipfile.ZipFile(zip_path, "w") as z:
z.write(os.path.join(ext_dir, "manifest.json"), "manifest.json")
z.write(os.path.join(ext_dir, "background.js"), "background.js")
return zip_path
ext = build_auth_ext("12.34.56.78", 8080, "你的账号", "你的密码")
opts = Options()
opts.add_extension(ext)
driver = webdriver.Chrome(options=opts)
有个坑得提前说:新版 Chrome 对旧版扩展的限制越来越紧,add_extension 有可能直接抛错或者加载后不生效。如果你正好撞上,别在那儿死磕,直接换方案二。
方案二:selenium-wire。它把代理认证和请求抓包一起解决了,代码改动很小,配置里直接写带账号密码的地址就行。
from seleniumwire import webdriver
sw_opts = {
"proxy": {
"http": "http://账号:密码@12.34.56.78:8080",
"https": "http://账号:密码@12.34.56.78:8080",
"no_proxy": "localhost,127.0.0.1"
},
"verify_ssl": False
}
driver = webdriver.Chrome(seleniumwire_options=sw_opts)
driver.get("https://你的IP查询接口/")
for req in driver.requests:
print(req.url, req.response.status_code if req.response else None)
driver.quit()
方案三:本地转发。思路更土但最稳——本地起一个不带认证的小端口,把流量原样转给上游带认证的代理。Selenium 只管连 127.0.0.1,认证的事情交给转发脚本处理。写起来大概二三十行 socket 代码,好处是浏览器侧永远是无认证代理,兼容性拉满。
设置完不算完,出口 IP 必须自己验一遍
见过太多人配置完就直接开跑,跑了半天发现拿到的全是本机 IP,白忙一场。代理这玩意儿,配了不等于通了,通了不等于走的是你以为的那条线路。每换一批 IP,先做四项校验再谈业务。
第一项,真实出口 IP。访问一个会返回访问者地址的接口,把返回内容和你的提取记录对一下。对不上说明代理没生效,或者被 bypassList 放行了。
import re
def check_ip(driver, ip_echo_url):
driver.get(ip_echo_url)
body = driver.find_element("tag name", "body").text
found = re.findall(r"\d{1,3}(?:\.\d{1,3}){3}", body)
print("页面看到的出口IP:", found[0] if found else "没取到")
return found[0] if found else None
第二项,城市归属。IP 是换了,但落到了隔壁省甚至境外,业务逻辑就全乱了。把接口返回的地区字段打出来,跟提取时选的城市核对一遍。做本地化内容研究的,这一项比 IP 本身还重要。
第三项,语言和时区。IP 在上海,浏览器 navigator.language 却是 en-US,Intl.DateTimeFormat().resolvedOptions().timeZone 是 UTC,这种组合在风控眼里一眼假。要么用 --lang=zh-CN 参数统一,要么在启动前把时区也一起对齐。
opts.add_argument("--lang=zh-CN")
opts.add_experimental_option("prefs", {"intl.accept_languages": "zh-CN,zh"})
driver.execute_cdp_cmd("Emulation.setTimezoneOverride", {"timezoneId": "Asia/Shanghai"})
第四项,请求头有没有漏。有些代理会往请求里塞 X-Forwarded-For、Via、Proxy-Connection 这类字段,目标站点一看就知道你背后有中转。driver.requests(selenium-wire)或者本地抓包看一眼头部,该去掉的去掉,有些代理服务商后台是可以关掉透传的。
几个踩过才知道的小坑
坑一:driver 复用导致 IP 不换。把 driver 定义在循环外面,每一轮只改 Options,这是最常见的写法错误。Options 只对新建的浏览器实例有效,老实例压根不理你。要么每轮重建,要么用插件方式动态改 chrome.proxy.settings,但后者调试成本明显更高,多数场景不值得。
坑二:DNS 在本地解析。Chrome 用 HTTP 代理时,域名解析默认在本地完成,等于把你要访问的域名暴露给了本地网络。如果你的业务对这点敏感,优先选 SOCKS5,或者在参数里加 --host-resolver-rules 把解析交给远端。
坑三:超时和重试没设。代理 IP 有失效概率,页面加载卡在 driver.get() 上十几分钟不动,整个脚本就废了。给 page_load_timeout 和 set_page_load_timeout 设个 20 到 30 秒,配上 try/except 捕获超时异常,一失败就换 IP 重来,比什么都管用。
from selenium.common.exceptions import TimeoutException
driver.set_page_load_timeout(25)
try:
driver.get(target_url)
except TimeoutException:
driver.quit()
# 这里去提取一条新 IP,重建 driver 再试
坑四:把 webdriver 特征忘了。除了代理,navigator.webdriver 这个标记也很显眼。启动时加 --disable-blink-features=AutomationControlled,再用 CDP 把属性抹掉,能省掉不少无谓的拦截。
opts.add_argument("--disable-blink-features=AutomationControlled")
opts.add_experimental_option("excludeSwitches", ["enable-automation"])
opts.add_experimental_option("useAutomationExtension", False)
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"
})
IP 池这块,交给专门的服务更省心
代码写好了,剩下最影响效果的就是 IP 从哪来。自己找免费列表,跑十个挂九个,光排查时间就够写三套脚本了。这类活还是交给正规的服务商,比如神龙HTTP。
它家是国内三大运营商正规授权,IP 储备在 3000 万以上,可用率标称 99.9%,纯度 99.8%。对 Selenium 这类场景最有用的其实是 300+ 城市级精准定位——我在校验那一节说的"城市归属",用它就能直接指定某个城市的 IP,不用抽盲盒。再配上短效动态 IP 的高频轮换,一个 driver 失败就换一条,循环跑一整天也不会因为 IP 重复被拦。
接入方式也简单,通过 API 提取,返回 JSON,几行 requests 就能拿一批回来,主流爬虫语言都有示例代码。
import requests
resp = requests.get(EXTRACT_API, timeout=10).json()
ip_list = [(i["ip"], i["port"]) for i in resp["data"]]
host, port = ip_list[0]
opts = Options()
opts.add_argument(f"--proxy-server=http://{host}:{port}")
产品线分得比较清楚:短效动态 IP 适合高频轮换,长效静态 IP 和固定 IP 代理适合需要长时间保持同一出口的任务,三种都是一站式的,不用来回切平台。个人中心里有可视化用量统计和实时监控,哪条 IP 消耗快、哪段时间失败率高,一眼能看出来,调参的时候不至于瞎猜。技术团队是 7×24 小时支持的,配置卡住了直接问,比在论坛翻旧帖快。
常见问题
Q:为什么 --proxy-server 里写了账号密码,浏览器就一直加载不出来?
因为 Chrome 不认这种写法,它会把 账号:密码@host:port 整串当作主机名去解析,自然连不上。两个办法:一是找服务商把本机 IP 加白名单,这样只用写 host:port;二是用认证插件或者 selenium-wire,把账号密码交给它们处理。
Q:代理设置成功了,但检测到的 IP 还是本机的,怎么回事?
先看 bypassList 里是不是把目标域名放行了,很多人图省事写了 localhost 之外的通配规则,结果全走直连。其次确认 Options 对象是在 webdriver.Chrome() 之前构造的,顺序反了参数不会生效。最后检查一下是不是复用了旧的 driver 实例。
Q:每跑几页就要换一次 IP,怎么切换最省资源?
Chrome 的代理参数是启动时固定的,最稳的做法是每轮 quit() 后重建实例,单次创建大概一到两秒,比调试动态切换插件省心得多。如果对速度要求特别高,可以用 --user-data-dir 复用配置目录,能缩短一点启动时间。
Q:无头模式(headless)下代理经常不生效,正常吗?
旧版 headless 对扩展和部分代理配置支持不完整,新版 --headless=new 已经好很多。如果还是不稳,先去掉 headless 跑一遍确认代理本身没问题,再逐个加参数定位是哪一项冲突,通常问题出在插件加载或者时区模拟上。






