资讯动态

Selenium异步并发实现动态网页高效采集

发布时间:2026/10/21 21:20:21 来源:尧图企业网站定制
动态网页数据采集这件事做久了你会发现一个规律静态页面用Requests就能轻松搞定但一旦碰上Ajax渲染、框架嵌套、登录后才能看到的内容传统方案立刻抓瞎。我在这个项目里把Selenium和异步处理结合到一起实际测下来效率提升非常明显这篇文章把这套方案的完整思路、关键代码和踩坑记录都摊开讲清楚希望能帮到正在为动态页面发愁的朋友。这套方案解决的核心痛点就是两个一是让爬虫能像真人浏览器一样处理JavaScript渲染后的页面二是解决Selenium逐个串行执行的效率瓶颈。我知道很多人听到Selenium第一反应就是慢确实它本身是重量级方案但如果你要采集的对象是动态加载的页面尤其是数据藏在XHR请求里、还需要模拟点击翻页的那种Selenium反而是成功率最高的选择。问题只在于怎么用而异步处理恰好能把并发能力补上去。1. 项目整体设计与技术选型思路1.1 为什么动态网页场景下传统Requests方案会失灵先还原一下常见的翻车现场。你用Requests发一个GET请求拿回来的HTML源码里根本没有你要的数据打开开发者工具一看数据是页面加载完成后通过JavaScript向后端接口发起异步请求再把返回的JSON渲染到DOM里的。这时候你的爬虫看到的只是一个空壳页面。这种情况在电商商品页、社交信息流、爬虫类平台、数据可视化大屏里特别常见。有些站点甚至更极端——页面里做了多层嵌套框架数据藏在iframe里Requests拿到的是外层框架内层内容完全没法直接访问。还有些页面设计了各种反爬验证比如滑块、点选、行为检测单纯靠分析接口和伪造Headers破解成本高到你宁愿放弃。TIPS判断一个页面是不是动态渲染有个很简单的办法用浏览器右键“查看网页源代码”如果里面能找到目标数据那走Requests就够了如果找不到那基本可以断定数据是JS后加载的Selenium这类方案就成了必选项。1.2 Selenium加异步的组合逻辑到底解决了什么问题选Selenium的理由其实不复杂它通过驱动真实的浏览器内核执行页面脚本JS渲染完再给你完整页面结构浏览器能看到的它都能拿到。登录态的携带、Ajax异步请求的触发、页面点击翻页这类交互动作天然就能支持。但Selenium的短板也很赤裸——每个页面的渲染要等网络请求、脚本执行、布局绘制全部跑完串行跑下来速度就是慢采集几千条数据要等很久。异步处理在这个项目里的职责就是解决这个瓶颈。我在这个项目里用的核心思路是并发调起多个浏览器Tab不同Tab处理不同的URL让页面渲染彼此并行主线程只负责调度和收数据不阻塞等待单个页面的完成数据落地采用分批写入的方式避免单次大结果集对内存的冲击这套组合方案跑下来的实际效果比单纯串行Selenium能快3到6倍具体倍数取决于页面复杂度和网络延迟。某些重度渲染的页面提升更明显这个在后面章节详细说。1.3 适用场景与边界条件说明不是所有采集需求都适合上这套组合方案我列一下比较合适的判定条件方便你对号入座页面数据依赖AJAX异步加载且请求参数高度动态化难以静态模拟采集目标需要登录态维持且存在会话时效性页面操作依赖鼠标悬停、滚动加载、点击展开等交互行为数据规模在万级左右需要比串行采集快但不至于到分布式调度那种复杂度如果你的目标数据能直接通过HTTP接口拿到的还是优先接口方案。Selenium是兜底的重量级工具用它采集静态接口数据属于杀鸡用牛刀而且资源消耗会高一个量级。2. 开发环境准备与工具选型细节2.1 浏览器驱动安装的正确姿势Selenium本身只是个自动化测试库真正干活的还是一个真实浏览器所以你需要安装对应版本的浏览器驱动。这里面最容易踩坑的就是版本不匹配问题。我的经验是先确认你本机浏览器的确切版本号再到驱动管理站下载对应大版本的驱动文件。如果你用的是Chrome只要浏览器主版本号和驱动一致就行比如浏览器是120.x驱动也选120版本。Firefox的GeckoDriver同理。驱动安装完成后别急着写代码先在命令行里手动执行一下驱动文件确认能正常拉起浏览器进程这一步能筛掉八成环境问题。有些环境还需要注意浏览器安装路径包含中文或者驱动文件没有加可执行权限都会静默失败。2.2 Python依赖清单与版本搭配建议这个项目核心依赖其实就四个库这里给出我自己长期用着稳定的版本组合# 依赖安装命令 pip install selenium4.27.0 pip install aiohttp3.11.11 pip install pyppeteer2.0.0 # 备用方案后面讲这里给几个选型理由selenium不用追最新4.x系列里找个稳定版本就够用。异步框架选aiohttp而不是requests的异步版是因为它原生支持连接池复用多Tab场景下能显著减少TCP握手开销。2.3 Chrome无头模式配置模板无头模式就是让浏览器在后台运行不弹窗口适合部署在服务器上跑采集任务。配置参数这里给一个调得比较成熟的模板from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service def create_driver(): options Options() options.add_argument(--headlessnew) options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--window-size1920,1080) options.add_argument(--user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36) # 禁用自动化控制标志降低被识别风险 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) service Service(./chromedriver) driver webdriver.Chrome(serviceservice, optionsoptions) # 执行CDP命令覆盖Navigator的webdriver属性 driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}) }) return driver这里两个参数比较重要disable-dev-shm-usage是为了解决容器环境里面/dev/shm空间不足导致的崩溃问题window-size设置成1920x1080一是为了符合大多数页面的桌面端渲染逻辑二是避免某些响应式布局高度不够导致懒加载不触发。disable-gpu在无头模式下能避免很多渲染兼容性怪问题建议保留。2.4 工具选型的另一个对照方案说明除了Selenium还可以考虑Playwright或者Pyppeteer这类方案它们走的是浏览器DevTools协议性能表现通常更好启动速度和页面加载速度都要比传统WebDriver方案快一些。不过我最终选了Selenium做主力原因是生态成熟。网上能找到的问题案例最多你遇到奇怪报错时搜索解决方案的成功率更高。Selenium的API直接对应WebDriver标准后面想接真实浏览器的云端集群代码迁移成本也低。3. 核心功能拆解与关键实现细节3.1 数据定位策略不要用脆弱的绝对XPathXPath的使用是这个项目成败的技术细节之一。很多初学者喜欢拷贝浏览器生成的绝对XPathfull路径又长又脆弱页面上稍微加点元素就全盘崩掉。这个项目里我强调一个定位策略先找稳定属性再找文本内容最后才退回到结构路径。具体操作优先级列表优先用id、name、data-*这类稳定属性其次用CSS选择器里的class名注意避开动态生成的样式名文本内容包括正则匹配用XPath的contains函数辅助绝对路径是最后的手段用上了就得做好频繁维护的心理准备3.2 等待策略的三种实现姿势动态页面采集里等待策略直接决定了稳定性。Selenium提供了三种等待方式这里逐一说明显式等待是最推荐的方式它可以在指定时间内轮询某个条件成立资源利用率高。我习惯用expected_conditions配WebDriverWaitCombo代码清晰还不会傻等。隐式等待是给驱动设置一个兜底超时时间但它的机械之处在于对每个find操作生效页面元素一直不出现时会拖慢速度。强制等待就是sleep这个慎用固定秒数在慢网络下不够用快网络下又浪费时间完全是靠撒面粉的方式赌稳定性。这个项目的等待策略我给了一个核心原则能用显式等待解决的绝不用强制沉睡对翻页和弹窗类操作可以叠加一个较短的重试等待兜底。3.3 异步处理中浏览器实例的并发上限评估在实现异步逻辑之前先说清楚一个关键问题启动多少个并发浏览器实例合适。很多人一上来就想开个20、30个并发觉得越多越快。实际测试下来这个想法会让资源直接爆炸。每个Chrome进程吃内存大概在200到500MB具体看页面复杂度。你要是开20个并发单单内存就吃掉了8到10GB服务器基本卡死。我给的建议是普通配置的机器并发控制在4到6个浏览器实例区间。每个实例内部开1到2个Tab这样既能享受并发收益又不至于压垮系统资源。如果你的数据规模和实时性要求高建议研究一下分布式采集把并发打散到多台机器上而不是单机上硬堆并发数。这个方向不在这个项目的范围里但思路是相通的。3.4 页面加载完成后的解析提效技巧Selenium拿到渲染后的页面之后解析速度也是可以优化的。直接拿着整个页面文本丢给解析库去跑资源消耗大我建议对页面结构进行裁剪后再解析。两个实用的加速手段先执行JavaScript把无关节点移除。比如你已经定位到大容器节点了直接把它内部的script、style、注释节点全数删除再拿剩余HTML去解析数据量大时用正则或字符串切片的思路替代完整解析。比如只需要取某个特定属性值直接通过定位返回的属性接口而不是重新走一遍解析流程这些细节看着小但累积起来对整体耗时的影响很可观。4. 实操过程与核心代码实现4.1 异步采集流程的完整设计图景这个项目的数据流我用下面这条链路来组织任务队列派发URL → 浏览器实例循环领取任务 → 页面渲染与数据提取 → 数据校验清洗 → 分批写入存储 → 自动翻页或跳转下一个任务异步处理在这里起到的是协调器作用。每个浏览器实例独立跑任务循环实例之间互不干扰。主进程负责任务队列的维护和异常采集结果的重试补充。这样设计的好处是单个页面卡死不会拖累整体流程超时的页面可以直接丢弃交给新任务顶上。4.2 浏览器Tab异步并发实现下面这一段是代码实现里最关键的一部分每个浏览器实例异步处理多个Tab任务import asyncio from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC async def process_url(driver, url, wait): 单个URL的处理流程 result None try: # 新开标签页 driver.execute_script(window.open(, _blank);) driver.switch_to.window(driver.window_handles[-1]) driver.get(url) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, .data-container))) # 执行数据提取逻辑 items driver.find_elements(By.CSS_SELECTOR, .data-item) result [{title: item.find_element(By.CSS_SELECTOR, .title).text, link: item.get_attribute(data-link)} for item in items] except Exception as e: print(f[任务失败] {url} - {e}) finally: driver.close() driver.switch_to.window(driver.window_handles[0]) return result async def worker(worker_id, url_queue, result_queue, driver): while not url_queue.empty(): url await url_queue.get() print(f[Worker {worker_id}] 处理 {url}) result await process_url(driver, url, WebDriverWait(driver, 15)) if result: await result_queue.put(result) url_queue.task_done() async def main(): url_queue asyncio.Queue() result_queue asyncio.Queue() urls [...] # 目标URL列表 for url in urls: await url_queue.put(url) drivers [create_driver() for _ in range(4)] workers [asyncio.create_task(worker(i, url_queue, result_queue, drivers[i])) for i in range(4)] await url_queue.join() for w in workers: w.cancel() for d in drivers: d.quit() # 汇总最终结果 all_results [] while not result_queue.empty(): all_results.extend(await result_queue.get()) return all_results if __name__ __main__: asyncio.run(main())这个实现里有个细节值得解释worker数量和我创建driver实例的数量保持一致每个worker持有自己的浏览器实例避免了多个协程同时操作一个driver导致状态错乱的问题这个坑很重要Selenium的driver对象不是线程安全的跨协程混用会出现各种幽灵报错。4.3 登录态维持与会话复用的处理很多动态网页数据需要登录后才能访问完整内容。首登一次后面的所有浏览器实例都可以复用这个登录态这是减少重复登录被风控的关键操作。做法其实很直接先用交互模式手动登录一次等登录成功后获取存储目录下的cookie文件保存到本地。下一步在所有无头浏览器实例启动时自动往目标域名注入这些cookie。import pickle def save_cookies(driver, pathcookies.pkl): with open(path, wb) as f: pickle.dump(driver.get_cookies(), f) def load_cookies(driver, pathcookies.pkl): with open(path, rb) as f: cookies pickle.load(f) for cookie in cookies: driver.add_cookie(cookie)Cookie注入完之后不需要再做登录动作直接就能访问受保护页面。注意cookie有实效期发现大量401或跳转登录页时就得重新走一遍登录流程更新cookie文件。4.4 数据落地异步写入与断点续采设计采集到的数据需要落地存储这个项目里我用了两层存储设计。第一层是原始数据直接以JSON行格式落盘好处是采集进程崩溃了数据不丢。第二层是消费进程从JSON行文件读取并导入目标数据库。这样设计的原因很现实爬虫进程和数据导入是两个节奏完全不同的任务硬耦合在一起的话一方异常会拖垮另一方。分离之后采集只管采集入库只管入库。异步写入的代码示例import json import aiofiles async def write_results(results, file_pathresults.jsonl): async with aiofiles.open(file_path, modea, encodingutf-8) as f: for item in results: await f.write(json.dumps(item, ensure_asciiFalse) \n)断点续采的思路也很简单采集前先扫描已存在的文件提取已完成的URL集合新任务队列直接从差集开始派发。这样即使任务中途崩溃重跑一遍也不会产生重复数据。4.5 页面翻页与滚动加载的模拟操作页面的翻页逻辑因站而异但大体逃不出两类一类是底部翻页按钮另一类是无限滚动加载。这里各给一段通用代码模板。底部翻页按钮的实现关键在于等待按钮可点击后执行点击操作再等待下一页内容刷新完成再执行数据提取。注意有些页面的下一页按钮会动态改变class名这时需要重新定位。无限滚动加载的实现则是模拟滚轮事件多次尝试加载新数据直到达到预期条数或列表不再增长。def scroll_to_load(driver, max_scrolls20): 模拟滚动加载新数据 last_count 0 scroll_pause 1.0 for _ in range(max_scrolls): driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(scroll_pause) new_count len(driver.find_elements(By.CSS_SELECTOR, .data-item)) if new_count last_count: break last_count new_count这里比较值得说的是scorllingTrail条件判断如果两轮滚动后数据条数没变就认为已经加载到末尾了避免无意义的死循环。5. 常见问题与稳定性排障实录5.1 页面元素定位失败的常见场景与应对手法页面元素定位失败是Selenium采集里出现频率最高的问题。根据我实测的经验下面几种场景占了绝大多数一、元素在iframe里直接在当前页面定位永远找不到必须先switch进去嵌套框架再定位二、元素是动态加载的虽然存在但render慢定位时处于不可交互状态。此时改用visibility_of_element_located条件替代presence条件确保元素不仅是存在而且渲染可见三、属性值包含动态哈希后缀每次刷新页面都在变这种情况下class名的匹配逻辑得改成前缀匹配四、页面里有多个重名元素find_element默认返回第一个但你想要的内容可能在最后一个。这时需要观察DOM结构找父级容器区隔定位失败的排查思路有一个顺序套路先确认等待条件有没有写对再检查有没有嵌套的iframe最后才考虑是不是元素属性动态变化。这个顺序能覆盖九成报错。5.2 WebDriver版本不匹配与崩溃问题合集这算是环境类第一坑。启动时直接报session not created或者unknown error多数原因就是浏览器自动更新了而驱动文件还是旧版本。这里分享两个实用经验一是构建一个自动拉取驱动文件的小脚本启动前检查本机浏览器版本需要时自动去下载匹配的驱动。第二种方案是直接用webdriver-manager这个Python库它会在第一次调用时自动下载最新匹配的驱动文件以后每次更新也不用操心。另外无头模式下偶发的进程崩溃通常与系统资源紧张有关。排查时除了看内存占用还要考虑临时目录是否空间不足。很多低配服务器上容器容量就几百MBChrome一展开就写满了。5.3 反爬机制识别与应对思路跨过基础的反爬验证是Selenium方案进阶的核心难点。这里需要跑通一套逻辑闭环识别检测点、修改请求特征、维持行为合理。常见检测逻辑包括用户代理不一致、浏览器窗口尺寸异常、访问频率过高、webdriver属性暴露、缺少浏览器指纹特征、鼠标轨迹过于规整。根据我的经验比较有效的应对手段是关闭自动化扩展特性覆盖navigator的webdriver标志前面代码里已经带上了随机设置页面窗口大小避免每次都是1920x1080这种固定形态控制访问节奏每个页面停留时间加一点随机延迟把浏览器指纹特征补齐比如时区、语言、硬件并发数注意这些手段的使用需要遵守目标站点的数据使用条款文中示例数据均为模拟构造仅用于技术探讨实际使用前请确认合规边界。5.4 异步并发中的死锁与资源泄漏处理异步并发模式下最常见的两种事故是协程等待一个永远不会完成的任务导致死锁定位以及与资源泄漏相关的内存持续增长。死锁的常见来源是某个worker由于页面加载异常卡在driver.get等待上迟迟不返回任务队列无法前进。做法是给每个页面的加载加显式超时控制超过时间直接标记失败同时让任务进入重试队列。driver.get本身有个超时限制但渲染过程的等待时间是不可控的所以要在process_url外层包一层超时保护。内存泄漏的来源主要是打开过的页面没有回收。虽然代码里做了close操作但很多前端页面绑定的事件循环、WebSocket连接不会立即释放资源被进程缓存在内存里。这个我目前的处理方式是定期轮换浏览器实例跑一批任务关掉实例重新建一个一般五十到一百个页面操作后轮换一次。5.5 常见错误速查表把上面遇到的各类报错整理成一张速查表遇到问题直接对照定位错误现象核心原因解决方向Session not created驱动与浏览器版本不匹配同步WebDriver版本ElementNotInteractable元素渲染未完成改等待条件为元素可点击或可见NoSuchElementException定位策略失效检查iframe嵌套或换定位依据TimeoutException单页加载超时加大显式等待时间并加重试机制进程无响应卡死协程等待未释放锁给页面加载增加外部超时内存持续增长旧页面资源未回收定期轮换浏览器实例6. 实测效果与性能对比参考6.1 串行与异步并行耗时对比我在模拟数据源上做了一轮基准测试。模拟数据源是含两百个需渲染的列表页每页大约五十条数据项。环境是一台中配虚机内存8GB四核CPU。串行跑完两百页总耗时约1小时50分钟。异步方案四个浏览器实例同时跑总耗时约35分钟。提升幅度达到了约3.2倍。这个倍率主要在页面越重、渲染越慢的场景越明显。轻量页面翻倍时间更短并发收益就没那么大。6.2 资源配置比例的经验参考推测配置与并发数关系来说纯经验参考值给一组 2核四点五GB内存适合两个浏览器实例 4核八GB内存四个实例是安全上限 8核十六GB内存可以尝试六个到八个实例同时需要注意网络带宽也是瓶颈之一。图片资源大、接口数据多的页面带宽消耗可能远高于预期带宽饱和时效率不升反降。6.3 任务阶段性耗时的分布特征一次采集任务的耗时分布大致是页面加载渲染占68%数据定位提取占17%等待队列调度占10%写入磁盘占5%。所以优化注意力应该集中在页面加载效率上数据解析环节花大力气优化的收益很有限。这个分布数据基本符合大多数动态页面采集场景。7. 个人经验总结与扩展建议做的项目多了以后我对动态网页采集的体感是Selenium这类自动化工具适合作为兜底方案而不是首选方案。能用接口解决的就走接口接口失效了再用自动化来对付。两者结合可以在稳定性和效率之间找到一个更好的平衡点。一些零碎但实用的经验都记在这网站布局改版是常态元素定位时尽量避免过度依赖主KV样式多使用稳定的全局属性会是更淘神的选择采集任务写入日志文件还是很有必要信息可以包括当前进度、单页耗时、失败原因后面排查问题时全靠它反爬策略升级是长期的猫鼠游戏短时间内采集大量的清单是会很显眼建议控制好节奏把采集频率设置在人眼浏览的合理水平这个项目后续的扩展方向如果让我继续深入我会考虑把多实例采集扩展到分布式调度用消息队列来托管任务列表各采集节点从队列里拉任务执行。另一个方向是加入页面渲染异常的自动修复机制——遇到弹窗、滑块、验证码时机器人自动截屏并标记待处理减少人工介入的次数。还有一个值得投入的方向是提升代码的可维护性。采集代码最容易变成一堆难以维护的临时脚本建议从一开始就按“基础服务层、业务提取层、调度配置层”来组织目录结构后面面对站点变更时改动成本会小很多。就我个人经验而言Selenium加异步的组合在动态数据采集这个细分场景里依然是目前项目管理最成熟、最容易上手的方案。虽然它不像接口采集那样轻快但面对复杂页面时的稳定性和容忍度是实实在在的。项目本身跑起来的收益也比较明确稳定采集动态页面数据、降低了重复手工操作的时间这就可以了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑