1. 项目概述为什么Selenium在爬虫领域依然坚挺提到Python爬虫很多人第一反应是requests搭配BeautifulSoup或者Scrapy。确实对于绝大多数静态网页这套组合拳又快又好用。但当你面对的是一个充斥着JavaScript动态渲染、登录验证复杂、操作流程繁琐的网站时传统的HTTP请求库就显得力不从心了。这时Selenium就该登场了。它本质上是一个浏览器自动化工具能模拟真人操作浏览器的一切行为点击、输入、滚动、下拉选择等等。在爬虫领域它扮演着“终极解决方案”的角色专治各种不服——特别是那些数据藏在JS动态加载背后的网站。我最初接触Selenium是为了抓取一个需要登录、然后通过多次点击和筛选才能看到数据的后台管理系统。用requests去逆向分析它的API接口发现其加密逻辑复杂且经常变动维护成本极高。转而使用Selenium虽然速度慢了点但代码逻辑直白得像写操作说明书稳定性反而大大提升。所以别再把Selenium仅仅看作一个“备胎”在应对现代Web应用尤其是单页面应用SPA时它往往是那条最稳妥、最省心的“主干道”。本教程将带你从零开始深入Selenium在Python爬虫中的每一个实用细节避开我踩过的那些坑。2. 环境搭建与核心组件解析工欲善其事必先利其器。用Selenium搞爬虫第一步就是把环境配通。这里面的门道直接关系到你后续脚本是稳定运行还是频频报错。2.1 浏览器与驱动版本匹配是生命线Selenium工作的原理是你的Python代码客户端通过WebDriver协议向一个特定的浏览器驱动程序如chromedriver发送指令驱动程序再去控制真实的浏览器如Chrome。因此浏览器、驱动程序、Selenium库三者的版本兼容性至关重要。浏览器选择Chrome/EdgeChromium内核是首选因为生态最好资料最全。Firefox也可用但驱动geckodriver的更新有时稍慢。驱动下载与管理查看浏览器版本打开Chrome在地址栏输入chrome://version/找到“Google Chrome”后面的版本号例如120.0.6099.109。下载对应驱动访问ChromeDriver官网或国内镜像站。关键点来了你下载的ChromeDriver主版本号必须与你的Chrome浏览器主版本号完全一致。例如Chrome是120.x.x.x就必须找120.x.x.x系列的ChromeDriver。放置驱动将下载的chromedriver.exeWindows或chromedriverMac/Linux放到一个目录并将该目录添加到系统的PATH环境变量中。更简单的做法是在代码里指定驱动路径。注意这是新手最容易栽跟头的地方。经常有人报错“This version of ChromeDriver only supports Chrome version XX”就是因为版本没对上。我的习惯是将驱动放在项目根目录的drivers文件夹下在代码中显式指定路径这样项目移植时不会出错。安装Selenium库这个最简单pip install selenium即可。建议使用虚拟环境管理项目依赖。2.2 基础脚本从打开浏览器到拿到源码环境配好我们来写第一个脚本感受一下Selenium的“所见即所得”。from selenium import webdriver from selenium.webdriver.chrome.service import Service import time # 1. 指定ChromeDriver路径 driver_path ./drivers/chromedriver # 根据你的实际路径修改 service Service(executable_pathdriver_path) # 2. 配置浏览器选项可选但很重要 options webdriver.ChromeOptions() # 添加常用选项 options.add_argument(--headless) # 无头模式不显示浏览器界面 options.add_argument(--disable-gpu) # 禁用GPU在某些系统上需要 options.add_argument(--no-sandbox) # 在Linux环境下可能需要 options.add_argument(--disable-dev-shm-usage) # 解决共享内存问题 # 防止被检测为自动化工具初级反反爬 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) # 3. 启动浏览器 driver webdriver.Chrome(serviceservice, optionsoptions) # 执行CDP命令进一步隐藏自动化特征 driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}) }) try: # 4. 访问网页 driver.get(https://www.example.com) # 等待页面加载简单粗暴的方式后面会讲更优的等待策略 time.sleep(3) # 5. 获取页面信息 print(当前标题:, driver.title) print(当前URL:, driver.current_url) # 获取渲染后的完整HTML源码这是requests做不到的 page_source driver.page_source # print(page_source) # 内容太多通常先打印看看结构 # 6. 一个简单操作在百度搜索框输入内容 driver.get(https://www.baidu.com) search_box driver.find_element(id, kw) # 找到搜索框 search_box.send_keys(Selenium爬虫教程) # 输入文字 search_box.submit() # 提交表单 time.sleep(2) finally: # 7. 关闭浏览器 driver.quit()这个脚本涵盖了最基本的流程。其中浏览器选项ChromeOptions的配置是进阶使用的开始。无头模式--headless对于服务器部署爬虫至关重要可以节省资源。而excludeSwitches和CDP命令则是为了应对一些网站对自动化工具的简单检测属于初步的“反反爬”措施。3. 元素定位八仙过海各显神通定位页面元素是Selenium所有操作的基础。找不到元素点击、输入都无从谈起。Selenium提供了多达8种定位策略但常用的就几种。3.1 八大定位器详解与选用策略driver.find_element(by, value)用于定位单个元素返回第一个匹配项。driver.find_elements(by, value)用于定位所有匹配元素返回一个列表。ID定位 (By.ID): 最优先选择。ID通常唯一定位最快、最准。driver.find_element(id, su)。Name定位 (By.NAME): 次优先。常用于表单元素。driver.find_element(name, wd)。Class Name定位 (By.CLASS_NAME): 注意class属性可能有多个值用空格分隔这里需要传入完整的单个class值或其中的一个。driver.find_element(class name, s_ipt)。Tag Name定位 (By.TAG_NAME): 按标签名定位如input,div,a。通常用于找多个同类元素。driver.find_elements(tag name, a)。Link Text / Partial Link Text (By.LINK_TEXT,By.PARTIAL_LINK_TEXT): 专门用于定位超链接a标签。前者需要完整文本后者只需部分文本。driver.find_element(link text, 新闻)。CSS Selector (By.CSS_SELECTOR):功能最强大、最灵活的定位方式。可以组合ID、class、属性、层级关系进行精准定位。语法和前端CSS选择器一模一样。强烈建议掌握。#id通过ID定位。.class通过class定位。input[namewd]通过标签名和属性组合定位。div#container ul.list li:nth-child(2)通过层级关系定位。XPath (By.XPATH): 和CSS Selector并列的定位神器。它通过XML路径来定位元素功能极其强大甚至可以定位没有ID、Class的元素或者通过文本内容定位。绝对路径/html/body/div[1]/form/input脆弱不推荐。相对路径//input[idkw]推荐。包含文本//a[contains(text(), 登录)]。多个属性//input[typetext and nameuser]。选用策略首选ID、Name简单直接。复杂场景用CSS Selector或XPath对于动态ID、嵌套复杂的元素这是唯一选择。CSS vs XPathCSS Selector通常性能稍好语法更简洁特别是对于class。XPath功能更强可以向上遍历DOM树找父节点并且对文本内容的支持更好。我个人更偏爱CSS Selector因为写起来快但在需要通过文本定位时XPath是无可替代的。3.2 定位实战与常见坑点假设我们要定位百度首页的搜索框和“百度一下”按钮。from selenium.webdriver.common.by import By # 访问百度 driver.get(https://www.baidu.com) # 方法1通过ID定位搜索框最稳 search_box driver.find_element(By.ID, kw) # 方法2通过Name定位同样很稳 search_box driver.find_element(By.NAME, wd) # 方法3通过Class Name定位注意它的class是多个 search_box driver.find_element(By.CLASS_NAME, s_ipt) # ‘s_ipt’是其中一个class # 方法4通过CSS Selector定位灵活 search_box driver.find_element(By.CSS_SELECTOR, #kw) # ID选择器 search_box driver.find_element(By.CSS_SELECTOR, input.s_ipt) # 标签class组合 search_box driver.find_element(By.CSS_SELECTOR, input[name\wd\]) # 属性选择器 # 方法5通过XPath定位强大 search_box driver.find_element(By.XPATH, //input[id\kw\]) search_box driver.find_element(By.XPATH, //*[id\kw\]) # 用*代替标签名 # 定位“百度一下”按钮 # 它没有ID可以用class或者XPath submit_btn driver.find_element(By.CSS_SELECTOR, #su) # 其实它有ID‘su’ submit_btn driver.find_element(By.XPATH, //input[type\submit\]) # 操作输入并搜索 search_box.send_keys(天气预报) submit_btn.click()常见坑点元素未加载/不可见这是最最常见的错误。你代码执行太快页面还没渲染出来就去定位当然找不到。解决方案是“等待”下一章详述。元素在iframe/frame内如果元素嵌套在iframe里你需要先切换到对应的frame中才能定位。# 1. 先找到这个iframe元素 iframe driver.find_element(By.TAG_NAME, iframe) # 2. 切换进去 driver.switch_to.frame(iframe) # 3. 现在可以定位iframe内的元素了 inner_element driver.find_element(...) # 4. 操作完成后切回主文档 driver.switch_to.default_content()动态ID/Class有些前端框架如React Vue会生成随机的ID或Class。此时绝不能依赖它们。要寻找其父级或相邻元素中稳定的特征使用CSS Selector或XPath的层级关系来定位。多个匹配元素使用find_element只会返回第一个。如果你需要操作第二个、第三个请使用find_elements获取列表后按索引操作或者使用更精确的选择器如:nth-child(n)。4. 等待的艺术告别time.sleep拥抱智能等待新手最爱用time.sleep(10)简单粗暴。但这是最低效的方式因为你永远不知道网络或服务器到底需要多久。设置短了元素没出来就报错设置长了大量时间在空等。Selenium提供了两种优雅的等待方式显式等待和隐式等待。4.1 显式等待精准狙击显式等待是针对某个特定条件进行等待条件满足则立即继续执行超时则抛出异常。这是推荐的主要等待策略。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 设置一个最长等待10秒的等待器 wait WebDriverWait(driver, 10) try: # 等待直到ID为‘kw’的元素出现在DOM中并且可见 search_box wait.until( EC.visibility_of_element_located((By.ID, kw)) ) # 一旦等到元素就可用了 search_box.send_keys(hello) except TimeoutException: print(等待超时元素未出现) # 其他常用的预期条件 (EC) # EC.presence_of_element_located: 元素出现在DOM中不一定可见 # EC.element_to_be_clickable: 元素可点击 # EC.text_to_be_present_in_element: 元素中包含特定文本 # EC.title_is / EC.title_contains: 标题判断显式等待的核心优势它轮询检查条件默认每0.5秒一次一旦条件满足就立刻返回最大程度节省时间。你需要根据操作目的选择合适的条件。例如要点击一个按钮就用element_to_be_clickable要获取一个元素的文本用presence_of_element_located通常就够了。4.2 隐式等待全局守候隐式等待是设置一个全局的等待时间在查找任何一个元素时如果元素没有立即找到WebDriver会等待一段时间直到超时。driver.implicitly_wait(10) # 单位秒 # 此后所有 driver.find_element 操作都会最多隐式等待10秒 element driver.find_element(By.ID, someId)隐式等待的坑只对find_element系列方法生效对get(url)等无效。和显式等待混用可能导致总等待时间变长。例如隐式等待10秒显式等待也10秒在最坏情况下实际等待时间可能是20秒。不够灵活无法针对特定条件如可点击、文本出现进行等待。最佳实践几乎总是使用显式等待因为它更精确、更高效。可以设置一个很短的隐式等待作为兜底如3秒处理一些简单的、确定性的元素。绝对避免混用长时间的隐式等待和显式等待。我的个人习惯是driver.implicitly_wait(0)禁用隐式等待全程使用显式等待代码控制力更强。4.3 强制等待与页面加载策略time.sleep()就是强制等待除非在调试或者等待非元素相关的固定流程如等待页面跳转完成否则应尽量避免。此外浏览器的页面加载策略也会影响等待from selenium.webdriver.common.desired_capabilities import DesiredCapabilities caps DesiredCapabilities.CHROME # ‘normal’等待整个页面包括静态资源加载完成。默认值。 # ‘eager’等待DOMContentLoaded事件完成DOM树构建好不等待图片、样式表等。 # ‘none’完全不等待。 caps[pageLoadStrategy] eager # 或 none driver webdriver.Chrome(desired_capabilitiescaps)在爬虫场景设置为eager可以显著加快driver.get()的速度因为我们通常只关心DOM结构不关心图片是否加载完。5. 元素操作与高级交互定位到元素后就可以对它为所欲为了。基础操作包括点击、输入、清空、提交等。5.1 基础操作与表单处理# 假设我们已经定位到元素 elem elem driver.find_element(By.ID, someId) # 1. 点击 elem.click() # 2. 输入文本 elem.send_keys(你要输入的文字) # 3. 清空输入框 elem.clear() # 通常在send_keys之前调用 # 4. 提交表单 # 如果元素在一个表单里可以用submit()。但更通用的做法是找到提交按钮并click()。 elem.submit() # 5. 获取元素属性、文本、状态 print(elem.get_attribute(href)) # 获取属性值如a标签的href print(elem.text) # 获取元素内的可见文本非常重要 print(elem.is_displayed()) # 是否可见 print(elem.is_enabled()) # 是否可用可操作 print(elem.is_selected()) # 是否被选中用于复选框、单选框 # 6. 处理下拉选择框Select from selenium.webdriver.support.ui import Select select_elem driver.find_element(By.NAME, country) select Select(select_elem) # 三种选择方式 select.select_by_index(1) # 通过索引从0开始 select.select_by_value(cn) # 通过option的value属性 select.select_by_visible_text(中国) # 通过显示的文本 # 获取所有选项 all_options select.options5.2 高级交互动作链与执行JS有些交互比如拖拽、鼠标悬停、复杂组合键需要用到ActionChains动作链。from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys # 鼠标悬停 menu driver.find_element(By.ID, menu) sub_menu driver.find_element(By.ID, submenu) actions ActionChains(driver) actions.move_to_element(menu).perform() # 移动到menu上触发悬停效果 # 等待子菜单出现这里需要显式等待 wait.until(EC.visibility_of(sub_menu)) sub_menu.click() # 拖拽元素 source driver.find_element(By.ID, draggable) target driver.find_element(By.ID, droppable) actions.drag_and_drop(source, target).perform() # 组合键操作 actions.key_down(Keys.CONTROL).send_keys(c).key_up(Keys.CONTROL).perform() # 模拟CtrlC执行JavaScript这是Selenium的“大招”可以完成WebDriver API无法直接实现的操作。# 滚动到页面底部 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(2) # 等待滚动后内容加载 # 滚动到某个元素 element driver.find_element(By.ID, footer) driver.execute_script(arguments[0].scrollIntoView(true);, element) # 修改元素属性可用于绕过一些前端限制 driver.execute_script(document.getElementById(readonlyInput).removeAttribute(readonly);) # 获取通过JS渲染后的数据有时.text属性拿不到 shadow_content driver.execute_script(return document.querySelector(...).shadowRoot.innerHTML;) # 点击被遮挡的元素 driver.execute_script(arguments[0].click();, element)6. 爬虫实战应对复杂场景与反爬策略现在我们把所有知识串联起来完成一个模拟真实爬虫需求的实战。6.1 实战案例爬取动态加载的电商商品列表目标爬取一个类似淘宝搜索页的商品列表该列表是向下滚动时通过AJAX动态加载的。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 import time import json options webdriver.ChromeOptions() # 无头模式并添加一些参数防止被识别 options.add_argument(--headless) options.add_argument(--disable-blink-featuresAutomationControlled) prefs {profile.managed_default_content_settings.images: 2} # 不加载图片加速 options.add_experimental_option(prefs, prefs) driver webdriver.Chrome(optionsoptions) wait WebDriverWait(driver, 15) base_url https://s.taobao.com/search?q手机 driver.get(base_url) product_list [] last_height driver.execute_script(return document.body.scrollHeight) scroll_pause_time 2 # 每次滚动后等待加载的时间 max_scroll 10 # 最多滚动10次防止无限循环 scroll_count 0 while scroll_count max_scroll: # 1. 滚动到底部 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(scroll_pause_time) # 等待新内容加载 # 2. 计算新的滚动高度判断是否已到底部 new_height driver.execute_script(return document.body.scrollHeight) if new_height last_height: # 高度未变可能已加载完毕或遇到“加载更多”按钮 try: # 尝试寻找并点击“加载更多”按钮 load_more_btn driver.find_element(By.CSS_SELECTOR, .load-more-btn) load_more_btn.click() time.sleep(scroll_pause_time * 2) except: # 没有找到按钮可能真的到底了 print(已滚动到底部或无法继续加载。) break last_height new_height scroll_count 1 print(f已滚动第 {scroll_count} 次当前页面高度{new_height}) # 3. 开始解析当前页面中的所有商品项 # 假设每个商品项有一个共同的CSS选择器 items driver.find_elements(By.CSS_SELECTOR, .item.J_MouserOnverReq) print(f共找到 {len(items)} 个商品项。) for index, item in enumerate(items): try: # 使用相对定位在item元素内查找子元素避免全局定位冲突 # 商品标题 title_elem item.find_element(By.CSS_SELECTOR, .title a) title title_elem.text.strip() link title_elem.get_attribute(href) # 价格 price_elem item.find_element(By.CSS_SELECTOR, .price strong) price price_elem.text.strip() # 销量和店铺名可能不存在 try: sales_elem item.find_element(By.CSS_SELECTOR, .deal-cnt) sales sales_elem.text.strip() except: sales N/A try: shop_elem item.find_element(By.CSS_SELECTOR, .shopname span) shop shop_elem.text.strip() except: shop N/A product_info { index: index 1, title: title, price: price, sales: sales, shop: shop, link: link } product_list.append(product_info) print(f已提取: {title[:30]}...) except Exception as e: print(f解析第{index1}个商品时出错: {e}) continue # 4. 保存数据 with open(taobao_products.json, w, encodingutf-8) as f: json.dump(product_list, f, ensure_asciiFalse, indent2) print(f数据已保存共 {len(product_list)} 条记录。) driver.quit()这个案例涵盖了无头模式、滚动加载、动态等待、异常处理、数据提取和保存。关键在于滚动逻辑和使用相对定位在item元素内find_element这能大大提高定位的准确性和鲁棒性。6.2 常见反爬策略与应对措施检测WebDriver一些网站会检测navigator.webdriver属性。我们之前用CDP命令已经将其置为undefined。还可以通过options.add_experimental_option(excludeSwitches, [enable-automation])来移除“正受到自动测试软件控制”的提示。行为指纹检测鼠标移动轨迹、点击速度等是否像机器人。应对方法使用ActionChains时加入随机延迟或者使用更高级的自动化框架如puppeteer-extra的Stealth插件在Python中对应undetected-chromedriver库。验证码这是硬骨头。简单图形验证码可以用OCR库如ddddocr,pytesseract尝试识别但成功率有限。复杂验证码如点选、滑块需要接入打码平台如超级鹰、图鉴或机器学习方案。对于爬虫来说遇到复杂验证码通常意味着需要降低频率、更换IP或者考虑是否值得投入成本破解。IP限制与封禁最有效的应对方法是使用代理IP池。from selenium.webdriver.common.proxy import Proxy, ProxyType proxy_ip_port http://123.45.67.89:8080 proxy Proxy() proxy.proxy_type ProxyType.MANUAL proxy.http_proxy proxy_ip_port proxy.ssl_proxy proxy_ip_port capabilities webdriver.DesiredCapabilities.CHROME proxy.add_to_capabilities(capabilities) driver webdriver.Chrome(desired_capabilitiescapabilities)更常见的做法是使用--proxy-server命令行选项options.add_argument(f--proxy-serverhttp://{proxy_ip_port})User-Agent检测定期更换User-Agent。from fake_useragent import UserAgent ua UserAgent() random_ua ua.random options.add_argument(fuser-agent{random_ua})7. 性能优化、调试与最佳实践用Selenium爬虫效率是永恒的痛点。以下是提升效率和稳定性的关键点。7.1 性能优化技巧无头模式Headless--headless必备节省大量GUI渲染资源。禁用图片/样式表/字体大幅减少流量和加载时间。prefs { profile.managed_default_content_settings.images: 2, # 2为禁用 permissions.default.stylesheet: 2, } options.add_experimental_option(prefs, prefs)设置页面加载策略为eager或none如前所述。合理使用等待杜绝全局sleep多用显式等待设置合理的超时时间。复用浏览器会话对于需要登录的网站可以保存cookies下次启动时加载避免重复登录。但Selenium直接保存会话状态较复杂通常需要配合pickle库保存cookies。并发与多线程/进程一个WebDriver实例对应一个浏览器进程资源消耗大。实现并发爬取通常采用多进程multiprocessing每个进程独立一个driver。注意线程间共享driver是危险且不推荐的。7.2 调试技巧非无头模式运行在开发调试阶段注释掉--headless选项亲眼看着浏览器操作能快速定位问题。截图出错时自动截图是强大的调试工具。try: some_operation() except Exception as e: driver.save_screenshot(error.png) print(f操作失败已截图。错误信息{e}) raise打印页面源码或元素HTML当定位不到元素时打印driver.page_source或element.get_attribute(outerHTML)检查元素是否真的存在或者你的选择器是否正确。使用浏览器开发者工具在非无头模式下按F12打开开发者工具。在Elements面板可以用CtrlF测试你的CSS Selector或XPath是否唯一匹配。Console面板可以直接执行document.querySelector(...)进行测试。7.3 项目结构与代码最佳实践配置与代码分离将浏览器路径、代理、用户代理等配置项放在config.py或settings.yaml中。使用Page Object模式对于大型爬虫项目将每个页面的定位器和操作封装成一个类。这极大提高了代码的可读性和可维护性。class LoginPage: def __init__(self, driver): self.driver driver self.username_input (By.ID, username) self.password_input (By.ID, password) self.submit_button (By.ID, submitBtn) def login(self, username, password): self.driver.find_element(*self.username_input).send_keys(username) self.driver.find_element(*self.password_input).send_keys(password) self.driver.find_element(*self.submit_button).click()完善的日志记录使用Python的logging模块记录运行信息、错误和警告而不是简单print。异常处理与重试机制网络不稳定、元素偶尔加载失败是常态。使用try...except包裹关键操作并配合重试库如retrying实现自动重试。资源清理务必在finally块或使用with上下文管理器调用driver.quit()确保浏览器进程被关闭防止内存泄漏。Selenium爬虫是一把重型武器它用资源换来了无与伦比的兼容性和拟真度。在动态网页数据抓取、需要复杂交互的自动化任务面前它往往是最高效从开发效率而非运行效率而言的解决方案。掌握其核心原理与避坑技巧就能让这把武器在你手中发挥出最大威力。记住爬虫之道贵在平衡——在数据获取、效率、稳定性和对目标网站的尊重之间找到属于你的那个平衡点。
Python Selenium爬虫实战:从环境搭建到反反爬策略
1. 项目概述为什么Selenium在爬虫领域依然坚挺提到Python爬虫很多人第一反应是requests搭配BeautifulSoup或者Scrapy。确实对于绝大多数静态网页这套组合拳又快又好用。但当你面对的是一个充斥着JavaScript动态渲染、登录验证复杂、操作流程繁琐的网站时传统的HTTP请求库就显得力不从心了。这时Selenium就该登场了。它本质上是一个浏览器自动化工具能模拟真人操作浏览器的一切行为点击、输入、滚动、下拉选择等等。在爬虫领域它扮演着“终极解决方案”的角色专治各种不服——特别是那些数据藏在JS动态加载背后的网站。我最初接触Selenium是为了抓取一个需要登录、然后通过多次点击和筛选才能看到数据的后台管理系统。用requests去逆向分析它的API接口发现其加密逻辑复杂且经常变动维护成本极高。转而使用Selenium虽然速度慢了点但代码逻辑直白得像写操作说明书稳定性反而大大提升。所以别再把Selenium仅仅看作一个“备胎”在应对现代Web应用尤其是单页面应用SPA时它往往是那条最稳妥、最省心的“主干道”。本教程将带你从零开始深入Selenium在Python爬虫中的每一个实用细节避开我踩过的那些坑。2. 环境搭建与核心组件解析工欲善其事必先利其器。用Selenium搞爬虫第一步就是把环境配通。这里面的门道直接关系到你后续脚本是稳定运行还是频频报错。2.1 浏览器与驱动版本匹配是生命线Selenium工作的原理是你的Python代码客户端通过WebDriver协议向一个特定的浏览器驱动程序如chromedriver发送指令驱动程序再去控制真实的浏览器如Chrome。因此浏览器、驱动程序、Selenium库三者的版本兼容性至关重要。浏览器选择Chrome/EdgeChromium内核是首选因为生态最好资料最全。Firefox也可用但驱动geckodriver的更新有时稍慢。驱动下载与管理查看浏览器版本打开Chrome在地址栏输入chrome://version/找到“Google Chrome”后面的版本号例如120.0.6099.109。下载对应驱动访问ChromeDriver官网或国内镜像站。关键点来了你下载的ChromeDriver主版本号必须与你的Chrome浏览器主版本号完全一致。例如Chrome是120.x.x.x就必须找120.x.x.x系列的ChromeDriver。放置驱动将下载的chromedriver.exeWindows或chromedriverMac/Linux放到一个目录并将该目录添加到系统的PATH环境变量中。更简单的做法是在代码里指定驱动路径。注意这是新手最容易栽跟头的地方。经常有人报错“This version of ChromeDriver only supports Chrome version XX”就是因为版本没对上。我的习惯是将驱动放在项目根目录的drivers文件夹下在代码中显式指定路径这样项目移植时不会出错。安装Selenium库这个最简单pip install selenium即可。建议使用虚拟环境管理项目依赖。2.2 基础脚本从打开浏览器到拿到源码环境配好我们来写第一个脚本感受一下Selenium的“所见即所得”。from selenium import webdriver from selenium.webdriver.chrome.service import Service import time # 1. 指定ChromeDriver路径 driver_path ./drivers/chromedriver # 根据你的实际路径修改 service Service(executable_pathdriver_path) # 2. 配置浏览器选项可选但很重要 options webdriver.ChromeOptions() # 添加常用选项 options.add_argument(--headless) # 无头模式不显示浏览器界面 options.add_argument(--disable-gpu) # 禁用GPU在某些系统上需要 options.add_argument(--no-sandbox) # 在Linux环境下可能需要 options.add_argument(--disable-dev-shm-usage) # 解决共享内存问题 # 防止被检测为自动化工具初级反反爬 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) # 3. 启动浏览器 driver webdriver.Chrome(serviceservice, optionsoptions) # 执行CDP命令进一步隐藏自动化特征 driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}) }) try: # 4. 访问网页 driver.get(https://www.example.com) # 等待页面加载简单粗暴的方式后面会讲更优的等待策略 time.sleep(3) # 5. 获取页面信息 print(当前标题:, driver.title) print(当前URL:, driver.current_url) # 获取渲染后的完整HTML源码这是requests做不到的 page_source driver.page_source # print(page_source) # 内容太多通常先打印看看结构 # 6. 一个简单操作在百度搜索框输入内容 driver.get(https://www.baidu.com) search_box driver.find_element(id, kw) # 找到搜索框 search_box.send_keys(Selenium爬虫教程) # 输入文字 search_box.submit() # 提交表单 time.sleep(2) finally: # 7. 关闭浏览器 driver.quit()这个脚本涵盖了最基本的流程。其中浏览器选项ChromeOptions的配置是进阶使用的开始。无头模式--headless对于服务器部署爬虫至关重要可以节省资源。而excludeSwitches和CDP命令则是为了应对一些网站对自动化工具的简单检测属于初步的“反反爬”措施。3. 元素定位八仙过海各显神通定位页面元素是Selenium所有操作的基础。找不到元素点击、输入都无从谈起。Selenium提供了多达8种定位策略但常用的就几种。3.1 八大定位器详解与选用策略driver.find_element(by, value)用于定位单个元素返回第一个匹配项。driver.find_elements(by, value)用于定位所有匹配元素返回一个列表。ID定位 (By.ID): 最优先选择。ID通常唯一定位最快、最准。driver.find_element(id, su)。Name定位 (By.NAME): 次优先。常用于表单元素。driver.find_element(name, wd)。Class Name定位 (By.CLASS_NAME): 注意class属性可能有多个值用空格分隔这里需要传入完整的单个class值或其中的一个。driver.find_element(class name, s_ipt)。Tag Name定位 (By.TAG_NAME): 按标签名定位如input,div,a。通常用于找多个同类元素。driver.find_elements(tag name, a)。Link Text / Partial Link Text (By.LINK_TEXT,By.PARTIAL_LINK_TEXT): 专门用于定位超链接a标签。前者需要完整文本后者只需部分文本。driver.find_element(link text, 新闻)。CSS Selector (By.CSS_SELECTOR):功能最强大、最灵活的定位方式。可以组合ID、class、属性、层级关系进行精准定位。语法和前端CSS选择器一模一样。强烈建议掌握。#id通过ID定位。.class通过class定位。input[namewd]通过标签名和属性组合定位。div#container ul.list li:nth-child(2)通过层级关系定位。XPath (By.XPATH): 和CSS Selector并列的定位神器。它通过XML路径来定位元素功能极其强大甚至可以定位没有ID、Class的元素或者通过文本内容定位。绝对路径/html/body/div[1]/form/input脆弱不推荐。相对路径//input[idkw]推荐。包含文本//a[contains(text(), 登录)]。多个属性//input[typetext and nameuser]。选用策略首选ID、Name简单直接。复杂场景用CSS Selector或XPath对于动态ID、嵌套复杂的元素这是唯一选择。CSS vs XPathCSS Selector通常性能稍好语法更简洁特别是对于class。XPath功能更强可以向上遍历DOM树找父节点并且对文本内容的支持更好。我个人更偏爱CSS Selector因为写起来快但在需要通过文本定位时XPath是无可替代的。3.2 定位实战与常见坑点假设我们要定位百度首页的搜索框和“百度一下”按钮。from selenium.webdriver.common.by import By # 访问百度 driver.get(https://www.baidu.com) # 方法1通过ID定位搜索框最稳 search_box driver.find_element(By.ID, kw) # 方法2通过Name定位同样很稳 search_box driver.find_element(By.NAME, wd) # 方法3通过Class Name定位注意它的class是多个 search_box driver.find_element(By.CLASS_NAME, s_ipt) # ‘s_ipt’是其中一个class # 方法4通过CSS Selector定位灵活 search_box driver.find_element(By.CSS_SELECTOR, #kw) # ID选择器 search_box driver.find_element(By.CSS_SELECTOR, input.s_ipt) # 标签class组合 search_box driver.find_element(By.CSS_SELECTOR, input[name\wd\]) # 属性选择器 # 方法5通过XPath定位强大 search_box driver.find_element(By.XPATH, //input[id\kw\]) search_box driver.find_element(By.XPATH, //*[id\kw\]) # 用*代替标签名 # 定位“百度一下”按钮 # 它没有ID可以用class或者XPath submit_btn driver.find_element(By.CSS_SELECTOR, #su) # 其实它有ID‘su’ submit_btn driver.find_element(By.XPATH, //input[type\submit\]) # 操作输入并搜索 search_box.send_keys(天气预报) submit_btn.click()常见坑点元素未加载/不可见这是最最常见的错误。你代码执行太快页面还没渲染出来就去定位当然找不到。解决方案是“等待”下一章详述。元素在iframe/frame内如果元素嵌套在iframe里你需要先切换到对应的frame中才能定位。# 1. 先找到这个iframe元素 iframe driver.find_element(By.TAG_NAME, iframe) # 2. 切换进去 driver.switch_to.frame(iframe) # 3. 现在可以定位iframe内的元素了 inner_element driver.find_element(...) # 4. 操作完成后切回主文档 driver.switch_to.default_content()动态ID/Class有些前端框架如React Vue会生成随机的ID或Class。此时绝不能依赖它们。要寻找其父级或相邻元素中稳定的特征使用CSS Selector或XPath的层级关系来定位。多个匹配元素使用find_element只会返回第一个。如果你需要操作第二个、第三个请使用find_elements获取列表后按索引操作或者使用更精确的选择器如:nth-child(n)。4. 等待的艺术告别time.sleep拥抱智能等待新手最爱用time.sleep(10)简单粗暴。但这是最低效的方式因为你永远不知道网络或服务器到底需要多久。设置短了元素没出来就报错设置长了大量时间在空等。Selenium提供了两种优雅的等待方式显式等待和隐式等待。4.1 显式等待精准狙击显式等待是针对某个特定条件进行等待条件满足则立即继续执行超时则抛出异常。这是推荐的主要等待策略。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 设置一个最长等待10秒的等待器 wait WebDriverWait(driver, 10) try: # 等待直到ID为‘kw’的元素出现在DOM中并且可见 search_box wait.until( EC.visibility_of_element_located((By.ID, kw)) ) # 一旦等到元素就可用了 search_box.send_keys(hello) except TimeoutException: print(等待超时元素未出现) # 其他常用的预期条件 (EC) # EC.presence_of_element_located: 元素出现在DOM中不一定可见 # EC.element_to_be_clickable: 元素可点击 # EC.text_to_be_present_in_element: 元素中包含特定文本 # EC.title_is / EC.title_contains: 标题判断显式等待的核心优势它轮询检查条件默认每0.5秒一次一旦条件满足就立刻返回最大程度节省时间。你需要根据操作目的选择合适的条件。例如要点击一个按钮就用element_to_be_clickable要获取一个元素的文本用presence_of_element_located通常就够了。4.2 隐式等待全局守候隐式等待是设置一个全局的等待时间在查找任何一个元素时如果元素没有立即找到WebDriver会等待一段时间直到超时。driver.implicitly_wait(10) # 单位秒 # 此后所有 driver.find_element 操作都会最多隐式等待10秒 element driver.find_element(By.ID, someId)隐式等待的坑只对find_element系列方法生效对get(url)等无效。和显式等待混用可能导致总等待时间变长。例如隐式等待10秒显式等待也10秒在最坏情况下实际等待时间可能是20秒。不够灵活无法针对特定条件如可点击、文本出现进行等待。最佳实践几乎总是使用显式等待因为它更精确、更高效。可以设置一个很短的隐式等待作为兜底如3秒处理一些简单的、确定性的元素。绝对避免混用长时间的隐式等待和显式等待。我的个人习惯是driver.implicitly_wait(0)禁用隐式等待全程使用显式等待代码控制力更强。4.3 强制等待与页面加载策略time.sleep()就是强制等待除非在调试或者等待非元素相关的固定流程如等待页面跳转完成否则应尽量避免。此外浏览器的页面加载策略也会影响等待from selenium.webdriver.common.desired_capabilities import DesiredCapabilities caps DesiredCapabilities.CHROME # ‘normal’等待整个页面包括静态资源加载完成。默认值。 # ‘eager’等待DOMContentLoaded事件完成DOM树构建好不等待图片、样式表等。 # ‘none’完全不等待。 caps[pageLoadStrategy] eager # 或 none driver webdriver.Chrome(desired_capabilitiescaps)在爬虫场景设置为eager可以显著加快driver.get()的速度因为我们通常只关心DOM结构不关心图片是否加载完。5. 元素操作与高级交互定位到元素后就可以对它为所欲为了。基础操作包括点击、输入、清空、提交等。5.1 基础操作与表单处理# 假设我们已经定位到元素 elem elem driver.find_element(By.ID, someId) # 1. 点击 elem.click() # 2. 输入文本 elem.send_keys(你要输入的文字) # 3. 清空输入框 elem.clear() # 通常在send_keys之前调用 # 4. 提交表单 # 如果元素在一个表单里可以用submit()。但更通用的做法是找到提交按钮并click()。 elem.submit() # 5. 获取元素属性、文本、状态 print(elem.get_attribute(href)) # 获取属性值如a标签的href print(elem.text) # 获取元素内的可见文本非常重要 print(elem.is_displayed()) # 是否可见 print(elem.is_enabled()) # 是否可用可操作 print(elem.is_selected()) # 是否被选中用于复选框、单选框 # 6. 处理下拉选择框Select from selenium.webdriver.support.ui import Select select_elem driver.find_element(By.NAME, country) select Select(select_elem) # 三种选择方式 select.select_by_index(1) # 通过索引从0开始 select.select_by_value(cn) # 通过option的value属性 select.select_by_visible_text(中国) # 通过显示的文本 # 获取所有选项 all_options select.options5.2 高级交互动作链与执行JS有些交互比如拖拽、鼠标悬停、复杂组合键需要用到ActionChains动作链。from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys # 鼠标悬停 menu driver.find_element(By.ID, menu) sub_menu driver.find_element(By.ID, submenu) actions ActionChains(driver) actions.move_to_element(menu).perform() # 移动到menu上触发悬停效果 # 等待子菜单出现这里需要显式等待 wait.until(EC.visibility_of(sub_menu)) sub_menu.click() # 拖拽元素 source driver.find_element(By.ID, draggable) target driver.find_element(By.ID, droppable) actions.drag_and_drop(source, target).perform() # 组合键操作 actions.key_down(Keys.CONTROL).send_keys(c).key_up(Keys.CONTROL).perform() # 模拟CtrlC执行JavaScript这是Selenium的“大招”可以完成WebDriver API无法直接实现的操作。# 滚动到页面底部 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(2) # 等待滚动后内容加载 # 滚动到某个元素 element driver.find_element(By.ID, footer) driver.execute_script(arguments[0].scrollIntoView(true);, element) # 修改元素属性可用于绕过一些前端限制 driver.execute_script(document.getElementById(readonlyInput).removeAttribute(readonly);) # 获取通过JS渲染后的数据有时.text属性拿不到 shadow_content driver.execute_script(return document.querySelector(...).shadowRoot.innerHTML;) # 点击被遮挡的元素 driver.execute_script(arguments[0].click();, element)6. 爬虫实战应对复杂场景与反爬策略现在我们把所有知识串联起来完成一个模拟真实爬虫需求的实战。6.1 实战案例爬取动态加载的电商商品列表目标爬取一个类似淘宝搜索页的商品列表该列表是向下滚动时通过AJAX动态加载的。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 import time import json options webdriver.ChromeOptions() # 无头模式并添加一些参数防止被识别 options.add_argument(--headless) options.add_argument(--disable-blink-featuresAutomationControlled) prefs {profile.managed_default_content_settings.images: 2} # 不加载图片加速 options.add_experimental_option(prefs, prefs) driver webdriver.Chrome(optionsoptions) wait WebDriverWait(driver, 15) base_url https://s.taobao.com/search?q手机 driver.get(base_url) product_list [] last_height driver.execute_script(return document.body.scrollHeight) scroll_pause_time 2 # 每次滚动后等待加载的时间 max_scroll 10 # 最多滚动10次防止无限循环 scroll_count 0 while scroll_count max_scroll: # 1. 滚动到底部 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(scroll_pause_time) # 等待新内容加载 # 2. 计算新的滚动高度判断是否已到底部 new_height driver.execute_script(return document.body.scrollHeight) if new_height last_height: # 高度未变可能已加载完毕或遇到“加载更多”按钮 try: # 尝试寻找并点击“加载更多”按钮 load_more_btn driver.find_element(By.CSS_SELECTOR, .load-more-btn) load_more_btn.click() time.sleep(scroll_pause_time * 2) except: # 没有找到按钮可能真的到底了 print(已滚动到底部或无法继续加载。) break last_height new_height scroll_count 1 print(f已滚动第 {scroll_count} 次当前页面高度{new_height}) # 3. 开始解析当前页面中的所有商品项 # 假设每个商品项有一个共同的CSS选择器 items driver.find_elements(By.CSS_SELECTOR, .item.J_MouserOnverReq) print(f共找到 {len(items)} 个商品项。) for index, item in enumerate(items): try: # 使用相对定位在item元素内查找子元素避免全局定位冲突 # 商品标题 title_elem item.find_element(By.CSS_SELECTOR, .title a) title title_elem.text.strip() link title_elem.get_attribute(href) # 价格 price_elem item.find_element(By.CSS_SELECTOR, .price strong) price price_elem.text.strip() # 销量和店铺名可能不存在 try: sales_elem item.find_element(By.CSS_SELECTOR, .deal-cnt) sales sales_elem.text.strip() except: sales N/A try: shop_elem item.find_element(By.CSS_SELECTOR, .shopname span) shop shop_elem.text.strip() except: shop N/A product_info { index: index 1, title: title, price: price, sales: sales, shop: shop, link: link } product_list.append(product_info) print(f已提取: {title[:30]}...) except Exception as e: print(f解析第{index1}个商品时出错: {e}) continue # 4. 保存数据 with open(taobao_products.json, w, encodingutf-8) as f: json.dump(product_list, f, ensure_asciiFalse, indent2) print(f数据已保存共 {len(product_list)} 条记录。) driver.quit()这个案例涵盖了无头模式、滚动加载、动态等待、异常处理、数据提取和保存。关键在于滚动逻辑和使用相对定位在item元素内find_element这能大大提高定位的准确性和鲁棒性。6.2 常见反爬策略与应对措施检测WebDriver一些网站会检测navigator.webdriver属性。我们之前用CDP命令已经将其置为undefined。还可以通过options.add_experimental_option(excludeSwitches, [enable-automation])来移除“正受到自动测试软件控制”的提示。行为指纹检测鼠标移动轨迹、点击速度等是否像机器人。应对方法使用ActionChains时加入随机延迟或者使用更高级的自动化框架如puppeteer-extra的Stealth插件在Python中对应undetected-chromedriver库。验证码这是硬骨头。简单图形验证码可以用OCR库如ddddocr,pytesseract尝试识别但成功率有限。复杂验证码如点选、滑块需要接入打码平台如超级鹰、图鉴或机器学习方案。对于爬虫来说遇到复杂验证码通常意味着需要降低频率、更换IP或者考虑是否值得投入成本破解。IP限制与封禁最有效的应对方法是使用代理IP池。from selenium.webdriver.common.proxy import Proxy, ProxyType proxy_ip_port http://123.45.67.89:8080 proxy Proxy() proxy.proxy_type ProxyType.MANUAL proxy.http_proxy proxy_ip_port proxy.ssl_proxy proxy_ip_port capabilities webdriver.DesiredCapabilities.CHROME proxy.add_to_capabilities(capabilities) driver webdriver.Chrome(desired_capabilitiescapabilities)更常见的做法是使用--proxy-server命令行选项options.add_argument(f--proxy-serverhttp://{proxy_ip_port})User-Agent检测定期更换User-Agent。from fake_useragent import UserAgent ua UserAgent() random_ua ua.random options.add_argument(fuser-agent{random_ua})7. 性能优化、调试与最佳实践用Selenium爬虫效率是永恒的痛点。以下是提升效率和稳定性的关键点。7.1 性能优化技巧无头模式Headless--headless必备节省大量GUI渲染资源。禁用图片/样式表/字体大幅减少流量和加载时间。prefs { profile.managed_default_content_settings.images: 2, # 2为禁用 permissions.default.stylesheet: 2, } options.add_experimental_option(prefs, prefs)设置页面加载策略为eager或none如前所述。合理使用等待杜绝全局sleep多用显式等待设置合理的超时时间。复用浏览器会话对于需要登录的网站可以保存cookies下次启动时加载避免重复登录。但Selenium直接保存会话状态较复杂通常需要配合pickle库保存cookies。并发与多线程/进程一个WebDriver实例对应一个浏览器进程资源消耗大。实现并发爬取通常采用多进程multiprocessing每个进程独立一个driver。注意线程间共享driver是危险且不推荐的。7.2 调试技巧非无头模式运行在开发调试阶段注释掉--headless选项亲眼看着浏览器操作能快速定位问题。截图出错时自动截图是强大的调试工具。try: some_operation() except Exception as e: driver.save_screenshot(error.png) print(f操作失败已截图。错误信息{e}) raise打印页面源码或元素HTML当定位不到元素时打印driver.page_source或element.get_attribute(outerHTML)检查元素是否真的存在或者你的选择器是否正确。使用浏览器开发者工具在非无头模式下按F12打开开发者工具。在Elements面板可以用CtrlF测试你的CSS Selector或XPath是否唯一匹配。Console面板可以直接执行document.querySelector(...)进行测试。7.3 项目结构与代码最佳实践配置与代码分离将浏览器路径、代理、用户代理等配置项放在config.py或settings.yaml中。使用Page Object模式对于大型爬虫项目将每个页面的定位器和操作封装成一个类。这极大提高了代码的可读性和可维护性。class LoginPage: def __init__(self, driver): self.driver driver self.username_input (By.ID, username) self.password_input (By.ID, password) self.submit_button (By.ID, submitBtn) def login(self, username, password): self.driver.find_element(*self.username_input).send_keys(username) self.driver.find_element(*self.password_input).send_keys(password) self.driver.find_element(*self.submit_button).click()完善的日志记录使用Python的logging模块记录运行信息、错误和警告而不是简单print。异常处理与重试机制网络不稳定、元素偶尔加载失败是常态。使用try...except包裹关键操作并配合重试库如retrying实现自动重试。资源清理务必在finally块或使用with上下文管理器调用driver.quit()确保浏览器进程被关闭防止内存泄漏。Selenium爬虫是一把重型武器它用资源换来了无与伦比的兼容性和拟真度。在动态网页数据抓取、需要复杂交互的自动化任务面前它往往是最高效从开发效率而非运行效率而言的解决方案。掌握其核心原理与避坑技巧就能让这把武器在你手中发挥出最大威力。记住爬虫之道贵在平衡——在数据获取、效率、稳定性和对目标网站的尊重之间找到属于你的那个平衡点。