做采集和自动化测试的朋友应该都有过类似的经历:脚本写完跑起来,前几十个页面好好的,突然就弹验证码了;或者一个页面等半天,图片转圈、异步脚本狂跑,单页耗时直奔8秒以上。我前段时间帮朋友调一个财经社区(类似东方股吧)的数据采集脚本,就同时踩了反爬和性能两座大山,折腾了一整天才把方案理顺。这篇文章就把我从环境搭建、ChromeDriver版本匹配、反爬对抗到性能调优、元素定位这一整套实战经验整理出来,给正在用Selenium做自动化测试或数据采集的人做个参考。文章不吹概念,直接给可落地的方案和代码,按步骤抄即可。
1. 反爬与性能是Selenium绕不开的两座山
先说一个反直觉的结论:Selenium脚本跑不动,大概率不是代码逻辑错了,而是你根本没有科学使用浏览器自动化这个工具。很多人一上来就是webdriver.Chrome()打开一个裸浏览器,然后find_element+click+sleep(3)三板斧。这种写法在本地演示场景没问题,可一旦面对真实生产环境——比如采集大量公开网页、跑自动化回归测试——问题就全冒出来了。
第一个问题是“命短”。网站会通过多个维度识别自动化浏览器,一旦识别出来,轻则返回验证码页面,重则直接封IP、封账号。你也别怪网站太敏感,从网站角度想,正常人不可能1秒钟打开10个页面,也不可能鼠标移动轨迹笔直一条线,更不可能navigator.webdriver是true还毫无防备。这些信息在浏览器里都是暴露的,服务端一行JS就能读到。
第二个问题是“腿慢”。Chrome是个庞然大物,默认模式下要加载全部图片、CSS、字体、第三方统计脚本,启动时还要重建一个全新的浏览器进程。你要是每个页面都重新driver = webdriver.Chrome(),那光启动开销就是3~5秒,完全没有优化空间。
这篇文章适合这么几类人:做自动化测试的工程师、需要从网页获取公开数据的开发者、系统学习Selenium的初学者。我会先讲环境和驱动匹配,这部分是最大也是最蠢的坑;再讲反爬对抗的常用手段;接着是性能优化组合拳;然后重点说说元素定位效率问题;最后给一个完整的财经社区采集实战配置。整体思路是:先让脚本活下来,再让它跑得快,最后让它好维护。
2. 环境搭建:ChromeDriver版本匹配是“第一滴血”
我在不同项目里帮人排查Selenium问题,至少有一半是环境问题。不是ChromeDriver版本不匹配,就是驱动文件路径不对,或是driver没正确退出导致进程残留。环境这关过不去,后面谈反爬和性能都是空中楼阁。
2.1 安装Selenium:比你想象的简单
Selenium现在主流版本是4.x,直接使用pip安装即可:
pip install selenium如果你在一个独立项目里开发,建议先建虚拟环境:
python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install selenium这里有个小提醒:不要在Jupyter Notebook里直接写自动化脚本并长期跑。Notebook的交互式环境对长任务的稳定性并不友好,后台kernel稍有阻塞,driver状态就不可控了。生产级脚本建议写成.py文件,通过命令行或调度工具执行。
2.2 ChromeDriver下载与版本匹配的“黄金法则”
ChromeDriver是Chrome与Selenium通信的桥梁。它实现了一套WebDriver协议,让外部程序可以控制Chrome的行为。桥的这头是Selenium,桥的那头是Chrome,如果两边的地基——也就是版本——对不上,桥就塌了。
最常见的报错长这样:
selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 114 Current browser version is 120.0.6099.109 with binary path ...这个报错信息已经把原因说得很直白了:你下载的ChromeDriver只支持Chrome 114,但当前浏览器的版本是120,两者主版本号不一致。
所以核心黄金法则就是:ChromeDriver的大版本号必须和Chrome的大版本号完全一致。小版本可以不完全相同,但大版本必须对得上。比如Chrome是120.x,那么ChromeDriver也应该选120.x。
查看Chrome版本的方法很简单,在Chrome地址栏输入:
chrome://version/里面能找到完整的版本号,比如120.0.6099.109,你只需要记住开头这个120。
下载ChromeDriver时,优先到ChromeDriver官方下载站找对应版本,也可以从国内的镜像地址下载,速度会更快。下载完后,文件是一个可执行文件,Windows下是chromedriver.exe,Linux/macOS下是chromedriver。
| Chrome大版本 | ChromeDriver版本选择 |
|---|---|
| 114 | 114.0.5735.90 |
| 115 | 115.0.5790.170 |
| 116 | 116.0.5845.96 |
| 120 | 120.0.6099.109 |
| 122 | 122.0.6261.128 |
2.3 三种驱动管理方式,效率天差地别
拿到chromedriver之后,有三种常见方式让Selenium知道驱动在哪:
第一种是最简单的手动方式:把驱动放到系统PATH目录里,代码里直接写webdriver.Chrome()。
第二种更明确:用Service指定驱动路径。
from selenium import webdriver from selenium.webdriver.chrome.service import Service service = Service(executable_path=r"E:\tools\chromedriver.exe") driver = webdriver.Chrome(service=service)第三种是我最推荐的方式:使用webdriver-manager库自动管理驱动。它会根据你本机Chrome的版本号,自动下载匹配的ChromeDriver到本地缓存,不需要你操心版本对了没有。
pip install webdriver-managerfrom selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service)这个方案的优点就是省心,尤其适合项目要在多台机器上部署的场景。编译服务器、测试服务器、本地开发机各装一套环境,手动管理驱动简直是一场灾难,而webdriver-manager能自动适配。缺点是第一次下载驱动时可能受网络影响,但下载完会缓存在本机,后续启动就很快了。
2.4 环境相关的“隐形炸弹”
版本匹配只是第一关。我总结几个常被忽视但特别坑的问题:
第一个坑:驱动文件和Chrome安装目录权限不足。比如公司电脑受控,驱动程序放在C盘受保护目录,运行时被拒绝执行。这种问题排查时往往没有明显报错,只是启动时卡住。解决思路是把驱动放到一个普通用户可读写的目录,比如项目的tools文件夹。
第二个坑:杀毒软件把chromedriver当成风险文件删掉。我至少遇到三回,人刚把驱动下载好,杀毒后台就给隔离了,然后代码报找不到文件。所以团队项目里,最好在文档里写明驱动下载和放置规范,避免每个人在自己机器上重复踩。
第三个坑:driver.close()和driver.quit()弄混。driver.close()只是关闭当前标签页,浏览器进程还在;driver.quit()才会关闭整个浏览器并清理进程。如果脚本中途异常退出,没有调用driver.quit(),系统里会残留多个chrome进程,挤占内存,机器越跑越慢。建议用try...finally或上下文管理器保证退出。
from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver = None try: driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) # 业务逻辑... finally: if driver: driver.quit()3. 反爬对抗:网站识别自动化的核心机制与应对思路
环境搭好后,真正决定脚本能不能活下来的是反爬对抗。很多人以为反爬只发生在HTTP请求层,改个User-Agent就万事大吉。实际站点对Selenium的识别是多维度的,最核心的一条就是WebDriver特征。
3.1 从HTTP到浏览器指纹,网站到底在看什么
网站识别自动化浏览器,本质上是看“你和正常用户哪里不一样”。差异点分布在三个层面:
第一层是HTTP特征。比如User-Agent、Accept-Language、Sec-Fetch-* 头等。Selenium本身不会刻意伪装,默认的UA在部分场景下会显示HeadlessChrome字样,明摆着告诉别人你是自动化。所以最基本的伪装就是自定义UA。
第二层是浏览器对象特征。打开Chrome,在DevTools控制台执行:
navigator.webdriver普通浏览器返回undefined或false,而被Selenium控制的正版Chrome,在旧版本下会返回true。这是因为ChromeDriver启动时会默认添加一个--enable-automation开关,向页面暴露WebDriver标记。服务端只要在页面JS里判断这个值,就能轻松识别自动化流量。
第三层是行为特征。正常用户的鼠标轨迹是曲线,操作间隔有随机性,页面滚动速度也各不相同。Selenium脚本如果一进去就瞬间滚动到底部、点击坐标固定不变,行为模型很容易被算法识别。除了这些,还有Canvas指纹、WebGL指纹、字体列表、时区、语言列表等深层次指纹,大型风控系统会综合校验。
3.2 数据接口的JS反爬:页面能看到,源码里却拿不到
现在很多站点的反爬重点已经不在HTML本身了,而在数据接口上。比如一个财经社区的帖子列表,你用Selenium打开页面,看起来内容是正常的,但查看页面源码却发现列表是空的。因为数据不是服务端渲染的,而是页面加载后用JavaScript发XHR请求异步拉取的。
更麻烦的是,这些接口通常带签名参数。比如URL里有一个sign字段和一串ts时间戳,服务端会用密钥对参数、时间、路径做HMAC加密生成签名,每次刷新页面签名都会变化。你想用纯HTTP库去模拟这个接口,就得先逆向JS找到加密函数,工作量极大。
这时候Selenium的优势就体现出来了:我根本不需要关心签名算法,因为我用的是完整浏览器环境,页面自己会去发请求、算加密、渲染结果。我只需要等待列表节点出现,然后直接解析DOM就行。但如果接口有更严的风控——比如检测请求是否来自真实的页面上下文——那就需要配合抓取接口响应,或者用execute_cdp_cmd将网络响应劫持回来,把JSON直接拿到手。这里就不展开具体站点案例了,思路是一致的。
3.3 一套实用的浏览器隐身配置方案
废话不多说,直接给一套我实测下来有效且稳定的基础配置。它的核心思路是:既要让浏览器看起来不像自动化,又要尽量减少启动时可能暴露的特征。
from selenium import webdriver from selenium.webdriver.chrome.options import Options def create_stealth_driver(): options = Options() options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) options.add_argument( "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" ) options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") driver = webdriver.Chrome(options=options) # 通过CDP覆盖 webdriver 标记 driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": """ Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); Object.defineProperty(navigator, 'languages', {get: () => ['zh-CN', 'zh']}); Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3]}); """ }) return driver这段配置里有几个关键点值得解释:
excludeSwitches去掉enable-automation开关,能消除一部分自动化痕迹。--disable-blink-features=AutomationControlled是另一个常用开关,能掩盖Blink内核的自动化控制标记。execute_cdp_cmd用Chrome DevTools Protocol在页面加载前注入脚本,重新定义navigator.webdriver属性,使它永远返回undefined。
如果不想手动维护这些覆盖逻辑,可以试试selenium-stealth这个第三方库,它有更完整的指纹覆盖方案。不过我实际用下来发现,不同Selenium版本下兼容性不太一样,有时候会和新版ChromeDriver冲突。所以最稳的方案还是理解原理后自己维护一段注入脚本,出了问题也能快速定位。
3.4 验证码:最后的堡垒
即使隐身配置做得不错,触发频率太高时依然会遇到验证码。滑块、点选、文字点选,各种形式都有。这里我的建议很明确:不要试图在公网上无限试错破解验证码,不仅成功率低,还会积累更多IP风险。
更稳的做法是把人工介入机制做到流程里:脚本检测到验证码特征时,自动截图并保存到本地目录,然后暂停当前页面的继续执行,通过企业微信或钉钉机器人发一条通知,由人工手动通过验证码后脚本再继续。虽然多了一道人工成本,但至少整个采集任务不会因为一个验证码整体挂掉。
还有一点必须强调:合规。任何采集行为都要尊重目标网站的robots.txt条款与用户协议,不要采集个人隐私数据,不要绕过登录墙和付费墙。技术本身没有对错,但使用技术的方式需要承担相应责任。
4. 性能优化:把单页面耗时从8秒压到2秒
反爬能让脚本“活着”,性能优化才能让脚本“跑得起来”。很多人在这一步停留在“用time.sleep(2)等页面加载”的原始阶段,其实性能损耗都发生在资源加载、同步等待和进程启动上。下面是我用过的最有效的组合拳。
4.1 加载策略:别等所有资源下载完
默认情况下,Selenium会等待页面所有资源加载完成才返回,包括图片、广告脚本、字体文件。这对采集任务来说是巨大的浪费。
ChromeDriver支持三种页面加载策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
normal | 等待所有资源加载完成 | 默认值,最保险但不推荐 |
eager | DOM加载完成后立即返回 | 列表页、详情页解析 |
none | 页面开始加载就返回 | 需要提前发请求或做拦截的场景 |
在代码里设置加载策略非常简单:
options.page_load_strategy = "eager"实测在同一个财经社区列表页,normal策略要等所有图片和统计脚本加载完,耗时约4~7秒;改成eager后,DOM就绪即返回操作,单页耗时能压到2秒左右。这还只是第一个优化点。
4.2 资源拦截:用CDP屏蔽图片、字体和第三方统计
在eager策略下,虽然DOM就绪就返回了,但浏览器后台可能仍在加载资源。如果我们要更彻底地“减负”,可以直接告诉Chrome哪些资源类型不需要加载。
下面这段代码通过CDP屏蔽了图片、样式、字体和媒体资源的加载:
from selenium import webdriver driver = webdriver.Chrome() # 屏蔽指定资源类型 driver.execute_cdp_cmd("Network.enable", {}) driver.execute_cdp_cmd("Network.setBlockedURLs", { "urls": ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.webp", "*.css", "*.woff", "*.woff2"] })屏蔽图片对纯文本数据采集尤其有效。很多论坛首页七八个板块各有背景图,每个图片请求都是几十上百KB,屏蔽之后网络开销大幅下降。但注意,如果目标页面用了图片懒加载,而懒加载的逻辑是“图片进入视口后才请求”,那屏蔽图片反而可能导致页面布局错乱或某些元素不可见,需要根据实际站点评估。
另外也可以在启动参数层面直接禁用图片:
options.add_argument("--blink-settings=imagesEnabled=false")这两种方式可以二选一,不要同时用,否则某些页面会出现兼容性问题。
4.3 等待策略:无脑sleep是万恶之源
新手最常见的性能杀手是time.sleep(3)。页面快的时候,3秒纯属浪费;页面慢的时候,3秒又不够,导致找不到元素报错。问题根源在于固定等待无法匹配动态页面的真实加载时间。
Selenium提供了两种官方等待机制:
- 隐式等待:设置一个全局超时时间,每次
find_element会在这个时间内轮询等待元素出现。 - 显式等待:对特定条件进行轮询,直到条件满足或超时。
一个重要经验是:不要同时混用隐式等待和显式等待,否则等待时间会叠加。比如隐式等待设置了10秒,显式等待也设置了10秒,最坏情况下单次定位会等20秒。
我推荐的做法是:全局设一个较短的隐式等待(比如1~2秒)作为兜底,对真正关键的元素用显式等待精确控制:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By page_list = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, "div.article-list")) )这段代码的意思是:最多等10秒,每0.5秒检查一下div.article-list是否出现,出现就立即继续,不浪费1毫秒。相比无脑sleep(3),这是质的提升。
4.4 复用浏览器实例:别频繁启动和关闭Chrome
Chrome进程启动开销很大,如果循环里每个URL都创建一个新driver,时间都花在浏览器启动上了。曾经有人拿我之前的配置去跑500个页面,结果发现有一半时间都耗在启动和退出浏览器上。
正确思路是尽量复用同一个浏览器实例,让driver在不同的URL之间跳转:
urls = ["https://example.com/page/1", "https://example.com/page/2"] driver = create_stealth_driver() try: for url in urls: driver.get(url) # 解析数据 finally: driver.quit()如果你确实需要并发采集多个任务,建议控制并发数,不要一次性开20个Chrome实例。4~5个实例已经能压满不少带宽和CPU了。多实例并发时,记得给每个driver设置独立的user-data-dir,避免会话串号:
options.add_argument("--user-data-dir=C:\\temp\\chrome_profile_1")4.5 代码细节带来的隐性提速
几个我平时很在意的小细节:
-关闭自动日志和诊断报告:Chrome默认会往磁盘写日志,跑久了会产生大量文件。加参数--log-level=3和--silent能减少日志IO。
- 不用的标签页及时关闭:如果开了多个标签页,
driver.quit()前确保所有页签被回收。 - 清理临时文件:浏览器崩溃后会留下临时文件,占空间且影响后续启动。写个定时清理脚本是值得的。
- Docker环境的特殊参数:如果你在容器里跑Selenium,记得加
--disable-dev-shm-usage,否则Chrome会在/dev/shm内存盘上耗尽空间导致崩溃。
5. 元素定位优化:为什么你的时间都耗在XPath上
反爬解决了“活下来”,性能解决了“跑得快”,接下来就是开发效率问题。好多人在一个页面上反复调试XPath,这一件事可能就消耗半天时间。这背后的原因不只是写选择器的技巧,还和元素定位的底层机制有关。
5.1 元素定位的底层机制
很多人以为find_element是本地在HTML字符串里做正则匹配,实际上Selenium要通过CDP协议向浏览器发送请求,让浏览器在实时DOM树里搜索符合条件的目标节点。这个搜索过程涉及协议通信和DOM遍历,复杂度比想象中高得多。
意味着,选择器本身写得越复杂,浏览器遍历DOM的开销就越大;通信次数越多,整体耗时就越长。如果脚本里有几十个find_element调用,累积起来优化空间相当可观。
5.2 不同定位策略的耗时对比
我在同一个页面上对同一目标元素用不同定位策略做了个简单测试,结果很有参考价值:
| 定位策略 | 示例 | 平均耗时 |
|---|---|---|
| ID | driver.find_element(By.ID, "title") | 10ms |
| CSS选择器 | driver.find_element(By.CSS_SELECTOR, "div.title") | 15ms |
| Class Name | driver.find_element(By.CLASS_NAME, "title") | 18ms |
| 相对XPath | //div[@class='title'] | 25ms |
| 绝对XPath | /html/body/div[3]/div[2]/div[1] | 30ms |
| 全文XPath | //*[@id='content']//div[contains(text(),'xxx')] | 60ms+ |
结论很清晰:id和css选择器优先,xpath尽量用相对路径,不要随手写一长串绝对路径。绝对XPath只要页面层级加一层div,整个选择器就废了,维护成本极高。此外,尽量避免使用//*[contains(text(),'xxx')]这种全文遍历型写法,它会对整棵DOM树做全量搜索,在复杂页面里尤其慢。
5.3 页面元素枚举与定位元数据分离
接着说一个很多人没接触过但特别好用的思路:元素定位元数据与实际DOM实例分离。意思是,你在代码里不应该反复写一串串裸的XPath字符串,而应该把这些定位信息集中管理起来,在运行时再动态解析。
这个地方可以用“页面元素枚举”来辅助:开发阶段,你先用脚本把页面里目标区域附近所有候选节点的id、class、name、tag、text快速枚举出来,用这个枚举结果来验证定位器是否可靠,而不是肉眼盯着DevTools猜。
然后把这些定位器元数据存成结构化配置:
{ "post_list": { "type": "css", "value": "div.article-list", "timeout": 10 }, "post_title": { "type": "xpath", "value": ".//a[@class='title']", "timeout": 5 } }在代码里写一个通用定位函数:
import json from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class ElementManager: def __init__(self, config_path, driver): with open(config_path, encoding="utf-8") as f: self.locators = json.load(f) self.driver = driver def element(self, name): meta = self.locators[name] by = getattr(By, meta["type"].upper()) wait = WebDriverWait(self.driver, meta.get("timeout", 10)) return wait.until(EC.presence_of_element_located((by, meta["value"])))这套方案最大的优势是:页面一旦改版,只需要改配置文件里的定位元数据,不需要在整个业务代码里搜索替换。尤其适合那些页面结构经常微调的站点或者大型测试项目,能省下无数改代码的时间。
5.4 通用定位工具箱的封装思路
顺着上面的思路,我建议每个人都封装一套属于自己的通用定位工具箱,包含几个基础方法:等待元素出现、点击、输入、判断是否可见、异常时截图加保存HTML。
def safe_click(driver, element): try: driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element) element.click() except Exception as e: driver.save_screenshot("debug_screenshot.png") with open("debug_page.html", "w", encoding="utf-8") as f: f.write(driver.page_source) raise e这段代码里scrollIntoView是很多新手会忽略的点:元素在DOM里存在,但不在可视区域内,直接点击会被浏览器拒绝。先滚动到可视区,再点击,能规避大量“元素不可交互”的报错。异常时保存截图和页面源码,后续排查问题时帮助巨大。
6. 综合实战:财经社区数据采集的完整调优链路
理论知识讲了这么多,最后整合一个完整的实战案例:假设我们需要从某个类似股吧的财经社区,采集某个公开板块的帖子标题、作者和回复数。目标站点有UA检测、webdriver检测,图片懒加载,列表数据由异步接口加载。
6.1 场景假设与整体思路
整体思路如下:
- 用隐身配置创建driver,加载策略设为
eager。 - 通过CDP屏蔽图片等非必要资源。
- 关闭显式自动化标记。
- 进入板块列表页后,等待列表容器出现,用CSS选择器提取每条帖子的信息。
- 处理翻页。
- 对疑似验证码页面做截图告警。
6.2 完整代码示例
import time import random from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def create_driver(): options = Options() options.page_load_strategy = "eager" options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) options.add_argument( "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" ) options.add_argument("--disable-gpu") options.add_argument("--log-level=3") driver = webdriver.Chrome(options=options) driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": """ Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); Object.defineProperty(navigator, 'languages', {get: () => ['zh-CN', 'zh']}); """ }) driver.execute_cdp_cmd("Network.enable", {}) driver.execute_cdp_cmd("Network.setBlockedURLs", { "urls": ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.webp", "*.css", "*.woff", "*.woff2"] }) return driver def fetch_post_list(driver, page_url): driver.get(page_url) wait = WebDriverWait(driver, 10) container = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.article-list"))) items = container.find_elements(By.CSS_SELECTOR, "div.article-item") data = [] for item in items: try: title_el = item.find_element(By.CSS_SELECTOR, "a.title") author_el = item.find_element(By.CSS_SELECTOR, "span.author") reply_el = item.find_element(By.CSS_SELECTOR, "span.reply-count") data.append({ "title": title_el.text.strip(), "author": author_el.text.strip(), "reply": int(reply_el.text.strip() or 0) }) except Exception: continue return data def main(): driver = create_driver() try: base_url = "https://example-finance-board.com/list?page={}" all_data = [] for page in range(1, 6): try: page_data = fetch_post_list(driver, base_url.format(page)) all_data.extend(page_data) print(f"page {page} done, got {len(page_data)} items") except Exception as e: print(f"page {page} error: {e}") driver.save_screenshot(f"error_page_{page}.png") continue # 随机延时,避免请求频率过于规律 time.sleep(random.uniform(1.0, 2.5)) print(f"total: {len(all_data)} items") finally: driver.quit() if __name__ == "__main__": main()代码里几个值得注意的点:
- 翻页循环中用了异常捕获,单页失败不会让整个任务崩溃。
- 随机延时控制在1到2.5秒之间,核心目的是避免访问频率过于规律,因为正常用户不会像秒表一样每2.0秒精准翻一页。
- 数据采集完先内存累积,后续可以落库或写文件,具体方式取决于业务需求。
6.3 稳定性设计:重试、断点续爬与告警
生产级采集脚本不能只靠一次循环跑完,必须考虑网络抖动、目标站升级、验证码突袭等意外情况。我通常会在脚本里加三样东西:
重试机制:连续失败3次的页面跳过并记录,不无脑重试,防止IP风险快速升级。
断点续爬:已经成功采集的帖子ID存到一个集合里,后续再跑时跳过这些ID,避免重复采集。这个集合可以序列化到本地文件,或直接存SQLite。
验证码人工介入:当页面出现验证码特征时(比如检测到滑块组件或提示文案),截图保存到本地,然后脚本暂停当前任务,通过消息机器人通知人工处理。人工处理完,脚本继续往下走,这是最稳的“人机协同”模式。
6.4 优化前后的实测对比
我在同一台机器、同一个目标板块页面上做了简单对比,结果如下:
| 指标 | 优化前(裸driver + sleep) | 优化后(完整配置) |
|---|---|---|
| 单页加载耗时 | 6~9秒 | 1.5~2.5秒 |
| 100页总耗时 | 约11分钟 | 约3.5分钟 |
| webdriver标记暴露 | 明显 | 无法从页面读取 |
| 触发验证码概率 | 高 | 低 |
这组数据不具备普适性,因为不同网站的防护策略和页面复杂度差异巨大,但它能说明一个趋势:同样的采集任务,通过合理的加载策略、资源拦截、等待优化和反爬伪装,耗时和封禁风险都能得到数量级的改善。
6.5 关于合规边界的提醒
最后把合规这件事再说透一点。做自动化采集前,先看目标站点是否提供了官方API或数据开放接口,能用API解决就不要去解析网页。如果必须用Selenium,也要遵守几个底线:不采集个人隐私信息,不绕过登录墙与付费墙,不抓取涉及版权保护的内容,控制采集频率,不给目标服务器增加明显压力。技术方案再强,也得在合理合法的框架内使用。
7. 写在最后的几点体感
整套方案用下来,我最大的感受是:Selenium反爬和性能优化没有银弹,只能在“被识别风险”和“采集效率”之间做平衡。隐身配置做得越彻底,浏览器越不像自动化,但对应的启动参数和注入脚本也越复杂;性能压得越狠,屏蔽的资源越多,遇到特殊页面时又容易出兼容性怪问题。所以不要一上来就套用全套满级配置,先轻量跑通流程,再根据目标站点的实际反应逐步加重。
最后分享一个小技巧:把create_driver()这类环境配置函数单独放进一个driver_manager.py文件,用项目配置文件统一管理驱动路径、加载策略和隐身开关。以后换项目、换目标站点,只用改参数,不用改逻辑。这个习惯帮我省掉了大量重复劳动,也让团队协作时的交接效率高了很多。