1. 先搞清楚driver到底是个什么东西,不然后面全是死记硬背
刚接触selenium的朋友,十个里有八个是这么学的:打开教程,看到driver.find_element(By.ID, "kw").send_keys("xxx"),复制粘贴,跑通了,收工。等到页面稍微变一下,元素找不到了,瞬间懵掉,然后开始到处搜"selenium定位不到元素怎么办"。我当年也是这样过来的。
问题出在哪?出在大家把driver当成了一堆API的集合去背,而没有把它当成一个"遥控器"去理解。driver在selenium里承担的角色,本质上是你本机浏览器的一个远程控制器,准确的类名是WebDriver。它本身不解析HTML,不渲染页面,也不执行JS——这些活儿全是浏览器干的。driver做的事情只有一件:把你写的Python调用,翻译成浏览器能听懂的指令(在selenium 4里走的是W3C WebDriver协议),然后把浏览器返回的结果再翻译回Python对象给你。
注意:网上搜"driver"很容易搜到显卡驱动、打印机驱动、虚拟机驱动那一堆东西,它们和这里的WebDriver没有半毛钱关系。selenium语境下的driver永远指WebDriver及其各浏览器实现类。
把这句话想明白,很多东西就顺了。为什么find_element找不到元素?因为浏览器那一侧根本没匹配到,driver只是如实告诉你"没有"。为什么click()会报元素不可交互?因为浏览器算出来这个元素当前被遮挡或者不可见,driver拒绝执行。你的所有排查动作,最终都要落到"浏览器现在到底看到了什么"这个问题上。
这篇文章我打算把driver上那些真正会被用到的属性和方法按功能分类过一遍,包括初始化、导航、定位、交互、等待、状态读取、截图日志这几大块,每一块都会说清楚"什么场景用它""为什么这么设计""坑在哪"。适合已经能跑通第一个demo、但遇到复杂页面就抓瞎的同学,也适合写了半年脚本想回头把基础夯实的人。全文的代码示例按selenium 4.x的写法来,3.x的部分我会单独标注差异。
2. driver的初始化与生命周期,很多坑从第一行就埋下了
2.1 浏览器驱动的创建方式和参数传递
最朴素的写法长这样:
from selenium import webdriver driver = webdriver.Chrome()selenium 4.6之后内置了Selenium Manager,会自动去下载匹配当前浏览器版本的驱动,不用再手动下载chromedriver丢到PATH里了。这是这几年的一个大改进,以前版本对不上直接报"session not created: This version of ChromeDriver only supports Chrome version XX",能把人折腾半小时。
需要传启动参数的时候,用Options对象:
from selenium import webdriver from selenium.webdriver.chrome.options import Options opts = Options() opts.add_argument("--headless=new") opts.add_argument("--window-size=1920,1080") opts.add_argument("--disable-gpu") opts.add_experimental_option("excludeSwitches", ["enable-automation"]) driver = webdriver.Chrome(options=opts)这里有个细节值得说:add_argument是加启动命令行参数,add_experimental_option是加浏览器偏好设置,两者作用层面不一样。像--headless这种属于前者,excludeSwitches这种属于后者。我第一次用的时候把excludeSwitches写成add_argument,脚本不报错,但完全没生效,排查了半天才发现是方法用错了。
如果你需要自定义驱动路径或者指定端口(比如远程调试、多浏览器隔离),用Service对象:
from selenium.webdriver.chrome.service import Service service = Service(executable_path="/path/to/chromedriver", port=9515) driver = webdriver.Chrome(service=service, options=opts)selenium 3.x里这些参数是直接平铺在webdriver.Chrome(executable_path=...)上的,4.x把它们收进了Service和Options,升级的时候老代码会大面积报错,这个迁移成本要有心理准备。
2.2 quit和close的区别,以及会话泄漏的真实代价
这两个方法我必须单独拎出来说,因为我见过太多人的脚本跑了一晚上,机器上堆了几百个僵尸浏览器进程。
driver.close():关闭当前焦点所在的窗口或标签页。如果这是最后一个窗口,浏览器进程通常会跟着退出,但这取决于驱动实现,不要依赖这个行为。driver.quit():结束整个会话,关闭所有窗口,杀掉浏览器进程,释放驱动端资源。
结论很简单:脚本结束前一律用quit()。养成try/finally的习惯:
driver = webdriver.Chrome(options=opts) try: driver.get("https://example.com") # 业务逻辑 finally: driver.quit()不写finally的代价是什么?脚本中途抛异常,quit不执行,浏览器进程留着,driver端会话也留着。跑批量任务时,几十个进程同时开着,内存直接吃满,后面新起的会话可能因为端口占用或者资源不足起不来。我用CI跑用例的时候踩过一次,500条用例跑到第80条开始大面积超时,查下来就是前面崩掉的会话没回收干净。
另外一个容易忽略的点:driver对象在quit之后就不能再用了,再调任何方法都会抛InvalidSessionIdException。别在finally之后还去读driver.title。
3. 页面导航和窗口控制,别只会一个get
3.1 get、back、forward、refresh的隐式等待陷阱
driver.get("https://example.com/list") driver.back() driver.forward() driver.refresh()这四个是导航四件套。get()的语义是等到页面load事件触发才算完成,注意这是load事件,不是"页面内容渲染完毕"。现在的前端页面大量用异步请求填数据,load事件触发的时候列表可能还是空的。所以很多人写完driver.get()下一行直接find_element,报找不到元素,就是这个原因。
back()和forward()走的是浏览器历史栈,理论上很稳,但有个经典坑:单页应用(SPA)里,历史栈的变化可能是由history.pushState造出来的,back()之后页面元素并不会像传统页面那样重新加载,你可能需要手动触发一次刷新或者等待某个组件重新挂载。
refresh()在表单提交后调用会弹"是否重新提交"的确认框,这个框不是alert,switch_to.alert抓不到它,会直接卡住。解决办法是在refresh之前先driver.get(driver.current_url)绕过去,或者用JS自己刷新。
提示:
get()后面永远别急着操作元素。要么显式等待,要么用一个稳定的标志性元素做同步点。这是selenium脚本稳定性的第一道防线。
3.2 窗口和标签页切换,handle的正确用法
点击一个链接开了新标签页,你的driver默认还停在老标签页上,这时候去找新页面的元素必然失败。标准流程是这样:
main_handle = driver.current_window_handle driver.find_element(By.LINK_TEXT, "查看详情").click() WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2)) for handle in driver.window_handles: if handle != main_handle: driver.switch_to.window(handle) break # 新页面操作完后切回 driver.close() driver.switch_to.window(main_handle)这里我强烈建议用EC.number_of_windows_to_be等待窗口数量变化,而不是time.sleep。sleep是在赌,等待是在等事实发生。
window_handles返回的是一个列表,顺序不保证和视觉上的标签页顺序一致。所以不要用window_handles[0]、window_handles[1]这种下标硬取,一定要用set差集的方式找出新增的那个。我以前图省事用下标,本地跑得好好的,到了无头模式下顺序变了,直接翻车。
3.3 窗口尺寸、位置与截图
driver.maximize_window() driver.set_window_size(1440, 900) driver.set_window_position(0, 0) print(driver.get_window_size()) # {'width': 1440, 'height': 900} driver.save_screenshot("full.png") # 保存到文件 png_bytes = driver.get_screenshot_as_png() # 拿到bytes无头模式下maximize_window()经常拿不到你期望的尺寸,因为无头浏览器没有真实的屏幕。稳妥做法是初始化的时候直接给--window-size参数,或者在启动后显式set_window_size。
save_screenshot截的是当前视口,不是整页。要截整页得用execute_script滚到底再截,或者借助CDP相关能力。做失败用例留证的时候,视口截图通常够用,但如果页面很长、报错的元素在底部,视口截图就会拍到一片空白,这时候要么先scrollIntoView把元素滚到视野内再截,要么老老实实做整页拼接。
4. 元素定位方法家族,selenium的核心战场
4.1 find_element和find_elements,一个字母的差别
elem = driver.find_element(By.ID, "submit") elems = driver.find_elements(By.CLASS_NAME, "item")区别很关键:
find_element返回单个元素,找不到直接抛NoSuchElementException。find_elements返回列表,找不到返回空列表,不抛异常。
这个差异直接决定了判断元素是否存在该用哪个:
def is_element_exist(driver, by, value, timeout=5): try: WebDriverWait(driver, timeout).until( EC.presence_of_element_located((by, value)) ) return True except TimeoutException: return False当然也可以用find_elements加长度判断,写法更短但会有一次完整的超时等待,在循环里频繁调用会很慢。我个人倾向用显式等待版本,语义更清晰。
注意:
find_element在找不到时抛异常,这个异常如果没有被捕获,会直接中断脚本。做数据采集的时候,对可选字段一律用find_elements加try包裹,保证单个字段缺失不会拖垮整个任务。
4.2 By的八种定位方式,以及优先级怎么排
| 定位方式 | 写法 | 适用场景 | 稳定性 |
|---|---|---|---|
| ID | By.ID | 元素有唯一id | 最高 |
| NAME | By.NAME | 表单元素 | 高 |
| CSS_SELECTOR | By.CSS_SELECTOR | 绝大多数场景 | 高 |
| XPATH | By.XPATH | 需要向上或按文本定位 | 中高 |
| CLASS_NAME | By.CLASS_NAME | 类名简单唯一 | 中 |
| TAG_NAME | By.TAG_NAME | 批量取标签 | 低 |
| LINK_TEXT | By.LINK_TEXT | 完整链接文本 | 中 |
| PARTIAL_LINK_TEXT | By.PARTIAL_LINK_TEXT | 部分链接文本 | 中 |
我的选择顺序是:ID > NAME > CSS > XPATH。CSS选择器性能比XPATH好,语法也更简洁,能用CSS就别用XPATH。但有两种情况必须上XPATH:一是要按元素文本定位,二是要往父级/祖先级找元素,CSS在这两点上无能为力。
# 按文本定位,XPATH独有 driver.find_element(By.XPATH, "//button[normalize-space(text())='确认提交']") # 从子元素往父元素找 driver.find_element(By.XPATH, "//span[text()='订单号']/ancestor::div[1]")XPATH写文本匹配时,text()外面套一层normalize-space()很实用,能自动去掉首尾空白和换行。前端模板渲染出来的按钮文字经常带缩进和换行,不处理的话text()='确认'匹配不上,你会以为定位写错了,其实是空格在捣乱。
还有一个细节:By.CLASS_NAME一次只能传一个类名,多个类名要用CSS写。比如class="btn btn-primary active",By.CLASS_NAME, "btn btn-primary"是错的,得写By.CSS_SELECTOR, ".btn.btn-primary"。这个错误非常常见。
4.3 非原生下拉框的定位与操作,div+ul+li怎么破
原生下拉框的HTML是<select><option>,selenium提供了专门的Select类:
from selenium.webdriver.support.ui import Select sel = Select(driver.find_element(By.ID, "city")) sel.select_by_index(2) sel.select_by_value("310000") sel.select_by_visible_text("上海") print(sel.first_selected_option.text)这套用起来很舒服。但现实是,现在的前端框架基本都自己实现了下拉组件,实际DOM是<div class="select"><ul><li>这种组合,Select类一用就报UnexpectedTagNameException,提示你期望的是select标签但拿到的是div。
这种非原生下拉框的正确打开方式分三步:
# 第一步,点开下拉面板 trigger = driver.find_element(By.CSS_SELECTOR, "div.select-trigger") driver.execute_script("arguments[0].scrollIntoView({block:'center'});", trigger) trigger.click() # 第二步,等选项列表可见,再按文本选中 target = WebDriverWait(driver, 10).until( EC.element_to_be_clickable( (By.XPATH, "//ul[@class='select-list']//li[normalize-space(.)='上海']") ) ) target.click() # 第三步,断言选中结果写回了输入框 value = driver.find_element(By.CSS_SELECTOR, "div.select-trigger").text assert "上海" in value几个实战要点:
- 点开面板前先
scrollIntoView,很多组件在视口外点击无效或者被固定头部遮挡。 - 选项列表往往是动态渲染的,点击触发之后才插入DOM,所以第二步必须等,不能紧接着就找li。
- 有些组件点击li之后面板不立即消失,导致下一次操作被浮层挡住。这种情况可以按一下ESC,或者等面板的
display:none生效再继续。 - 如果组件对真实鼠标事件敏感(有些会监听
mousedown而不是click),普通click不生效,就上ActionChains:
from selenium.webdriver.common.action_chains import ActionChains ActionChains(driver).move_to_element(trigger).click().perform()我遇到过最刁钻的一个组件,用JS直接触发click死活不行,最后发现它监听的是mousedown+mouseup的组合事件,用ActionChains的click_and_hold().release().perform()才搞定。所以当点击不生效时,别死磕,换事件模拟方式试试。
4.4 相对定位器,元素结构复杂时的救命稻草
selenium 4引入了相对定位器,可以基于已知元素找它附近的目标:
from selenium.webdriver.support.relative_locator import locate_with label = driver.find_element(By.XPATH, "//label[text()='手机号']") input_box = driver.find_element( locate_with(By.TAG_NAME, "input").to_right_of(label) )支持above、below、to_left_of、to_right_of、near五种关系。表格类页面里,用表头定位到对应列的数据特别顺手。不过要注意,这个是基于元素渲染后的坐标计算的,元素必须已经可见,否则位置算不出来。
5. 元素操作与交互,点击不生效的N种原因
5.1 click、send_keys、clear的基本盘
elem.click() elem.send_keys("hello") elem.clear() elem.send_keys("world") elem.submit() # 只在form内的元素上可用,等价于回车提交send_keys可以直接传组合键:
from selenium.webdriver.common.keys import Keys elem.send_keys(Keys.CONTROL, "a") elem.send_keys(Keys.BACKSPACE) elem.send_keys("新内容")clear()在富文本编辑器(contenteditable的div)上无效,因为它不是input或者textarea。对这种情况,通用做法是全选后删除:
elem.send_keys(Keys.CONTROL, "a") elem.send_keys(Keys.DELETE)click不生效是最常见的问题,按我踩坑的经验,原因大致这么几类:元素还没渲染出来(等)、元素被浮层遮挡(关掉浮层或滚动)、元素在视口外(scrollIntoView)、元素被禁用(检查disabled属性)、点击落在了子元素上但事件绑在父元素(改点父元素或者用JS触发)。排查顺序建议就按这个来,从等待开始查,八成问题出在第一类。
5.2 ActionChains处理复杂交互
拖拽、悬停、右键、双击这些用普通click搞不定的,都交给ActionChains:
actions = ActionChains(driver) # 悬停展开二级菜单 actions.move_to_element(menu).perform() driver.find_element(By.LINK_TEXT, "子菜单项").click() # 拖拽 actions.drag_and_drop(source, target).perform() # 右键菜单 actions.context_click(elem).perform() # 带偏移量的点击,绕过遮挡 actions.move_to_element_with_offset(elem, 5, 5).click().perform()有个必须记住的点:ActionChains是链式累积的,所有动作写好之后必须调perform()才会真正执行。忘记perform是新手高频错误,脚本不报错但什么也没发生。
move_to_element_with_offset的偏移量是相对于元素左上角的,不是页面坐标,这个容易搞反。另外selenium 4.2之后新增了滚轮方法:
actions.scroll_by_amount(0, 500).perform() actions.scroll_to_element(elem).perform()比用JS滚屏更贴近真实用户行为,某些对滚动事件有监听的前端组件只认这种。
5.3 execute_script,最后的万能钥匙
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") driver.execute_script("arguments[0].click();", elem) driver.execute_script("arguments[0].setAttribute('value', 'abc');", elem) result = driver.execute_script("return document.title;")execute_script的返回值规则:脚本里写了return,返回的就是那个值;没写return,返回None。参数通过arguments数组传入,索引从0开始。
什么时候必须用它?我总结三个场景:一是原生click被拦截,用JS直接触发DOM的click方法;二是需要修改元素属性(有些框架的只读输入框,send_keys会被拦,改value属性反而能写进去);三是需要拿页面全局变量或者调用页面上的函数。
但别把JS当第一选择。JS触发的click不走真实的鼠标事件链,有些前端会校验event.isTrusted,发现不是真实用户操作就直接忽略。所以能用原生click就优先用原生,JS是兜底手段。
6. 等待机制,selenium脚本稳定性的命门
6.1 三种等待的差异和混用陷阱
- 强制等待:
time.sleep(2)。无脑,但永远不推荐作为主力,它要么太短要么太长。 - 隐式等待:
driver.implicitly_wait(10)。全局生效,作用于每一次find_element,找不到就轮询到超时。 - 显式等待:
WebDriverWait配合expected_conditions,针对具体条件等待。
最关键的一条经验:隐式等待和显式等待不要混用。两者叠加时,实际等待时间可能变成两者之和,甚至出现隐式等待把显式等待的超时判断搞乱的情况。selenium官方文档也明确不建议混用。
我个人的配置是全局不开隐式等待,所有同步点都用显式等待写清楚。这样每个等待点等的是什么条件一目了然,出问题也好定位。
6.2 expected_conditions常用条件速查
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10, poll_frequency=0.5, ignored_exceptions=[ElementNotInteractableException]) wait.until(EC.presence_of_element_located((By.ID, "list"))) wait.until(EC.visibility_of_element_located((By.ID, "list"))) wait.until(EC.element_to_be_clickable((By.ID, "submit"))) wait.until(EC.invisibility_of_element_located((By.CLASS_NAME, "loading"))) wait.until(EC.text_to_be_present_in_element((By.ID, "status"), "完成")) wait.until(EC.url_contains("/dashboard")) wait.until(EC.alert_is_present())presence和visibility的区别一定要分清:元素在DOM里但display:none,presence通过,visibility不通过。很多"找不到元素"其实是找到了但不可见,用错条件的后果就是明明元素存在却一直超时。
ignored_exceptions参数值得多用。默认情况下,等待过程中抛出的异常(除了TimeoutException)会直接冒泡出去中断等待。比如元素出现了但在动画中途不可点击,会抛ElementNotInteractableException,加上这个参数它就会继续轮询而不是直接炸。
自定义条件也不难,接受driver返回布尔值即可:
def page_loaded(driver): return driver.execute_script("return document.readyState") == "complete" WebDriverWait(driver, 15).until(page_loaded)判断加载完成这件事,比起等某个具体元素,document.readyState在整页导航场景下更通用。但SPA里页面不重新加载,这个值一直是complete,就失效了,还是得回到等具体元素。
7. 状态读取与信息获取,断言和调试全靠这批方法
7.1 元素信息类方法的正确取用
elem.text # 渲染后的可见文本 elem.get_attribute("href") # HTML属性原始值 elem.get_property("value") # JS属性当前值 elem.value_of_css_property("color") elem.tag_name elem.size # {'width':.., 'height':..} elem.location # {'x':.., 'y':..}text和get_attribute的差别要讲清楚。text取的是渲染后用户能看到的文本,会做空白折叠,隐藏元素返回空字符串。get_attribute("textContent")取的是DOM里的原始文本,不受CSS可见性影响。我断言列表内容时用前者,校验隐藏字段的值时用后者。
还有一个高频困惑:input输入框的内容用text取不到,返回空。因为input的值不在文本节点里,而在value属性上。正确写法是:
value = driver.find_element(By.ID, "username").get_attribute("value") # 或者用selenium 4的属性读取 value = driver.find_element(By.ID, "username").get_property("value")用户输入进去的值,get_attribute("value")能拿到,但要注意它读的是HTML attribute还是DOM property。现代框架里,用户输入只改DOM property,不改HTML attribute,这时候必须用get_property。这个坑我在测React应用时踩过,排查思路是先看页面源码,源码里的value还是初始值,就说明得用get_property。
7.2 元素状态判断方法
elem.is_displayed() # 是否可见 elem.is_enabled() # 是否可用,对应disabled属性 elem.is_selected() # 复选框、单选框是否选中is_displayed的实现逻辑是判断元素及其祖先链上有没有display:none、visibility:hidden、尺寸为0这些情况。但它不能判断元素是否被其他元素遮挡,这也就是为什么is_displayed()返回True,click还是可能失败。要判断遮挡,得靠elementFromPoint那套JS写法,或者直接点一次试试。
7.3 cookie、日志和页面级信息
driver.get_cookies() driver.add_cookie({"name": "token", "value": "abc", "domain": "example.com"}) driver.delete_all_cookies() print(driver.current_url) print(driver.title) print(driver.name) # 浏览器名,如 chrome print(driver.capabilities) # 会话能力描述,排查环境问题很有用 print(driver.page_source) # 当前页面源码add_cookie有个必须注意的点:必须先访问目标域名下的任意页面,才能往这个域写入cookie。直接开一个新driver就add_cookie会报InvalidCookieDomainException,因为此时还没有确定域。标准写法是先driver.get(url)再加cookie再refresh。
page_source在排查定位失败时非常有用。把源码dump到本地,用浏览器的开发者工具打开,你会发现有些元素在selenium眼里和在浏览器里长得很不一样——比如iframe里的内容、shadow DOM里的内容,page_source里可能就看不到完整结构。这时候要意识到问题出在iframe或者shadow root上,而不是定位表达式写错了。
driver.capabilities我经常用来做环境自检,脚本启动后打一行日志出来,出问题的时候能直接看出浏览器版本、驱动版本、平台信息,省去很多来回问。
8. 排错实录,那几个每年都要遇到几次的报错
8.1 无法创建会话类报错
unable to obtain driver for chrome这个提示在selenium 4.6之前特别常见,根因是找不到和本机浏览器版本匹配的驱动。处理路径有这么几条:升级selenium到4.6以上让Selenium Manager自动处理;或者手动下载对应版本驱动并放进PATH;或者显式指定Service(executable_path=...)。
如果本机浏览器是企业统一推送的、你没法升级,那就必须走手动指定驱动这条路。这时候版本对照表要收藏好,大版本号必须对上,小版本一般兼容,但跨大版本一定不行。
还有一种情况是脚本在容器里跑,容器里压根没装浏览器。这种要么装个无头浏览器进镜像,要么用远程方式连到外部浏览器节点。远程连接的写法:
driver = webdriver.Remote( command_executor="http://192.168.1.100:4444", options=opts )8.2 元素找不到、过期、不可交互
这三类是selenium日常报错的主力,处理思路可以用一张表来对照:
| 报错 | 典型原因 | 处理方向 |
|---|---|---|
| NoSuchElementException | 元素未渲染、在iframe内、定位表达式错 | 加显式等待、切iframe、用page_source核对 |
| StaleElementReferenceException | 元素被重新渲染,引用失效 | 重新定位元素,或把操作封装成重试 |
| ElementNotInteractableException | 元素不可见或被遮挡 | 滚动到可视区、关闭浮层、改用JS点击 |
| ElementClickInterceptedException | 点击被其他元素拦截 | 等待浮层消失、scrollIntoView、ActionChains |
| TimeoutException | 条件始终不满足 | 检查条件本身是否合理,别盲目加时间 |
| InvalidSelectorException | 选择器语法错误 | 检查XPATH括号引号、CSS转义 |
StaleElementReferenceException值得多说两句。它的本质是:你手里拿到的元素引用指向的是一个已经被销毁的DOM节点。页面上任何一次局部刷新、列表重排、Vue/React的重新渲染,都可能让之前的引用失效。解决办法不是"多试几次"那么简单,而是把定位和操作放在同一个短周期内,中间不要插入会触发页面变化的其他操作。如果确实需要重试,重试的时候必须重新执行find_element,拿新引用,而不是拿旧对象再点一次。
def click_with_retry(driver, by, value, retries=3): for _ in range(retries): try: elem = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((by, value)) ) elem.click() return except StaleElementReferenceException: continue raise RuntimeError(f"点击失败: {by}={value}")8.3 alert、iframe、多窗口这三座大山
alert是浏览器的原生弹窗,不是页面元素,find_element永远找不到它,必须用:
alert = driver.switch_to.alert print(alert.text) alert.accept() # 确定 alert.dismiss() # 取消要注意alert是阻塞式的,弹出的时候页面所有操作都会卡住。有时候alert出现得很慢,脚本先跑过去了,后面操作全超时,还不报alert相关的错,非常误导人。遇到莫名其妙的集体超时,先去检查有没有alert没处理。
iframe的处理原则是"进去要切,出来也要切":
driver.switch_to.frame("frame_id") # 或者 frame = driver.find_element(By.CSS_SELECTOR, "iframe.pay") driver.switch_to.frame(frame) # 操作完后 driver.switch_to.default_content()嵌套iframe要一层层切进去,切回的时候用default_content()回主文档,或者parent_frame()回上一层。忘了切回主文档是最常见的低级错误,表现为"在主页面找元素全都找不到",其实driver的上下文还停在iframe里。
9. 封装层面的几个心得,以及我踩过的那些坑
9.1 把定位和操作分离,是降低维护成本的关键
我早期的脚本长这样:满屏的driver.find_element(By.XPATH, "...").click(),一个定位表达式在多个用例里复制粘贴。后来页面改版,一个类名变了,我得翻遍所有文件去改,改漏一处就跑失败。
现在的做法是把定位集中到一个页面对象文件里,用字典或者类属性统一管理:
class LoginPage: USERNAME = (By.ID, "username") PASSWORD = (By.ID, "password") SUBMIT = (By.CSS_SELECTOR, "button.login-btn") def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def login(self, user, pwd): self.wait.until(EC.visibility_of_element_located(self.USERNAME)).send_keys(user) self.driver.find_element(*self.PASSWORD).send_keys(pwd) self.wait.until(EC.element_to_be_clickable(self.SUBMIT)).click()这里*self.USERNAME的解包写法要习惯,因为find_element要求By和值作为两个参数传,而我们把它们放在了一个元组里。写的时候如果忘了星号,会报参数数量错误。
9.2 定位表达式尽量用稳定特征,别依赖样式类
前端重构最常动的就是样式类名。class="btn_click_2024_v2"这种带版本号或者随机后缀的类名,基本半年就变一次。相对稳定的特征是:id、name、>