1. 项目概述与整体设计思路1.1 什么是Selenium为什么先学基础操作Selenium这个名字在自动化测试圈子里混过的朋友应该都不陌生。它是一个专门用来驱动真实浏览器的自动化框架简单说就是让代码像真人一样去操作Chrome、Firefox、Edge这些浏览器打开页面、点击按钮、填写表单、翻页、截屏甚至处理弹窗和多标签页切换。我最早接触Selenium的时候以为它是个很高深的东西真正上手之后才发现最核心的其实就那么几块启动一个浏览器、告诉浏览器去访问哪个地址、控制浏览器窗口的各种状态、定位页面上的元素。在这几块里启动/访问/窗口控制又是重中之重因为不管你后面要写多复杂的自动化脚本——是爬数据还是做UI回归测试——开头这几步都是绕不开的底座。底座不稳后面全白搭。这篇文章的定位比较偏入门和筑基适合两类人一类是刚接触Selenium、连环境都还没配好的新手另一类是写过一些脚本但总是被各种奇怪报错卡住的老手我后面写的问题排查部分大概率能帮你省下不少排查时间。1.2 基础操作在整个自动化体系中的定位很多人学Selenium容易犯一个毛病一上来就盯着页面元素定位去学XPath、CSS Selector研究得头头是道结果自己连浏览器都还没能稳定地拉起来。这就像学开车油门、刹车、方向盘都没摸熟就直接想上高速了。实际上浏览器启动、页面访问、窗口控制这三件事虽然看起来简单却决定了你整个自动化脚本的稳定性。举个例子公司大楼的保安不让你进你代码写得再好也白搭——WebDriver起不来后面所有步骤都执行不了。再比如多窗口切换的句柄管理如果你没搞明白遇到那种点击按钮后弹出新标签页的场景八成会拿元素找不到的报错干瞪眼。所以我建议不管你最终要做什么方向先把这三大基础操作吃透每一步都知道它背后的原理到了写复杂脚本的时候你会感谢当初打下的这个底子。2. 环境准备与技术选型解析2.1 安装Selenium的两种方式说到安装其实现在比前几年省心多了。Selenium从4.0版本开始把很多过去让人头疼的依赖问题都解决了。安装方式很简单用pip直接装pip install selenium如果你用的是Anaconda环境也可以用condaconda install selenium装完之后建议顺手验证一下版本确保能正常importimport selenium print(selenium.__version__)我在实际项目里遇到过不少装不上的情况后来排查下来绝大部分是因为网络源的问题。如果你用默认pip源特别慢或者老超时可以换用国内镜像源速度会立竿见影pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple这里要多说一句Selenium本身只是一个库它能驱动浏览器靠的是一个叫WebDriver的东西。WebDriver相当于浏览器和代码之间的翻译官浏览器不认识Python也不认识Java它只认WebDriver发出来的指令。2.2 WebDriver与浏览器版本的匹配潜规则WebDriver的版本必须和你本机安装的浏览器版本对应上这是Selenium新手踩坑最多的一个点没有之一。你用的Chrome浏览器就下载对应版本的ChromeDriver用Edge就下载msedgedriver用Firefox就下载geckodriver。至于去哪里下载这里不多展开直接搜索对应名字加上版本号一般都能找到官方发行页。为什么说要对应版本因为Chrome更新得太频繁了经常是ChromeDriver还停在旧版本你的Chrome已经自动更新到新版本了。一旦版本不匹配启动浏览器的时候就会报session not created之类的错误。这里我分享一个我自己的习惯装好ChromeDriver之后第一件事就是跑一个最简单的启动脚本确认浏览器能弹出来。这件事花不了30秒但能帮你把环境层面的问题先排除掉后面踩坑的范围就小了很多。2.3 代码里设置驱动的两种配置方法老版本的Selenium写启动代码时必须手动指定driver的路径from selenium import webdriver driver webdriver.Chrome(executable_path/your/path/chromedriver)这个写法在Selenium 4.0之后已经过时了现在更推荐的方式是直接让Selenium自动去系统PATH里找驱动from selenium import webdriver driver webdriver.Chrome()前提是你把ChromeDriver的路径加到了系统环境变量的PATH里。这种做法的好处是代码干净不用把路径写死在脚本里换台机器改一下环境变量就能跑。不过话说回来如果你不想折腾环境变量还有更省事的一条路用Selenium Manager。新版Selenium已经内置了Selenium Manager你只需要指定浏览器名它会自动帮你下载匹配的驱动。对新手来说这是最友好的一种方式但对网络环境有要求因为需要去访问官方源下载驱动。3. 浏览器启动与页面访问实操全记录3.1 启动浏览器的完整流程我习惯把启动浏览器这件事拆成四步来理解创建配置对象、配置浏览器选项、实例化驱动、验证是否启动成功。最基础的一段启动代码长这样from selenium import webdriver from selenium.webdriver.chrome.options import Options # 第一步创建配置对象 options Options() # 第二步往配置里加选项这里先不加后面细说 # options.add_argument(--headless) # 第三步实例化驱动 driver webdriver.Chrome(optionsoptions) # 第四步验证 print(driver.title) print(driver.current_url)实测下来如果上面这段代码能跑通浏览器窗口弹出来环境基本就算OK了。你可能注意到我没有调driver.get()就直接打印title和current_url这时候拿到的是空白页的数据。没关系这一步的目的只是验证浏览器到底能不能被代码驱动起来能起来就说明WebDriver和浏览器版本是匹配的。这里有个细节我要特别强调每次跑完脚本一定要记得调用driver.quit()而不是driver.close()。quit()会把整个浏览器进程彻底关掉close()只是关掉当前标签页。如果你只开了这一个标签页两者看起来没区别但一旦你的脚本开了多个标签页close()关掉当前页之后浏览器进程还在资源就一直占着。我在公司帮同事排查脚本内存占用过高的问题时十有八九都是这个原因。3.2 页面访问与常用导航操作浏览器启动成功之后第一件事自然是访问目标地址。Selenium里最核心的方法就是get()driver.get(https://www.example.com)这里有个坑要提醒一下get()方法会阻塞直到页面完全加载完成onload事件触发才会返回。如果目标网页里有一些第三方统计、广告之类的资源特别慢你的脚本就会停在这一步等很久。遇到这种情况最常见的处理思路是给页面加载设一个超时时间driver.set_page_load_timeout(10)超过10秒还没加载完会抛出TimeoutException。有些老手会故意捕获这个异常然后继续执行后面的逻辑因为对自动化测试来说只要关键元素能操作页面有没有完全加载其实没那么重要。除了get()还有几个导航相关的方法也经常会用到# 后退 driver.back() # 前进 driver.forward() # 刷新 driver.refresh()这三个方法在真实项目中用得非常频繁。特别是刷新很多场景下点击操作之后页面数据没更新先refresh()一下再重新定位元素问题就解决了。还有一个我想单独拿出来说的点地址栏里的URL是怎么被解析的。get()方法里传入的URL必须带协议头也就是http://或者https://。如果你只写www.example.comSelenium会抛InvalidArgumentException。原因很简单浏览器需要知道用什么样的协议去建立连接缺了协议头它就不知道该怎么处理了。3.3 启动参数里的几个实用配置说回到前面预留的options这部分内容虽然不算严格的基础操作但对启动环节的影响非常大。我挑几个最常被用到的启动参数来讲options Options() # 无头模式不弹出浏览器窗口适合跑在服务器上 options.add_argument(--headlessnew) # 关闭沙箱模式解决Linux环境下的一些权限问题 options.add_argument(--no-sandbox) # 忽略证书错误 options.add_argument(--ignore-certificate-errors) # 禁用GPU加速无头模式下有时能避免奇怪的报错 options.add_argument(--disable-gpu) # 指定浏览器窗口初始大小 options.add_argument(--window-size1920,1080)无头模式我单独说几句。headless的意思就是浏览器在后台运行不显示界面。这个模式特别适合放在CI/CD流水线上跑自动化测试因为服务器上根本没有显示器也没有人会去看那个弹出的窗口。但无头模式有个坑有些网站会检测这种环境从而返回异常页面。我见过不少案例同样的脚本带界面跑得好好的一换成无头模式就各种元素找不到排查到最后发现是网站的反爬机制在作怪。所以我的建议是本地调试的时候一定要用有头模式亲眼看着浏览器操作出了问题也容易定位部署到服务器上跑定时任务的时候再考虑切无头。还有一个选项值得单独提就是用户数据目录options.add_argument(--user-data-dir/path/to/chrome/profile)加上这个参数浏览器会加载你指定的用户数据目录相当于带着你平时的登录态、Cookie、收藏夹启动。这个技巧在做一些需要登录态的自动化任务时非常管用可以帮你省掉每次都要重新登录的麻烦。4. 窗口控制从基础属性到多窗口管理4.1 窗口尺寸、位置与最大化窗口控制是很多人容易忽略的一块觉得不就是最大化嘛一句代码的事。但实际上窗口控制里藏着不少细节直接影响自动化脚本的稳定性和执行效率。先看几个基础操作# 获取当前窗口的尺寸 size driver.get_window_size() print(size) # 设置窗口尺寸 driver.set_window_size(800, 600) # 获取窗口位置左上角坐标 position driver.get_window_position() print(position) # 移动窗口位置 driver.set_window_position(100, 100) # 最大化窗口 driver.maximize_window() # 最小化窗口 driver.minimize_window() # 全屏 driver.fullscreen_window()这些方法里最常用的不用想肯定是maximize_window()。为什么因为如果你不设置窗口大小浏览器会以一个默认的尺寸打开而这个初始尺寸在不同电脑、不同分辨率下可能都不一样。这就造成一个问题同样的脚本在A同事电脑上能跑在B同事电脑上就跑挂了报元素被遮挡或者元素不可见。原因不复杂——页面往往是响应式布局窗口宽度不一样元素的位置和可见性就不一样。你把脚本里改成启动后立刻maximize_window()至少窗口大小这个变量就被你控制住了脚本的可复现性会高很多。这段代码放在启动之后、访问页面之前执行效果最好顺序很重要。我自己的习惯是先maximize_window()再driver.get()这样页面一加载出来就是以目标窗口尺寸渲染的避免先加载完页面再改窗口尺寸导致的重排闪烁。4.2 全屏与最大化不是一回事可能有人会问maximize和fullscreen看起来效果差不多有什么区别这么说吧最大化maximize是占满整个屏幕的工作区但窗口这个壳还在顶部依然有标签栏、地址栏Windows的任务栏也依然可见。而全屏fullscreen是真正意义上只显示网页内容浏览器的所有UI元素全部隐藏就像你在浏览器里按F11看到的效果一样。自动化测试里maximize用得远多于fullscreen原因很简单截图对比的时候需要固定的页面帧地址栏和标签栏的样式会影响截图的稳定性。但fullscreen也有它的用途场景比如做某些视频类网站或者展示型页面的自动化操作时全屏模式下反而更接近真实用户的操作习惯。4.3 多窗口切换与窗口句柄管理窗口控制里最有技术含量的部分要数多窗口多标签页的管理。页面里点击一个链接经常会在新标签页里打开目标网页。这时候如果你继续用原来的driver去定位元素大概率会报NoSuchElementException。原因很好理解你的driver还停留在旧标签页的上下文里新标签页里的元素它根本看不见。Selenium用窗口句柄window handle来区分不同的标签页或窗口句柄本质上是一个字符串标识。核心方法有这么几个# 获取当前窗口句柄 current_handle driver.current_window_handle # 获取所有窗口句柄列表 all_handles driver.window_handles # 切换到指定句柄 driver.switch_to.window(target_handle)完整的多窗口切换流程我一般这样写# 记录打开新页面之前的窗口句柄列表 initial_handles driver.window_handles # 执行点击操作触发新标签页打开 element.click() # 等待新窗口出现自己封装一个简单的显式等待 import time time.sleep(2) # 获取新的窗口句柄列表 new_handles driver.window_handles # 遍历找出新增的句柄并切换过去 for handle in new_handles: if handle not in initial_handles: driver.switch_to.window(handle) break这里有一个常见的坑很多人会用wait WebDriverWait(driver, 10)配合expected_conditions来判断新窗口是否打开。这个思路本身是对的但要注意预期条件的写法有个内置条件叫number_of_windows_to_be(2)可以这样用from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10) wait.until(EC.number_of_windows_to_be(2))这个写法比sleep要优雅得多推荐优先使用。切换到新窗口之后如果你还想回到原来的标签页千万别指望driver会自动切回来。你必须手动切回那个旧句柄driver.switch_to.window(initial_handles[0])我调试的时候吃过一次亏脚本里在多窗口之间来回切换切了几轮之后发现driver.current_window_handle居然指向了一个已经关闭的句柄再定位任何元素都报错。后来养成一个习惯每次切换窗口之前都先把需要保留的句柄存到变量里然后显式地去切换而不是依赖当前窗口这个隐含状态。这个习惯让我少踩了很多坑。4.4 关闭窗口的正确姿势前面提到了driver.close()和driver.quit()的区别这里放到窗口控制的语境里再强调一遍。close()是关闭当前窗口或标签页如果关闭的是最后一个标签页那浏览器进程也就跟着退出了。此时driver对象还在如果你继续调用driver.title这类方法会报一个会话失效的错因为浏览器已经关了根本没有页面可以查询。quit()则是彻底关闭整个浏览器进程同时清理掉WebDriver建立的临时文件和代理配置属于干净退出。正规的做法是在脚本的finally块里调用quit()确保不管程序运行成功还是中途挂了浏览器进程都不会残留在后台from selenium import webdriver driver webdriver.Chrome() try: driver.get(https://www.example.com) # 你的业务逻辑 finally: driver.quit()这个小习惯看起来不起眼但在跑批量任务的时候差别很大。没有quit()的脚本跑十遍你可能发现系统多了十个后台的chrome进程每一个都在吃着内存。你把脚本改成统一quit()之后这个现象就消失了。5. 常见问题与排查技巧实录5.1 浏览器启动失败的三大原因我见过最多的启动失败基本集中在三类问题上我把排查顺序和解决方案整理成了一张速查表症状大概率原因解决办法session not created错误WebDriver版本与浏览器版本不匹配去下载与浏览器版本完全对应的WebDriver浏览器一闪而过随即抛错环境变量没配好或WebDriver路径不对检查PATH配置或用Selenium Manager自动管理浏览器能启动但页面白屏启动参数配置了不兼容选项注释掉不常用的options参数逐一排查第三类问题值得多说两句。Selenium的options功能很强大但也正因为灵活很容易出现配置互斥或者配置项参数写错的情况。我见过最典型的是在options里同时加了--headless和一些跟GPU渲染强相关的参数结果窗口起不来。排查的时候把options一行一行注释掉问自己这几行配置到底在干什么比盲目在网上搜报错要高效得多。我还遇到过一种比较隐蔽的情况本机Chrome版本是75但网上搜索教程时顺手下载了别人博客里给的ChromeDriver 2.46。这种教程里明晃晃摆着的版本最容易让人放松警惕因为你会默认它是对的。经验是永远不要相信默认,每次装驱动前都打开浏览器的关于页面确认一下当前版本号再去下载对应驱动。5.2 窗口句柄相关的两个高频报错窗口句柄这个话题新手问得最多的问题是两个拿到多个句柄之后怎么确定哪个是我想要的切过去了为什么又找不到了第一个问题我一般建议用URL或者title来做二次校验而不要单纯依赖句柄顺序。所谓的第几个句柄在不同系统、不同浏览器下可能是不稳定的。稳妥的做法是切过去之后验证一下driver.switch_to.window(handle) if 期望的页面标题关键字 in driver.title: # 确认就是这个窗口继续操作 pass else: # 不是想要的窗口继续找 pass第二个问题就更常见了。很多人切到新窗口干完事情之后没有切回原来的窗口接着去操作旧页面的时候报错信息却是NoSuchElementException。这个报错特别有迷惑性因为它不会直接告诉你你切错窗口了只会告诉你元素找不到。如果你在错误元素上浪费半天时间最后才发现是窗口上下文的问题那种心累我太懂了。所以排查思路要反过来凡是遇到元素找不到先检查当前窗口句柄是不是你想操作的那个窗口。一行代码就能确认print(driver.current_window_handle)跟你预期的不一样切换过去再定位就好了。5.3 页面加载慢导致的未完成困惑还有一个高频问题跟get()方法的阻塞机制有关。你的脚本明明执行了driver.get(url)但页面一直处于加载状态后面的代码迟迟不执行。这通常不是程序卡死了而是页面在等一些慢资源。处理思路有这么几条设置page_load_timeout避免无限等待。用WebDriverWait配合expected_conditions等待某个关键元素出现而不是等整个页面加载完。适当地用driver.execute_script(window.stop())强制停止页面继续加载。第三种方法比较粗暴但有奇效。特别是面对一些新闻门户类网站广告脚本多到离谱根本等不到它自己加载完。你在关键元素定位成功之后就执行stop()能省下不少执行时间。不过要提醒一下window.stop()这个操作是全局性的执行之后页面就不加载新的资源了如果后续还要依赖异步接口返回的数据就要谨慎使用最好只在确认关键元素已经渲染完成的前提下再调用。5.4 截图定位问题的妙用最后分享一个排错小技巧。遇到定位不到元素、页面显示不正常这类问题先别急着打印各种日志直接截一张图看看现场driver.save_screenshot(/tmp/debug.png)截图能看到代码执行那一刻页面的真实状态是弹窗挡住了元素还是页面根本没加载出内容又或者是切错了窗口看到了别的页面一张图全都清楚了。这个方法我已经用了一年多每次排查疑难杂症截图都是第一步。6. 我的一点实操体会文章写到这儿基础的启动、访问、窗口控制逻辑基本都覆盖了。最后分享一个我自己坚持了很久的习惯所有Selenium脚本不管多简单我都维护一个启动-访问-窗口设置-清理退出的四段式模板。启动部分统一封装成函数窗口设置固定先最大化访问地址放在独立的配置变量里退出动作放进finally块。这样写出来的脚本可读性高到了真正做页面元素定位和业务操作的时候才不会因为基础环境问题分心。浏览器基础操作看着简单但它是整个Selenium自动化的地基。把地基打牢了后面学元素定位、显式等待、Page Object模式都是水到渠成的事。你在练习的时候多用几种浏览器跑一跑多试试不同的窗口控制组合很快就能找到自己顺手的那套节奏。