news 2026/9/20 13:19:56

Selenium反爬与性能优化实战:从ChromeDriver到元素定位

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Selenium反爬与性能优化实战:从ChromeDriver到元素定位

做采集和自动化测试的朋友应该都有过类似的经历:脚本写完跑起来,前几十个页面好好的,突然就弹验证码了;或者一个页面等半天,图片转圈、异步脚本狂跑,单页耗时直奔8秒以上。我前段时间帮朋友调一个财经社区(类似东方股吧)的数据采集脚本,就同时踩了反爬和性能两座大山,折腾了一整天才把方案理顺。这篇文章就把我从环境搭建、ChromeDriver版本匹配、反爬对抗到性能调优、元素定位这一整套实战经验整理出来,给正在用Selenium做自动化测试或数据采集的人做个参考。文章不吹概念,直接给可落地的方案和代码,按步骤抄即可。

1. 反爬与性能是Selenium绕不开的两座山

先说一个反直觉的结论:Selenium脚本跑不动,大概率不是代码逻辑错了,而是你根本没有科学使用浏览器自动化这个工具。很多人一上来就是webdriver.Chrome()打开一个裸浏览器,然后find_element+click+sleep(3)三板斧。这种写法在本地演示场景没问题,可一旦面对真实生产环境——比如采集大量公开网页、跑自动化回归测试——问题就全冒出来了。

第一个问题是“命短”。网站会通过多个维度识别自动化浏览器,一旦识别出来,轻则返回验证码页面,重则直接封IP、封账号。你也别怪网站太敏感,从网站角度想,正常人不可能1秒钟打开10个页面,也不可能鼠标移动轨迹笔直一条线,更不可能navigator.webdrivertrue还毫无防备。这些信息在浏览器里都是暴露的,服务端一行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版本选择
114114.0.5735.90
115115.0.5790.170
116116.0.5845.96
120120.0.6099.109
122122.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-manager
from 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

普通浏览器返回undefinedfalse,而被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等待所有资源加载完成默认值,最保险但不推荐
eagerDOM加载完成后立即返回列表页、详情页解析
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 不同定位策略的耗时对比

我在同一个页面上对同一目标元素用不同定位策略做了个简单测试,结果很有参考价值:

定位策略示例平均耗时
IDdriver.find_element(By.ID, "title")10ms
CSS选择器driver.find_element(By.CSS_SELECTOR, "div.title")15ms
Class Namedriver.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+

结论很清晰:idcss选择器优先,xpath尽量用相对路径,不要随手写一长串绝对路径。绝对XPath只要页面层级加一层div,整个选择器就废了,维护成本极高。此外,尽量避免使用//*[contains(text(),'xxx')]这种全文遍历型写法,它会对整棵DOM树做全量搜索,在复杂页面里尤其慢。

5.3 页面元素枚举与定位元数据分离

接着说一个很多人没接触过但特别好用的思路:元素定位元数据与实际DOM实例分离。意思是,你在代码里不应该反复写一串串裸的XPath字符串,而应该把这些定位信息集中管理起来,在运行时再动态解析。

这个地方可以用“页面元素枚举”来辅助:开发阶段,你先用脚本把页面里目标区域附近所有候选节点的idclassnametagtext快速枚举出来,用这个枚举结果来验证定位器是否可靠,而不是肉眼盯着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 场景假设与整体思路

整体思路如下:

  1. 用隐身配置创建driver,加载策略设为eager
  2. 通过CDP屏蔽图片等非必要资源。
  3. 关闭显式自动化标记。
  4. 进入板块列表页后,等待列表容器出现,用CSS选择器提取每条帖子的信息。
  5. 处理翻页。
  6. 对疑似验证码页面做截图告警。

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文件,用项目配置文件统一管理驱动路径、加载策略和隐身开关。以后换项目、换目标站点,只用改参数,不用改逻辑。这个习惯帮我省掉了大量重复劳动,也让团队协作时的交接效率高了很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 13:17:55

pnpr OCI 仓库级作用域令牌认证:`oci.bearerAuth` 配置完全指南

包管理器开发工具CLI 【免费下载链接】pnpm Fast, disk space efficient package manager 项目地址: https://gitcode.com/gh_mirrors/pn/pnpm 点击查看 免费下载 导读 本指南围绕 pnpr(pnpm 仓库自带的 OCI 镜像分发服务)新增的 oci.beare…

作者头像 李华
网站建设 2026/9/20 13:15:29

毕业论文知识图谱构建:SpringBoot+Vue+Neo4j实战

简介:本资源是一套面向高校计算机专业教师、毕业设计指导者及高年级本科生的毕业论文知识图谱构建与可视化教学原型系统,聚焦教育领域中毕业设计质量监控与技术热点分析的实际需求。系统基于SpringBoot后端与Vue前端实现,集成Neo4j图数据库与…

作者头像 李华
网站建设 2026/9/20 13:15:22

OpenClaw 接 DeepSeek V4 Pro/Flash,Base URL 填 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 13:13:10

Dify+Ollama+DeepSeek-r1私有化部署:知识库与智能体实战

简介:面向企业技术团队、运维人员与正在选型大模型私有化方案的开发者,这份幕僚云私有化部署 Dify、Ollama 与 DeepSeek-r1 的资源包,聚焦于在数据不出内网的前提下搭建可用的 LLM 应用服务,解决隐私保护、安全合规与个性化落地问…

作者头像 李华