资讯动态

Selenium WebDriver爬虫实战:动态页面采集与性能优化指南

发布时间:2026/10/14 5:23:39 来源:尧图企业网站定制
做爬虫的人迟早会遇到 Selenium WebDriver 这一关。我从 requests 一把梭起步直到碰上那些渲染型页面——在浏览器里明明看得到商品价格、评论内容抓回来的 HTML 却空空如也才发现光靠模拟 HTTP 请求根本拿不到动态加载的数据。Selenium WebDriver 是干这个的利器它驱动真实浏览器去执行页面里的 JavaScript把 DOM 里最终生成的内容原原本本交给你。这篇文章是“爬虫工程师成长之路”系列的第八篇围绕 Selenium WebDriver 从安装、定位、等待到实战采集、问题排查、性能优化完整过一遍适合已经会 requests/Scrapy 但卡在动态页面的爬虫开发者也适合想系统补齐浏览器自动化技能的测试工程师。1. 为什么爬虫工程师要掌握 Selenium WebDriver1.1 静态请求与动态渲染之间的鸿沟先说一个很多人都经历过的场景你用 requests 打开某个页面返回的 HTML 里确实有标题、有框架但涉及数据的部分全是空壳要么是一个巨大的 JSON 占位符要么是一整段要等浏览器执行完 JavaScript 才会填充的 div。原因很简单现在前端框架都在做客户端渲染数据不是服务器拼好的而是页面加载后由脚本请求接口、拿到数据再动态渲染到页面上。你把整个 HTML 保存下来看只有骨架没有血肉。这种页面用传统爬虫怎么处理最经典的方法是抓包找接口、分析加密参数、模拟签名逻辑这是很多老爬虫工程师的看家本领。但这条路对每个网站都要单独逆向工作量大且不说前端代码一改参数就失效维护成本高得惊人。Selenium 换了一条路我不去管你背后的接口长什么样直接把浏览器打开等脚本跑完读取最终的 DOM 结果。把“理解页面渲染逻辑”这件事整体置换成了“模拟真实用户操作”你不需要知道数据从哪个接口来只要知道它在页面上以什么形式呈现这对复杂交互、登录态、分页加载、文件下载这类场景尤其有效。1.2 方案选型Selenium 什么时候是最优解并不是所有爬虫都适合拿 Selenium 做先把场景分清楚再选型否则会掉进“拿着锤子看什么都是钉子”的坑。我自己选型时会先列一张表格过一遍场景推荐方案核心原因纯静态页面、接口参数简单requests BeautifulSoup / lxml开销小、速度快、代码简单目标网站有公开 API 且参数不复杂Scrapy 中间件并发高、调度成熟、生态完善页面有动态渲染且有复杂交互Selenium WebDriver直接操作真实浏览器稳定可靠大规模并发、需要截图录屏Playwright / Selenium Grid异步能力更强、上下文隔离更好只想轻量渲染、不想常驻浏览器渲染服务或 headless 浏览器减少本地资源占用适合定时批跑选择 Selenium 的核心依据是“交互复杂度”。页面如果只是数据延迟渲染、没有任何特殊交互用更轻的方案反而稳但如果页面有元素点击、悬浮菜单、拖拽上传、多标签页操作Selenium 这套 WebDriver API 沉淀多年坑少、资料多、上手路径成熟团队协作成本也低。我曾在某公司实习时踩过一次选型错误一个模拟项目 X 只是需要抓取一批图表数据我上来就上 Selenium结果页面每次刷新都要重新注册图表组件定位按钮极其不稳定维护代码折腾了好几天。后来静下心抓包发现数据其实在页面全局变量里以 JSON 形式存着直接用取值或正则提取速度快了一个数量级。这个教训让我后来养成了习惯动手前先花十分钟想清楚“我要的是 DOM 里的最终结果还是页面呈现后的动作”再决定工具这比盲目堆技术重要得多也省去大量后期返工。2. 环境搭建驱动、浏览器与版本匹配2.1 安装依赖与驱动管理Selenium 本身是一个 Python 包安装非常简单一行命令就能搞定。但这里要提醒一句pip 装的是客户端库而不是浏览器本身真正干活的是电脑里已经装好的 Chrome、Firefox 或 EdgeSelenium 通过一套“驱动程序”去和这些浏览器通信驱动就相当于是遥控器。pip install selenium真正让新手抓狂的是驱动版本问题。Selenium 并不自带浏览器你装好 Chrome 之后还得下载一个版本匹配的 ChromeDriver。驱动和浏览器版本必须一致否则启动时直接报 “session not created” 或 “this version of ChromeDriver only supports Chrome version X”。我第一次搭环境时下载的 ChromeDriver 比本机浏览器高了两个主版本死活启动不了反复折腾才发现驱动主版本号和浏览器主版本号要对齐。现在有一个工具能省掉这个麻烦就是 webdriver-manager。它会根据你本机浏览器的实际版本自动下载匹配的驱动并缓存起来pip install webdriver-managerfrom selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice)如果你用 Firefox就把 ChromeDriverManager 换成 GeckoDriverManager用 Edge 则换成 EdgeChromiumDriverManager。这里有一个实际工程中的坑webdriver-manager 默认需要联网去下载驱动如果运行环境网络受限脚本会长时间卡在下载步骤所以正式的容器镜像里最好把驱动提前下载好、固化到镜像层而不是每次启动都去拉取。2.2 三种启动浏览器的姿势环境搭好了启动浏览器也有不同姿势我在不同阶段分别用过这三种。第一种是最简单的标准启动适合本地小范围调试代码就是 webdriver.Chrome() 一行它会自动去找 PATH 里的驱动并拉起一个带界面的浏览器窗口。第二种是带自定义参数的启动适合自动化采集。常见的参数包括无头模式、禁用 GPU、固定窗口大小、指定语言等from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headless) # 无头模式不开浏览器界面 options.add_argument(--disable-gpu) # 禁用 GPU 加速 options.add_argument(--no-sandbox) # 容器环境通常需要 options.add_argument(--window-size1920,1080) # 固定窗口大小保证布局稳定 options.add_argument(--langzh-CN) # 指定页面语言 options.add_experimental_option(prefs, { profile.default_content_setting_values.notifications: 2 }) driver webdriver.Chrome(optionsoptions)这些参数不是随便加的每个都对应一类实际问题固定窗口大小是为了避免因为窗口尺寸不同导致元素遮挡或懒加载不触发禁用 GPU 是为了避免某些服务器环境下渲染异常no-sandbox 则是容器里跑 Chrome 的必备项不加很可能直接闪退。第三种是复用已有浏览器进程通过远程调试端口连接。典型场景是调试阶段先用普通浏览器手工登录、保存 cookie、把目标页面调到指定状态然后让 Selenium 连上同一个浏览器继续操作免去每次都要重新登录的麻烦。实现方式是在命令行启动 Chrome 时加上远程调试端口代码里用 Remote 连接。这种方式的坑在于页面标签页是共享的定位元素容易串到其他标签页我一般只把它用于调试正式的采集脚本还是走标准启动保证环境可重复、结果可预期。3. 定位与等待稳住动态页面的基本功3.1 定位方式的选择与 XPath 实战WebDriver 最核心的动作就是“找到元素再对它做点什么”。Selenium 提供了 id、name、class name、tag name、css selector、xpath 等定位方式。按我实际项目的使用频率排序css selector 和 xpath 占绝大多数id 有就用但不多见class name 很少直接用因为现在动态页面的 class 经常是一长串混淆字符串直接用很容易踩雷。给新手一个建议简单场景优先用 css selector复杂层级和文本匹配用 xpath。比如定位一个按钮css 可以这样写from selenium.webdriver.common.by import By btn driver.find_element(By.CSS_SELECTOR, button.submit-btn)但如果按钮在不同状态下文本会变化用 xpath 按文本定位更稳btn driver.find_element(By.XPATH, //button[contains(text(), 加载更多)])实际练习里最常用的几个 xpath 模式按属性匹配用 //div[data-id123]按后代文本匹配用 //div[.//span[text()价格]]按出现序号匹配用 (//div[classitem])[2]。把这些记熟大部分定位需求都能覆盖。还有一个容易被忽略的细节find_element 和 find_elements 的区别。前者返回第一个匹配项后者返回所有匹配项的列表。需要判断页面上是否存在某个元素时很多人习惯 try/except 配合 find_element但异常捕获的开销和代码可读性都不理想我推荐用 find_elements 判空代替items driver.find_elements(By.XPATH, //div[classitem]) if not items: print(还没有加载出数据) else: for item in items: # 处理每个元素这种写法的另一个好处是天然支持列表遍历当你需要处理所有匹配元素时find_elements 直接返回列表不用先定位容器再二次查找代码结构清晰很多。3.2 隐式等待、显式等待与强制停顿怎么用动态页面的最大麻烦是时机。元素还没渲染完你就去找肯定报 NoSuchElementException。Selenium 官方推荐用“等待”机制解决但我观察很多新手只会用 time.sleep(3)这既慢又不可靠——网络快的时候白白浪费 3 秒慢的时候 3 秒还不够日志里堆满超时和重试。睡眠时间设大了拖慢全流程设小了又经常抽风确实让人很头疼。隐式等待是全局生效的一旦设置之后的每次 find_element 都会在元素未出现时多等一小会儿。元素早点出现就早返回不出现才等到超时时间。适合做全局兜底但副作用是元素确实不存在时查询会白白等满超时时间才抛异常如果代码里大量循环查询累计等待时间会非常可观所以隐式等待的超时值不宜设得太大。driver.implicitly_wait(5)显式等待是更精准的手段针对某个具体条件轮询等待。它和 time.sleep 最大的区别是“事件驱动”元素等到了就立刻继续等不到才一直等到超时。实际场景中最常用的是 WebDriverWait 配合 expected_conditionsfrom selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, timeout10, poll_frequency0.5) button wait.until( EC.element_to_be_clickable((By.XPATH, //button[contains(text(), 加载更多)])) )timeout 是总时长poll_frequency 是每 0.5 秒轮询一次比固定 sleep 快很多又稳。常用的 expected_conditions 有 element_to_be_clickable、presence_of_element_located、visibility_of_element_located、text_to_be_present_in_element 等每个都有明确的适用场景用错会有隐蔽问题后面排查章节再展开。我最初也踩过混用的坑隐式等待和显式等待同时设置时等待行为会叠加轮询间隔莫名翻倍页面元素明明很快就出现了脚本却总感觉卡顿。后来的统一策略是全局只设一个较短的隐式等待做兜底比如 5 秒关键操作全部用显式等待精确控制普通操作之间不用 time.sleep除非确实有动画过渡需要缓冲。这个组合目前用下来最舒服既能兜底又不过度浪费。4. 实战案例商品列表页的数据采集4.1 先拆分页面结构再动手写代码纸上得来终觉浅这里用一个模拟项目把完整流程走一遍。假设要抓取的某演示站点是典型的商品列表页顶部有筛选项往下是商品卡片卡片里包含名称、价格、销量和详情链接页面底部“加载更多”按钮点击后会自动追加新的卡片一共要翻 10 次才能拿完所有数据。动手之前先拆三步打开页面、循环点击加载、提取数据。最容易犯的错误是一上来就写定位代码连页面实际长什么样都没看。我的习惯是先在浏览器开发者工具里按 F12 检查元素把商品卡片的公共父节点找出来确认每次点击加载后新卡片接入的规律然后再写代码。这个前置分析看起来多花几分钟实际上能帮你避开大量的无效定位尝试。页面拆解的结果通常是这样商品卡片的公共容器是 div.product-item名称在卡片内部的 h3.product-name价格是 span.product-price销量是 span.sales-count加载按钮是 button.load-more。把这些结构记下来接下来的定位代码就顺理成章了。如果页面里每个卡片结构一致就放心用相对定位在卡片内部提取字段千万不要用全局绝对路径去定位每一个商品名称那样一旦页面结构有微调整个脚本就全废了。4.2 企稳式采集代码完整实现整体代码我习惯写成“函数 主流程”的结构每一块职责单一既方便复用也方便出问题时快速定位是哪一段出了问题。下面这段代码是这个模拟项目的完整实现import csv import time 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 def init_driver(): options Options() options.add_argument(--headless) options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) return webdriver.Chrome(optionsoptions) def wait_for_products(driver, timeout10): wait WebDriverWait(driver, timeouttimeout) return wait.until( EC.presence_of_all_elements_located((By.XPATH, //div[classproduct-item])) ) def collect_page_items(driver, products): items [] for product in products: try: name product.find_element(By.XPATH, .//h3[classproduct-name]).text.strip() price product.find_element(By.XPATH, .//span[classproduct-price]).text.strip() sales product.find_element(By.XPATH, .//span[classsales-count]).text.strip() items.append({name: name, price: price, sales: sales}) except Exception as e: print(f[warn] 解析商品失败: {e}) return items def save_to_csv(filename, rows): with open(filename, a, encodingutf-8, newline) as f: writer csv.DictWriter(f, fieldnames[name, price, sales]) writer.writerows(rows) if __name__ __main__: driver init_driver() try: driver.get(https://example.com/list) for page in range(10): products wait_for_products(driver) if not products: print(未拿到商品数据退出) break rows collect_page_items(driver, products) save_to_csv(output.csv, rows) print(f第 {page 1} 批已保存 {len(rows)} 条) load_more driver.find_elements(By.XPATH, //button[contains(text(), 加载更多)]) if not load_more: print(没有加载更多按钮采集结束) break load_more[0].click() time.sleep(1) finally: driver.quit()示例页面使用保留域名 example.com实际运行时请替换成你自己的测试页面。这段代码里有几个关键点值得展开说。第一个是 XPath 里的.//和//的区别。在 product 这个元素内部继续查找时.//h3表示从当前元素的后代里查找而//h3会从整个文档根节点开始全局查找。一旦页面顶部还有其他 h3 元素用后者就会抓到完全错误的数据。我第一次写时就是少了一个点结果把页面标题抓成了商品名排查了半天才发现是定位范围的问题。第二个是单卡解析失败不影响整体。列表页面里偶尔会有某个商品缺字段如果解析异常直接抛出去整个采集脚本就断掉了用 try/except 包住单条解析顶多丢一条数据脚本还能继续往下跑。批量任务里“宁可缺一不可死”是最基本的哲学尤其当采集时长动辄几小时的时候一次小异常导致全盘重来谁都受不了。第三个是点击“加载更多”后加了 time.sleep(1)。虽然前面已经用了显式等待但页面有滚动加载动画时新卡片是渐进式出现的立刻去定位新卡片很可能只拿到上一批的旧数据。等一秒在这里是合理的属于“动画缓冲”场景比无脑 sleep 三秒高效得多。等多久要自己实测一般 0.5 到 2 秒就够。4.3 数据落盘与失败重试上面代码里用了最简单的“追加 CSV”落盘方式因为示例数据量不大。实际工程中我强烈建议每页保存一次而不是最后一次性保存这样即使跑了一半崩了已经抓到的数据也不会丢。保存时用追加模式字段名写进表头后面做数据分析时直接读 CSV 就行省去很多二次整理。还有一个很容易被忽略的点是重试。网络抖动、页面结构临时变化、驱动崩溃都可能导致某次抓取失败我习惯给整个采集流程包一个重试函数def run_with_retry(task, retry_times3): for attempt in range(retry_times): try: return task() except Exception as e: print(f[retry] 第 {attempt 1} 次失败: {e}) time.sleep(2 attempt * 0.5) return None重试次数和间隔也要讲究间隔不要固定加一点递增或者随机抖动避免整个脚本表现出明显的机器节奏重试两三次还不行就干脆放弃返回 None 交给外层人工介入不要无限循环硬扛。真正的采集任务里“快速失败”往往比“死磕到底”更节省整体时间因为大多数失败原因是暂时性的过一段再重跑反而更靠谱。5. 高频报错与排查技巧实录5.1 元素找不到、超时与 iframe 陷阱Selenium 最常见的报错就是 NoSuchElementException 和 TimeoutException。原因五花八门但按照我排障的经验顺序一定是先查三个地方。这就像修水管先关总阀一样别一上来就怀疑代码逻辑先排除环境问题。第一元素是不是在 iframe 里。页面里嵌了一个其他系统的区域常见于登录框、支付弹窗、地图模块iframe 里的元素默认在外面看不到必须先切换进去driver.switch_to.frame(frame_id_or_name) # 操作完再切回主文档 driver.switch_to.default_content()判断方法很简单在开发者工具里看元素标签是不是包在 iframe 标签内或者检查元素所在位置是否来自独立子文档。切进去容易出来时别忘了切回主文档否则后续定位全会跑偏。第二元素是不是开在了新窗口或新标签页里。点击一个链接打开新窗口后driver 的焦点仍停留在旧页面要对新窗口执行操作就必须先切窗口句柄handles driver.window_handles driver.switch_to.window(handles[-1])第三元素本身是动态生成的刚出现时可见性不足。这就要回到等待那一节的核心你用的是 presence 还是 visibility。presence_of_element_located 只代表 DOM 里有这个节点不代表它可见如果元素定义了却被 CSS 隐藏用 presence 判断会误以为它已经出现后续点击就会报 element not interactable。所以点按钮用 element_to_be_clickable读文本用 visibility_of_element_located这两个条件才符合“真的可以操作”的语义。5.2 浏览器进程残留与资源释放另一个高频问题不是报错而是“脚本跑完了机器却越来越卡”。很多人写爬虫只记得开头 webdriver.Chrome()忘了结束要 quit。结果脚本一崩溃浏览器进程就留在后台一次采集能攒下十几个僵尸 Chrome 进程内存直接报警排查半天还找不到原因。正确姿势是无论正常结束还是异常退出都必须保证 driver.quit() 被调用。最稳妥的就是用 finallydriver None try: driver webdriver.Chrome() # 业务逻辑 finally: if driver is not None: driver.quit()批量任务里还有个更隐蔽的坑如果 driver 已经崩溃再次调用 quit() 会抛异常。所以要给 quit 也套一层 try/except或者把 driver 的生命周期管理封装成上下文管理器出错时不至于二次报错掩盖原始异常。如果已经产生了残留进程Linux 下可以按进程名清理Windows 下在任务管理器里结束 chrome.exe 进程树即可。理解思路就能举一反三关键是“进程树级别清理”因为 Chrome 本身会拉起一堆子进程只杀父进程是不够的。5.3 关于反爬检测与合规采集的提醒说到动态渲染采集绕不开的一个话题是反爬。现在不少站点会检测浏览器指纹、检测 headless 特征、检测 navigator.webdriver 属性一旦命中就返回验证码或者渲染假数据。很多新人第一反应是搜怎么伪装成真人我明确说一下这里讨论的都是技术原理目的是让你理解现象、排查脚本为何不稳定而不是教你针对特定网站做违规绕过。真正规范的思路其实有三点第一只采集你有权限访问、且使用条款允许的数据第二对目标站点保持合理频率不冲击正常服务不做会加重别人服务器负担的操作第三优先使用官方提供的 API 或页面公开的结构化数据。合规和效率并不矛盾长期来看反而更省心不用成天担心脚本因为反爬升级而失效。在没有条件取得授权、仅做技术验证时我的做法是严格控制频率每次请求间隔设置随机延时同时尽量让脚本保持“自然节奏”而不是伪装成真人。所谓反爬对抗本质上是在持续消耗资源没有任何爬虫能保证永远有效把脚本本身的稳定性守住不给自己和站点添麻烦远比研究各种花哨的伪装手法更实际。6. 性能优化与工程化落地6.1 让 Selenium 跑得更快的几个配置Selenium 慢是出了名的一个页面从启动浏览器到拿到数据可能要几十秒。围绕“降低单任务耗时”和“提高并行度”两个方向我总结几个立竿见影的做法实测下来效果都不错。第一个是无头模式加禁用无用资源。无头模式不开窗口省掉渲染 UI 的开销再配合浏览器 preferences 禁用图片、弹窗、CSS 字体加载大多数页面能提速 20% 到 40%。注意禁用图片要谨慎某些页面数据依赖图片懒加载或者元素尺寸布局禁了图反而导致数据抓不到prefs { profile.managed_default_content_settings.images: 2, # 禁止图片 profile.default_content_setting_values.notifications: 2, } options.add_experimental_option(prefs, prefs)第二个是复用 Driver 实例。一个 driver 对应一个浏览器进程频繁创建销毁是重量级操作。单任务循环采集时整个流程只启动一次 driver然后在循环里不断 get 不同页面最后统一 quit。我实测启动一次 Chrome 再做 10 次抓取比每次只抓 1 次就退出再重新启动总耗时能省一半以上。与之配套的是页面级超时设置比如 page_load_timeout 和 script_timeout避免某个页面异常时无限等下去。第三个是减少无意义的等待。显式等待的 timeout 不要设成一个巨大的值尤其对已知可能不存在的元素宁可快速失败进重试逻辑也不要陪它干等 30 秒。很多页面其实 5 秒内就会出数据超过 10 秒大概率是出了问题这时候快速失败、记录日志、换下一个页面整体产出反而更高。6.2 接入调度任务与结果存储采集脚本写好后下一步是把它变成可调度的任务。两个方向我都实践过按场景选就好。第一个是轻量定时跑。用系统自带的定时任务或者 Python 的 schedule 库每天固定时间跑一次增量采集结果写入 CSV 或 SQLite。对这种场景我建议在脚本入口加参数控制采集页数和输出目录这样调度时直接传参就行不用每次改代码import sys pages int(sys.argv[1]) if len(sys.argv) 1 else 10 output sys.argv[2] if len(sys.argv) 2 else output.csv第二个是接入队列和并行 worker。数据量大、目标页面多的时候让多个 worker 各负责一批 URL。注意 Selenium 的 driver 是重量级资源进程级并行优于线程级并行Python 的 GIL 会限制线程效率所以用 multiprocessing 起多个进程每个进程一个 driver各自负责一部分 URL最后汇总入库。并行数不要盲目开大浏览器进程占用的内存和 CPU 都不少4 个并发通常已经能满足绝大多数场景太多反而会因为系统资源争抢导致浏览器崩溃。结果存储方面超过几千条就建议别再用 CSV 了入 SQLite 或 MySQL 更稳。入库前做一次数据清洗价格字符串去掉货币符号转成浮点销量为空时补 0日期统一格式。这部分听着简单却是真实数据分析环节最大的坑——脏数据比少数据可怕得多因为它是隐蔽的会污染你后面的一切结论。6.3 从 Selenium 平滑过渡到 Playwright最后聊一下技术路线的下一步。Selenium WebDriver 是入门浏览器自动化的经典选择但这个领域这些年变化很快新项目里我越来越多地推荐 Playwright。它自带浏览器内核管理、自动等待、截图录屏、移动端模拟接口更现代化对并发场景的支持也更好。如果在 Selenium 上已经积累了经验迁移并不难核心概念是一一对应的维度Selenium WebDriverPlaywright驱动管理手动下载 webdriver 或 webdriver-manager自动安装并管理浏览器内核核心对象driver / elementpage / locator元素定位find_element(By.XPATH, ...)locator(xpath...)等待机制WebDriverWait expected_conditions默认自动等待极少手写等待会话隔离需要手动管理窗口句柄BrowserContext 天然隔离迁移的收益在长期维护时特别明显。Selenium 时代要手工维护的驱动版本、等待逻辑在 Playwright 里大部分被框架吃掉代码量明显减少出问题的点也少。但 Selenium 也有不可替代的场景老项目迁移成本高、某些浏览器插件生态依赖、团队既有技术栈沉淀都可能让你继续留在 Selenium。我的建议是两条线并行Selenium 作为基本功要扎实掌握它帮你理解浏览器自动化的底层机制新项目优先评估 Playwright兼顾迭代效率与维护成本。掌握前者是理解地基学会后者是提升效率两者并不冲突。最后分享一个我坚持了很久的小习惯。现在写任何 Selenium 脚本我都不会直接拿最终代码开跑而是先写一个最小验证脚本打开目标页面、打印 driver.title、截图保存确认环境通、版本对、页面出得来再往里面加业务逻辑。这个习惯救过我很多次它能把“环境问题”和“代码问题”快速分开避免你在定位逻辑和驱动配置之间来回折腾。还有浏览器自动化这条路上的问题九成以上出在“时机”和“上下文”上。数据没加载完就去找元素是时机问题找错了 iframe 或者窗口句柄是上下文问题。把这两个意识刻进脑子里Selenium 基本就稳了。爬虫成长之路越往后走真正比拼的其实不是会用多少工具而是排查问题的思路是否清晰、代码结构是否扛得住长期维护。希望这篇能把 Selenium 这一关讲透让你少走一些我走过的弯路。

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

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

免费获取报价 →
↑