
前阵子我们组跑一个跨境页面监测任务。脚本在本地稳得像焊死了,放到服务器后,前两页正常,第三页开始变慢,第五页弹验证,再往后直接提示访问异常。
我最初也以为是 Selenium 暴露了某个特征,于是翻配置、换参数、查 navigator.webdriver。折腾一圈才发现,真正扎眼的不是某一行代码,而是整套环境都不太像正常用户:
浏览器语言是中文,出口 IP 在美国,系统时区却在欧洲;每次点击固定间隔两秒;一个会话刚从新加坡访问完,十秒后又跑到了德国。
这种情况下,就算把 webdriver 藏得再深,也只是给异常行为化了个妆。
所以,Selenium 所谓的“防检测”,重点不是研究怎么绕过风控,而是减少授权测试、公开页面采集和内部自动化中的误判。验证码、登录权限、付费墙和网站明确设置的访问限制,不应该靠脚本硬闯。
不少教程会把 navigator.webdriver 说成总开关,好像改成 undefined,浏览器就从机器人变成真人了。
真这么简单,风控团队早下班了。
现在的网站通常会综合观察浏览器环境、网络出口、会话状态和访问节奏。单个特征异常未必有事,一堆矛盾同时出现,风险才会迅速上升。
常见信号包括:
我见过最离谱的配置,是 Windows 系统配着 macOS 字体和移动端显卡信息,出口在日本,浏览器语言却写死成中文。每一项单独看都能解释,拼起来就像一台临时组装的“赛博缝合怪”。
所以第一条经验很简单:不要急着隐藏 Selenium,先让环境内部说得通。
网上有不少“全套隐身参数”,一上来就修改 User-Agent、Canvas、WebGL、字体、语言和设备信息,还顺手装几个 stealth 插件。
我现在看到这种配置,第一反应不是专业,而是先检查它有没有互相打架。
实际项目里,我更愿意从一份保守配置开始:
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def create_driver(profile_dir=None, proxy=None):
options = Options()
options.add_argument("--window-size=1440,900")
options.add_argument("--lang=zh-CN")
options.add_argument("--disable-notifications")
options.page_load_strategy = "eager"
if profile_dir:
path = Path(profile_dir).resolve()
path.mkdir(parents=True, exist_ok=True)
options.add_argument(f"--user-data-dir={path}")
if proxy:
options.add_argument(f"--proxy-server={proxy}")
driver = webdriver.Chrome(options=options)
driver.set_page_load_timeout(30)
return driver这段代码没什么神秘之处,重点反而是没有乱改底层指纹。
浏览器和驱动版本保持匹配,调试阶段先用有界面模式,流程稳定后再测试新版无头模式。User-Agent 如果没有明确需求,也不要随手伪造。你改了表面的版本号,客户端提示信息还暴露着真实版本,效果不是隐藏,而是主动提交矛盾证据。
能通过公开、授权 API 完成的任务,也别硬上浏览器。Selenium 更适合需要渲染、点击和交互的流程,不是所有数据采集都要开一个 Chrome。
很多 Selenium 脚本的节奏都长这样:
driver.get(url)
time.sleep(3)
driver.find_element(...).click()
time.sleep(2)偶尔能跑,不代表写对了。
页面加载时间受网络、缓存、接口响应和前端渲染影响。固定等三秒,有时浪费两秒,有时还差五秒。更麻烦的是,脚本会形成非常整齐的访问节奏:打开、三秒、点击、两秒、翻页,循环一百次。
正常用户很少这么守纪律。
更稳的办法,是等待页面进入具体状态:
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
wait = WebDriverWait(driver, 15, poll_frequency=0.25)
button = wait.until(
EC.element_to_be_clickable(
(By.CSS_SELECTOR, "button.load-more")
)
)
button.click()
wait.until(
lambda d: len(
d.find_elements(By.CSS_SELECTOR, ".result-item")
) > old_count
)这里等的是按钮可点击、结果数量发生变化,而不是凭感觉猜页面几秒能加载完。
翻页、提交查询等高成本操作之间,可以加入少量时间抖动,避免多个任务在同一时间整齐发送请求。但随机等待只是辅助,不能代替显式等待,更不是什么“模拟真人神器”。
真正该优化的是并发数、请求频率和任务调度。脚本一边每秒开几十个页面,一边随机睡零点几秒,这种自我安慰通常没什么用。
跨境自动化最容易踩的,是语言、时区和网络出口对不上。我把它叫作避坑三连。
第一坑是语言。出口在日本,浏览器却固定为简体中文,不仅会增加环境矛盾,还可能导致页面返回另一套文案。元素定位如果依赖按钮文字,切个地区就直接失效。能用 data-testid、稳定属性或结构定位,就少写依赖文案的 XPath。
第二坑是时区。IP 在美国,系统时区却长期停在亚洲。自动化测试当然可以故意制造这种差异,但如果目标是还原当地访问环境,语言、时区和出口地区最好保持一致,并在任务日志中记录下来。
第三坑是出口乱跳。很多人一看到访问受限,第一反应就是换 IP,失败后再换。几轮下来,同一个 Cookie 已经横跨三个国家。网站原本可能只是在限速,最后真被折腾成了高风险会话。
我们组做海外分区测试时也会接入极安的海外代理,但原则一直是:一个会话绑定一个相对稳定的出口,地区、语言和时区一起配置。代理解决的是网络出口,不负责替混乱的浏览器环境擦屁股。
会话本身也要适度复用。每访问一页就启动一个全新浏览器,Cookie、缓存和本地存储永远为空,却上来就直奔列表第十页,这种行为同样突兀。
不过,不要为了复用 Cookie,直接把个人 Chrome 目录交给 Selenium。里面可能有登录账号、同步信息、自动填充数据和无关站点记录。更稳妥的方式,是为每类任务建立独立 profile,并确保多个进程不同时占用同一个目录。
自动化任务最危险的写法,不一定是指纹配置,而是无限重试:
while True:
try:
run_task()
break
except Exception:
driver.refresh()页面已经弹出验证码或访问异常,脚本还在后台持续刷新。这不叫容错,叫给风控补材料。
网络短暂超时可以有限重试,页面结构变化则应该保存现场后报警;如果出现验证码、登录失效或明确的访问限制,就应停止任务并转人工处理。
我的处理顺序一般是先停掉高频请求,再检查页面结构、会话状态和出口稳定性。确认只是短暂网络问题后,才进行有限次数的指数退避重试。不要一边快速刷新,一边频繁更换 IP。
代理也不是橡皮擦。前面的访问行为已经留在服务端日志里,换个出口不会让历史记录自动消失。
脚本报错后只留一句 element not found,基本等于没留。
遇到异常时,至少应该保存当前 URL、页面标题、截图、HTML、浏览器版本、任务地区、出口标识和重试次数。因为很多所谓的“被检测”,最后查出来根本不是检测:
没有截图和页面快照,大家只能围着“是不是 Selenium 暴露了”开玄学会议。
我现在搭 Selenium 任务,会先固定浏览器和驱动版本,用独立 profile 保留必要会话,再用显式等待处理页面状态。地区任务会统一语言、时区和网络出口,并限制并发和重试次数。等这些基础工作稳定后,才会判断是否真的存在自动化特征误判。
说到底,Selenium 的“防检测秘籍”不是把自己伪装得多神秘,而是三件事:
环境别打架,会话别乱跳,失败别硬冲。
这三件事做好了,大多数脚本已经能稳定不少。要是还没检查这些,就先抄几十行“隐身神参”,通常只会把原本能解释的问题改成一锅谁也看不懂的配置粥。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。