1. 先说清楚Selenium 到底是什么为什么爬虫也用它Selenium 最初是给 Web 自动化测试准备的工具核心能力就是“模拟真人操作浏览器”——打开网页、点击按钮、填写表单、滚动页面、切换标签页它都能干。后来做爬虫的人发现这玩意儿简直就是动态页面的救星。普通的爬虫脚本用 requests 直接发 HTTP 请求拿到的是 HTML 源码可遇到 JavaScript 动态渲染的内容源码里只有一堆脚本标签什么数据都拿不到。Selenium 不一样它直接驱动真实的浏览器内核页面加载完是什么样它就能拿到什么样跟人眼看到的一模一样。爬虫领域有个经典困境目标页面全是异步加载接口又做了签名加密分析数据包的成本越来越高。用 Selenium 就可以绕开这套复杂分析直接把浏览器当成解析器。你说它慢是慢但很多时候这是最省事、最稳妥的方案。适合用 Selenium 的场景也很有特征数据量不大、对实时性要求不高、页面交互复杂要登录、要点击、要翻页、要滚屏或者接口加密实在解不动。这篇文章我按自己的实操经验从环境搭建、元素定位、等待策略、反爬应对、并发加速、踩坑排查这几个角度完整讲一遍 Selenium 爬虫实践。以下代码全是 Python 版框架我用 Selenium 4.x这个版本相比老版本有不少 API 调整我会把关键差异点也点出来——很多人还在网上抄到老代码跑不通就是因为版本坑。2. 环境准备与版本选型这一节能帮你少走一半弯路2.1 安装 Selenium 和浏览器驱动版本必须严丝合缝安装 Selenium 本身很简单pip install selenium 一行搞定。真正的坑在浏览器驱动。Selenium 4.x 之后驱动管理已经集成进来了有个叫 Selenium Manager 的机制你指定浏览器版本它会自动去找匹配的驱动省掉了老版本手工下载、手工配置环境变量的麻烦。但正因为太方便很多人就忽略了版本概念于是出现你用的是 Chrome 120驱动却是 Chrome 110 的浏览器直接启动不了。这种问题一上来就报“session not created: This version of ChromeDriver only supports Chrome version XX”看得人头大。我的建议如果能装新版本就走自动管理的路线。如果你想手工下载驱动去浏览器的官方驱动站点看清楚浏览器版本号的三段式匹配规则。以 Chromium 内核为例浏览器更新到 121.0.6167.85驱动版本最好是 121 开头小版本号一般向下兼容。Firefox 对应 geckodriverEdge 对应 msedgedriver这三者各有各的脾气但核心逻辑是一致的驱动版本必须覆盖浏览器版本。2.2 常用浏览器选项配置无头模式与反检测基础先看一段我常用的浏览器初始化配置这是整个爬虫的起点也是后面所有反检测设置的基础from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service chrome_options Options() chrome_options.add_argument(--disable-blink-featuresAutomationControlled) chrome_options.add_experimental_option(excludeSwitches, [enable-automation]) chrome_options.add_experimental_option(useAutomationExtension, False) chrome_options.add_argument(--disable-gpu) chrome_options.add_argument(--no-sandbox) chrome_options.add_argument(--disable-dev-shm-usage) chrome_options.add_argument(--window-size1920,1080) # 无头模式生产环境里一般开着 chrome_options.add_argument(--headlessnew) driver webdriver.Chrome(optionschrome_options)这几个参数我逐个解释--disable-blink-featuresAutomationControlled 是关掉浏览器里跟自动化控制相关的特征标记。网页端的 JavaScript 有办法探测 navigator.webdriver 属性正常浏览器这个值是 falseSelenium 驱动起来之后是 true。上面这行配置能把这个特征压制掉一部分但这只是基础伪装真正的高级反检测咱们放到后面细讲。--disable-gpu 和无头模式一起用是为了避免一些 Linux 服务器环境下的渲染兼容问题。你如果只是在 Windows 本地跑开着窗口模式GPU 一般没什么大碍但服务器上没显卡驱动这个参数就很有用。--no-sandbox 和 --disable-dev-shm-usage 是专门解决容器环境问题的。Docker 之类的环境里沙箱机制经常导致 Chrome 启动失败或者内存共享不足直接闪退这两个参数就是统一处理这些兼容性问题的。3. 核心实操定位元素、等待策略与数据提取的完整套路3.1 元素定位的八种方式与实战选型Selenium 官方提供了多种元素定位方式我用过一遍之后感受就一句话没有万金油只有最适合当下场景的。我们直接看表格对比定位方式写法示例适用场景坑点IDfind_element(By.ID, content)元素有稳定 ID首选方式ID 经常动态生成一天一变Namefind_element(By.NAME, username)表单元素带 name 属性时同名元素多不准Classfind_element(By.CLASS_NAME, list-item)同风格样式批量抓取类名可能巨长还带空格Tagfind_element(By.TAG_NAME, li)批量统计同标签元素一抓一大把必须配合条件Link Textfind_element(By.LINK_TEXT, 下一页)定位带文字的链接文字稍微变了就失败Partial Link Textfind_element(By.PARTIAL_LINK_TEXT, 下一页)长链接里的局部文字匹配不唯一XPathfind_element(By.XPATH, //*[idapp]/div[2])没有稳定 ID靠层级定位写长了脆弱DOM 一变就挂CSS Selectorfind_element(By.CSS_SELECTOR, #app div.list)靠样式、属性组合定位学习成本略高但值得学我在爬虫里用得最多的是 XPath 和 CSS Selector。XPath 的好处是能做文本匹配比如 //button[contains(text(), 确认)] 这种写法是 CSS 做不到的遇到动态列表很有用。CSS Selector 的好处是简洁结构清晰性能也更好。但别一上来就复制各种浏览器插件生成的 XPath那些绝对路径比如 /html/body/div[1]/div[2]/div[3]/span你们自己脑补一下只要页面上方多一个广告位整个路径就废了。要写就写相对路径围绕元素的稳定属性来定位。3.2 显式等待、隐式等待与强制等待怎么取舍这一节是 Selenium 爬虫最容易翻车的地方。很多人上来就是一个 time.sleep(5)好一点用隐式等待但实际上这两种方式都有各自的坑。强制等待 time.sleep()固定睡几秒无脑但低效。网络快的时候白等慢的时候不够用我在调试初期会用正式脚本里基本不出现。隐式等待 driver.implicitly_wait(n)全局生效每次 find_element 的时候最多等 n 秒。问题是它看着好用碰上元素存在但在加载中、或者被遮住的情况一样报错。显式等待 WebDriverWait核心推荐。专门等某个条件成立比如元素可见、可点击、文字匹配逮着一个条件精准地等。实战里显式等待用法如下from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10, poll_frequency0.5) # 等按钮可点击 login_btn wait.until(EC.element_to_be_clickable((By.ID, login-btn))) # 等某个元素出现在 DOM 中 item wait.until(EC.presence_of_element_located((By.CLASS_NAME, product-item))) # 等某个文字消失常用于翻页加载完成判断 wait.until(EC.invisibility_of_element_located((By.CLASS_NAME, loading-spinner)))这里有个非常重要的细节element_to_be_clickable 和 presence_of_element_located 这俩条件返回的时机完全不同。前者要求元素可见、可用、尺寸不为零适合登录按钮这种需要交互的后者只要求元素出现在页面上适合数据列表这种只需要读取的。你要是把两个条件用反了要么等半天超时要么刚拿到元素去点击就被 Element Not Interactable 砸脸。等待策略这块还有一个我自己很依赖的操作等网络请求状态。虽然 Selenium 没有直接暴露 https://example.com 请求的 API但你可以执行 JavaScript 来轮询 document.readyState结合网络空闲条件做判断。这里分享一个工具函数def wait_for_page_load(driver, timeout10): WebDriverWait(driver, timeout).until( lambda d: d.execute_script(return document.readyState) complete )3.3 登录、翻页、数据提取——三个最常见的爬虫场景登录场景直接看代码。driver.get(https://example.com/login) # 第一次可能会弹 Cookie 弹窗先把遮罩点掉 try: WebDriverWait(driver, 5).until( EC.element_to_be_clickable((By.XPATH, //button[contains(text(), 同意)])) ).click() except Exception: pass driver.find_element(By.NAME, username).send_keys(my_account) driver.find_element(By.NAME, password).send_keys(my_password) driver.find_element(By.XPATH, //button[typesubmit]).click()登录成功之后第一件事不是立刻抓数据而是把登录状态保存下来。Selenium 的 Cookie 可以序列化保存下次启动直接把 Cookie 注入进去免登录import json # 登录完成后保存 cookies driver.get_cookies() with open(cookies.json, w) as f: json.dump(cookies, f) # 下次启动后注入 with open(cookies.json, r) as f: cookies json.load(f) for cookie in cookies: driver.add_cookie(cookie)这个做法适合那些登录有效期比较长的网站能大幅减少登录次数。注意add_cookie 必须在访问到对应域名之后才能操作先 driver.get 一次首页再注入否则报 InvalidCookieDomainException。翻页场景翻页的本质是找到下一页链接或者触发加载更多按钮。分页规则有三种常见形式URL 参数型/list?page2这种最简单循环改参数就行。点击下一页按钮型需要在每次翻完后等新内容加载。滚动加载型需要不断向下滚动触发懒加载。点击型翻页我一般这么写import time page_num 1 while True: # 抓取当前页数据 items driver.find_elements(By.CLASS_NAME, product-item) print(f第 {page_num} 页抓到 {len(items)} 条数据) # 寻找下一页按钮 try: next_btn driver.find_element(By.XPATH, //a[contains(text(), 下一页)]) except Exception: break # 判断是否禁用 if disabled in next_btn.get_attribute(class): break next_btn.click() page_num 1 time.sleep(1) # 这里最好改用显式等待比如等分页元素刷新数据提取Selenium 取数据有个老生常谈的问题——你看到的数据在页面上以某种结构组织但你要的是一个干净的数据集。常见做法是先用 execute_script 疯狂触发页面的渲染完成再用 find_elements 批量抓取最后用循环逐条清洗rows driver.find_elements(By.CSS_SELECTOR, div.list div.item) data [] for row in rows: title row.find_element(By.CSS_SELECTOR, .title).text.strip() price_text row.find_element(By.CSS_SELECTOR, .price).text.strip() # 清洗价格文本例如把 1,299.00 转为浮点数 price float(price_text.replace(, ).replace(,, )) url row.find_element(By.CSS_SELECTOR, a).get_attribute(href) data.append({title: title, price: price, url: url})这里 .text 和 get_attribute(textContent) 的差别需要提一嘴。.text 返回的是渲染后的可见文本如果元素设置了 display:none 或者其他不可见属性.text 就取不到内容。get_attribute(textContent) 不受可见性影响就算元素被隐藏也能拿到文本。我遇到隐藏元素抓取的需求时基本都是用 textContent。4. 反爬应对、效率优化与分布式思路4.1 从 User-Agent 到指纹伪装反检测到底要做到哪一步做爬虫的人都知道反爬是个博弈过程。Selenium 因为特征明显其实是反爬系统重点照顾的对象。webdriver 标记、自动化特征的修改、浏览器插件痕迹、本地指纹这些都是能探测的方向。我的实践策略是分层的基础层设置合法 User-Agent、禁用自动化控制标记、去掉自动化扩展。这能骗过低级检测但骗不了稍微有点实力的反爬系统。进阶层使用 Undetected-Chromedriver 或者 patch 过的 chromedriver我实际用下来效果还不错。这类工具的本质是改掉驱动和浏览器之间的通信特征让浏览器看起来跟用户手动打开没什么两样。环境层尽量保持浏览器指纹的一致性。分辨率固定、字体不变、Timezone 固定、语言不变减少 JS 探测指纹的波动。这里要说句实在话把核心业务压在全自动破解反爬上是不健康的。真的遇到高级验证码、行为分析特别严格的目标我更建议用半自动方案——人工过码程序做别的。混合模式虽然要人盯着但稳定性高得多。4.2 并发采集单浏览器多标签还是多浏览器实例Selenium 跑得慢是出了名的一个页面动辄几秒。想提速很多人第一反应是上多线程。我实测下来的结论是多线程配合多浏览器实例是可行路线但千万别在同一个浏览器里搞多线程操作同一会话——那会引发各种状态混乱今天崩溃明天响应超时后天元素找不到。推荐做法是进程池配合每个进程一个独立的浏览器实例from concurrent.futures import ProcessPoolExecutor def crawl_one_page(url): driver _create_driver() # 每个进程创建独立浏览器 driver.get(url) # 抓取数据... driver.quit() return data with ProcessPoolExecutor(max_workers4) as executor: results list(executor.map(crawl_one_page, url_list))这里有个资源预算的问题我踩过坑必须写出来4 个浏览器实例每个占用内存约 300~500MB取决于页面复杂度8 个就是 4GB 左右。服务器的内存规划要按这个算不然跑着跑着被 OOM Killer 干掉。无头模式对内存占用能缓解一些但别指望能省一半。真正吃内存的是 Chrome 的渲染进程哪怕是无头的照样有渲染进程在工作。4.3 分布式扩展从单机到多机的架构思路页面量大到单机撑不住的时候就该考虑分布式了。Selenium Grid 是官方的分布式方案它把节点分成 Hub 和 NodeHub 负责任务分发Node 是真实的浏览器执行端。我在自己的项目里搭过一个简单的 Grid 环境效果还行。另一种思路是把 Selenium 爬虫拆成生产者消费者模型一个进程专门跑页面抓取把原始 HTML 丢进消息队列另外几个进程消费队列解析数据、清洗入库。这样看起来还是单机但解耦后调优更容易。真要完全分布式通常是用 Kubernetes 或者云厂商的弹性容器组把每个浏览器实例跑在一个容器里通过队列调度。这种方案运维成本高不适合小项目但如果你有大规模采集需求这是绕不过去的方向。需要提醒的是Selenium 适合做小规模精准采集如果目标是千万级甚至亿级数据先思考接口直爬或者用 Playwright 的并行模式Selenium 的并发上限会让你很痛苦。5. 封 IP、超时、页面结构变动——常见问题与排查技巧5.1 WebDriver 超时、元素找不到、假死这三个老大难我最常被问到的问题是为什么我在本地跑得好好的放到服务器上就各种崩溃我总结了几种典型场景页面元素找不到这类问题的排查顺序是先看元素是否在 iframe 里。Selenium 默认操作的是顶级文档iframe 里的元素必须切上下文才能找到。我一开始不知道这个浪费过大量时间。切 iframe 的代码很简单driver.switch_to.frame(driver.find_element(By.TAG_NAME, iframe)) # 操作完之后切回来 driver.switch_to.default_content()然后看是不是新窗口打开的问题。点击target_blank链接会开新标签页这时候 driver 还停留旧页面上需要手动切换driver.switch_to.window(driver.window_handles[-1])脚本假死Selenium 脚本挂起不报错这个很恼人。大多数情况是浏览器在等待某个资源加载比如页面上有一个特别大的图片或者某些第三方统计脚本一直没有响应。解决方案是给加载策略做限制chrome_options.page_load_strategy eager # 在 DOM 加载完就返回不等待所有资源如果你只要操作 DOM 里的数据这个策略的效率提升很明显。还有一个终极手段是启动一个看门狗线程超时就强制刷新或者重启浏览器这个我在长期无人值守采集里验证过能救回不少莫名其妙的卡死。IP 被封IP 频繁被封很多是请求频率太高加上浏览器指纹太统一。Selenium 处理 IP 封禁的能力其实是弱点因为占用资源大单 IP 连接数又不好压。这里我能给的实操建议是限制启动频率让每个浏览器实例完成采集后随机 sleep 3~8 秒再退出整个任务在时间上打散不要集中在同一个时间段内爆发。5.2 页面结构变动的容错设计爬虫最大的敌人是页面改版。改版之后选择器全失效代码全线报错。我建议从一开始就给选择器做个集中管理把页面元素定位方式都放在一个配置文件里页面变了只改配置不改逻辑。这个习惯帮我赢回了很多时间。代码层面的容错也要做足。批量 find_elements 不会抛异常但拿到的是空列表如果你没做空值判断后面一长串代码就白跑。我的建议是每轮循环结束加一个断言数据条数明显偏少时告警。要么记录日志要么发个通知至少不能静默失败。if len(data) 0: logging.warning(fURL {url} 返回 0 条数据可能页面结构变了) # 可在这里截图留证 driver.save_screenshot(ferror_{datetime.now()}.png)截图留证真的是排查异常最直观的方式比看一堆报错日志强多了。5.3 异常处理框架不要让一个数据点拖垮整次任务Selenium 爬虫有个特点不可控因素太多一个页面出问题整个脚本就可能崩。所以异常处理框架要从一开始就设计好不能靠 try...except 零零散散地补。我的做法是给每一步核心操作定义一个超时和重试机制。比如页面加载每次最多等 20 秒超过就重试重试 3 次还不行就跳过记录失败原因。下面是简化版的封装思路def retry_get(driver, url, retries3, timeout20): for attempt in range(retries): try: driver.set_page_load_timeout(timeout) driver.get(url) return True except Exception as e: print(f第 {attempt1} 次加载失败: {e}) driver.delete_all_cookies() return False这个函数的核心不只是重试还有失败后清 Cookie 的操作——有些失败是因为会话状态异常清掉会话重新开始往往能恢复。6. 一套更省心的替代方案什么时候该换 Playwright写到这里得讲个诚实的话。Selenium 作为老牌工具确实皮实上手曲线也低。但你要是准备做一个新项目我不建议一上来就选 Selenium可以先看看 Playwright。理由很实在Playwright 有更好的等待策略——自动等待元素可被操作几乎不用手写显式等待。它还能截取网络请求、修改请求头、mock 接口观察页面加载的数据流。这些能力让调试动态页面变得极其简便。Selenium 最大的优势是生态成熟、资料齐全遇到任何问题都搜得到答案Playwright 的优点是 API 设计更现代化性能更好反检测能力也更强。我自己是两个工具都在用按项目需求选择。目标站点特性偏向老浏览器兼容、需要多浏览器覆盖Selenium 更稳追求性能和调试体验Playwright 更合适。但有一点必须提醒用哪个工具不改变爬虫的本质风险。做爬虫最好自建一个稳定的代理池避免单一 IP 频繁访问被封数据采集过程中要甄别目标数据是否涉及隐私和个人信息频繁请求带来的目标站点性能压力也要克制。我一般给自己定的规则是单页采集间隔不低于 1 秒并发数不超过 5任务跑一段时间就暂停给站点留出喘息空间。7. 一个完整的实战小项目抓取某个数据列表并入库最后用一个完整的项目串一遍所有知识。假设我们要抓取某个公开数据网站的列表页面要求是自动翻页 10 页提取标题、链接、日期然后存成 CSV。完整代码我整理到了下面其中用到了前面讲到的显式等待、相对 XPath、页面加载策略以及异常重试import csv import time import logging 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 from selenium.webdriver.chrome.options import Options logging.basicConfig(levellogging.INFO, format%(asctime)s - %(message)s) def create_driver(): opts Options() opts.page_load_strategy eager opts.add_argument(--headlessnew) opts.add_argument(--disable-gpu) opts.add_argument(--no-sandbox) opts.add_argument(--disable-blink-featuresAutomationControlled) opts.add_experimental_option(excludeSwitches, [enable-automation]) return webdriver.Chrome(optionsopts) def fetch_page_data(driver, page): url fhttps://example.com/list?page{page} try: driver.get(url) except Exception: logging.error(f第 {page} 页加载失败重试...) return [] wait WebDriverWait(driver, 10) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, .list-item))) items driver.find_elements(By.CSS_SELECTOR, .list-item) page_data [] for item in items: try: title item.find_element(By.CSS_SELECTOR, .title).text.strip() link item.find_element(By.CSS_SELECTOR, a).get_attribute(href) date item.find_element(By.CSS_SELECTOR, .date).text.strip() page_data.append([title, link, date]) except Exception as e: logging.warning(f解析单条数据出错: {e}) continue return page_data def main(): driver create_driver() all_data [] max_page 10 for page in range(1, max_page 1): data fetch_page_data(driver, page) logging.info(f第 {page} 页获取 {len(data)} 条数据) all_data.extend(data) time.sleep(1.5) # 控制频率避免给服务器太大压力 driver.quit() with open(output.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([标题, 链接, 日期]) writer.writerows(all_data) logging.info(f全部完成共获取 {len(all_data)} 条数据) if __name__ __main__: main()这个项目麻雀虽小五脏俱全。如果想扩展可以加个去重逻辑防止页面重跑产生重复数据也可以加个定时调度每天自动跑一次再往上就是加数据库入库而不是存 CSV这些都是一路线性扩展的事。我建议你自己动手改一改这个脚本把目标 URL 换成你自己要抓的站点把选择器换成元素检查里看到的实际结构。第一次跑通之后你会对这套 Selenium 的整个流程有个非常清晰的掌控感。跑不通也别急元素定位报错就 F12 手动检查对应节点的路径多半是选择器精度的问题。按照我个人这些年的体验Selenium 爬虫最需要花功夫的不是代码本身而是“适配”和“持续维护”这两件事。适配是指让浏览器、驱动、页面结构、等待策略全都匹配上持续维护是指在目标站点改版之后你得快速修复定位规则。所以强烈建议新项目在架构上留出灵活性不要为了省事把所有定位写死在业务代码里。灵活一点后期维护效率完全不一样。
阅读完成 · 觉得有帮助?