
做UI自动化的人多半都是从“自动化登录”这一步开始的。不管你用Selenium是要做爬虫、做回归测试、还是做定时任务登录永远是第一个绕不开的关口。但真正上手之后你会发现难点从来不是“把账号密码填进去、点一下登录按钮”而是登录前后的这一堆破事验证码怎么处理、下拉框是div拼的怎么选、文件上传怎么搞、下载文件怎么等它完事、登录态怎么保住。这篇文章我把这些年用Selenium做自动化登录踩过的坑、沉淀下来的套路用实例一点点拆开讲。不管是刚装好Selenium还不会写第一行脚本的新手还是已经写过不少用例但老被复杂控件卡住的老手都能在这里面找到直接能拿走的东西。1. 自动化登录的整体思路与前置准备1.1 自动化登录到底在自动化什么如果你只把“自动化登录”理解成自动填账号密码那就低估它了。一个完整的自动化登录链路至少包含下面几块打开目标页面可能还要先走一遍前置跳转。定位账号输入框、密码输入框、登录按钮甚至还有“记住我”这种勾选框。处理登录前的滑块、点选文字、短信验证码等各类验证。登录后可能触发弹窗、新手引导、文件上传、下载更新包。校验是否登录成功比如判断页面跳转URL、某个元素是否出现。把登录态保存下来供后续步骤复用。也就是说自动化登录是前半场后半场是登录成功之后那些操作能不能顺畅跑下去。很多人的脚本卡住不是卡在登录本身而是卡在了“登录完之后那个上传控件”“登录完之后那个非原生下拉框”上。这也是我把这些都塞进一篇文章里的原因。1.2 环境准备与浏览器驱动注意事项Selenium本身只是一个自动化协议库它要控制浏览器需要一个中间层——浏览器驱动。Python环境下标准安装配置是pip install selenium然后去下载对应浏览器的驱动。比如Chrome浏览器用ChromeDriverFirefox用GeckoDriverEdge用EdgeDriver。这里有个很关键的坑驱动版本必须和浏览器版本大致匹配否则启动浏览器的时候会直接报SessionNotCreatedException提示版本不兼容。好一点的实践是不要手动下载驱动用工具自动管理pip install webdriver-manager代码里这样用from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice)这样每次跑脚本前都会自动检测本地浏览器版本匹配对应的驱动省掉很多环境层面的烦恼。还有一个容易被忽略的设置是浏览器启动参数。做自动化登录的时候如果不想每次弹出一个浏览器窗口可以加“无头模式”from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) options.add_argument(--window-size1920,1080) options.add_argument(--disable-blink-featuresAutomationControlled) driver webdriver.Chrome(serviceservice, optionsoptions)注意headless模式下有些页面的元素渲染行为和肉眼看到的正常浏览器不一样尤其是那些“下拉框选项悬浮层”之类的东西。所以在调试阶段我一般先不开无头模式等脚本稳定了再切换无头跑批量任务。2. 表单填充与复杂控件定位实战2.1 最经典的账号密码输入与登录按钮点击先看一段最朴素的登录代码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 driver.get(https://example.com/login) # 用显式等待确保元素出现后再操作 username_input WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, username)) ) username_input.send_keys(my_account) password_input driver.find_element(By.NAME, password) password_input.send_keys(my_password) login_button driver.find_element(By.CSS_SELECTOR, button[typesubmit]) login_button.click()这段代码看着简单但里面有两个习惯很值得养成。第一等待元素出现再操作。不要用time.sleep(3)这种固定等待因为网络快慢、页面渲染速度都会有波动固定的sleep要么太慢要么不够。用WebDriverWait配合expected_conditions是更稳的做法。比如等待可点击状态用element_to_be_clickable等待元素消失用invisibility_of_element_located这些都是实战中经常会用到的。第二能用CSS Selector尽量用。Selenium里定位元素的方式有ID、Name、Class Name、XPath、CSS Selector等。ID和Name是最高效的但很多前端框架生成的ID是动态的每次刷新都不一样这时候用CSS Selector的层级关系或属性匹配会更稳。比如input[nameusername]、input[placeholder*手机号]这种写法比一段又臭又长、依赖页面层级顺序的XPath可靠得多。2.2 非原生下拉框的定位divulli组合现在前端框架越来越多很多页面上的“下拉框”根本不是select标签而是用div、ul、li自己拼出来的。你用Selenium默认的Select类去选会发现根本切不过去因为Select类只认原生select元素。遇到这种divulli组合的下拉框思路要转变它本质上不是一个“选择控件”而是一串可点击的元素。操作逻辑拆开就是先点击触发展开下拉框的div。等待ul列表项出现。从li列表里找到目标文本点击它。举个例子页面上有个“用户类型”标签是div点开之后是一组lifrom selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 点击展开下拉框 trigger driver.find_element(By.CSS_SELECTOR, div.select-trigger) trigger.click() # 等待下拉选项渲染完成 options WebDriverWait(driver, 10).until( EC.visibility_of_all_elements_located((By.CSS_SELECTOR, ul.select-dropdown li)) ) for option in options: if option.text.strip() 企业用户: option.click() break这里有几个细节要特别留意点击展开后下拉选项是异步加载的所以不能立刻去拿li列表要等visibility_of_all_elements_located。li里面的文本可能带空格、换行所以判断的时候要strip()一下不然字面值对不上。有的li文本前面还有图标、小标签实际展示文本和text属性不一致这时可以用contains匹配企业用户 in option.text。如果点击li之后弹出的是“多级联动”或“可搜索的下拉框”那大概率要处理键盘事件先send_keys输入关键词再等过滤后的li出现再点击。还有一个坑是下拉框选项打开后是absolute定位的悬浮层有时候菜单被其他元素遮挡直接click会报ElementClickInterceptedException。这时候可以先用JavaScript强制点击或者先把遮挡元素隐藏掉。如果click被拦截我用得最多的兜底方案是JavaScript点击driver.execute_script(arguments[0].click();, option)但注意这只是“把点击事件触发出来”如果要验证完整用户行为还是优先模拟真实点击。2.3 更隐蔽的控件iframe内外切换做自动化登录时还有一类控件藏着让人防不胜防——iframe。有些系统的登录框整体嵌在一个iframe里外围页面只是壳。你直接find_element找输入框怎么都找不到控制台里却明明能看到。这时候要切进去from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待iframe可切换 iframe WebDriverWait(driver, 10).until( EC.frame_to_be_available_and_switch_to_it((By.CSS_SELECTOR, iframe.login-frame)) ) # 现在可以正常定位登录框内部元素了 driver.find_element(By.ID, username).send_keys(my_account) driver.find_element(By.ID, password).send_keys(my_password) driver.find_element(By.CSS_SELECTOR, button.login-btn).click() # 登录完成记得切回默认内容 driver.switch_to.default_content()iframe操作的几个常见坑如果页面里嵌套了多层iframe要一层一层切进去switch_to.frame(第一层)→ 再往里switch_to.frame(第二层)。切到iframe内部之后原先外层页面的元素就访问不到了需要先switch_to.default_content()切回顶层。有些iframe是动态创建的比如登录页加载后3秒才插入iframe所以要用frame_to_be_available_and_switch_to_it等待而不是直接找。3. 登录链路中的文件上传与下载处理3.1 绕过文件上传框的三种思路很多系统登录成功之后第一件事就是上传头像、上传材料。文件上传控件也是Selenium里的一块硬骨头。最常见的上传控件是input typefile这种最简单直接用send_keys把本地文件路径填进去不用真正去点系统文件选择框file_input driver.find_element(By.CSS_SELECTOR, input[typefile]) file_input.send_keys(/path/to/avatar.jpg)但很多现代前端框架会把这个input元素隐藏掉显示层是一块自定义区域。这时候你可以直接对隐藏的input执行send_keys因为Selenium操作的是DOM不要求元素可见hidden_input driver.find_element(By.CSS_SELECTOR, input[typefile]) driver.execute_script(arguments[0].style.display block;, hidden_input) hidden_input.send_keys(/path/to/file.zip)如果页面用的是拖拽上传的组件没有input[typefile]那处理起来就麻烦一些。一个常用的思路是用JavaScript构造DataTransfer对象模拟拖拽事件js_script var file new File([], test.txt, {type: text/plain}); var dataTransfer new DataTransfer(); dataTransfer.items.add(file); var input document.querySelector(input[typefile]); input.files dataTransfer.files; input.dispatchEvent(new Event(change, {bubbles: true})); driver.execute_script(js_script)这种方式不是万能的有些前端框架还会校验文件的size、lastModified属性甚至走分片上传逻辑模拟起来很费劲。遇到那种崇山峻岭级别的上传组件我的建议是评估一下能不能通过接口直接上传文件然后刷新页面让前端识别能不能用AutoIt、pywinauto这类工具直接操作系统级文件选择窗口能不能在测试环境里把这个上传步骤跳过自动化脚本的目标是完成业务闭环不是所有交互都得靠Selenium硬刚。上传文件如果Selenium侧成本太高通过request库调上传接口再用Selenium刷新页面验证结果是性价比很高的方案。3.2 文件下载完成后如何再执行下一步“Selenium怎样使文件下载完成之后才进行下一步”这个问题被问烂了因为Selenium本身没有提供“等待下载完成”的API。下载是浏览器的行为Selenium甚至不知道文件下到哪儿了。标准做法是配置浏览器下载目录固定下载路径。点击下载按钮。轮询检查目录下是否有文件出现且文件大小在持续增长。等文件大小连续几次不变或者扩展名从.crdownload变成正式扩展名说明下载完成。Chrome配置固定下载目录的写法import os download_dir os.path.abspath(./downloads) if not os.path.exists(download_dir): os.makedirs(download_dir) options Options() options.add_experimental_option(prefs, { download.default_directory: download_dir, download.prompt_for_download: False, download.directory_upgrade: True, safebrowsing.enabled: True, })点击下载之后用轮询去判断import time import os from pathlib import Path def wait_for_download_complete(download_dir, timeout60): deadline time.time() timeout last_size -1 stable_count 0 while time.time() deadline: files [f for f in Path(download_dir).iterdir() if f.is_file()] if files: latest max(files, keylambda f: f.stat().st_mtime) size latest.stat().st_size if size 0 and size last_size: stable_count 1 if stable_count 3: return latest else: stable_count 0 last_size size time.sleep(1) raise TimeoutError(download timeout)这里面的逻辑是文件刚创建的时候大小是0然后增长增长到一定程度停下来。连续三次检测大小都没变化基本可以认为下载完成。如果文件临时名是.crdownload判断的时候要把这种临时文件排除掉或者检测临时文件不存在了、正式文件存在了才算完成。如果是多个文件并发下载判断逻辑要更复杂一些但核心还是围绕“文件大小稳定”来做。4. 登录状态的维持与复用cookie与本地会话4.1 为什么要保存登录态自动化登录本身不慢慢的是登录前的验证码、滑块、短信。如果你每一次跑脚本都要重新走一遍完整登录流程时间成本会让人崩溃。所以在日常实操里我会把登录态保存下来后续脚本直接加载登录态跳过验证环节。保存登录态有两种常见方式保存cookie下次用add_cookie恢复。直接复用浏览器的用户数据目录让登录信息留在浏览器里。4.2 保存cookie并恢复登录保存cookie的思路是正常登录成功后拿到所有cookie序列化成文件import json cookies driver.get_cookies() with open(cookies.json, w, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse, indent2)恢复的时候注意一个关键点必须先打开目标域名下的任意一个页面才能add_cookie。因为cookie是绑定域名的Selenium不允许在空页面状态下给某个域名添加cookie。driver.get(https://example.com) with open(cookies.json, r, encodingutf-8) as f: cookies json.load(f) for cookie in cookies: # 有些cookie带sameSite等字段添加时可能会有异常可去掉后重试 try: driver.add_cookie(cookie) except Exception: # 清理可能导致冲突的字段 safe_cookie {k: v for k, v in cookie.items() if k not in (sameSite, storeId)} driver.add_cookie(safe_cookie) driver.refresh()这里我踩过不少坑cookie里的expiry过期时间如果转储时是字符串add_cookie会报类型错误需要转成int。有些cookie是HttpOnly的但读取get_cookies()的时候仍然能拿到只是JS侧访问不到不影响Selenium恢复。如果登录后的页面在浏览器里出现“请重新登录”的提示大概率是有些本地存储localStorage、sessionStorage里的token没被保存。Selenium没有直接获取localStorage的API但可以用execute_script去读local_storage driver.execute_script(return window.localStorage;)把localStorage也dump下来恢复的时候再写回去很多“登录态丢失”的问题都能解决。直接复用用户数据目录是另一种方式适合那种登录态长期有效、又不想维护cookie解析逻辑的场景options Options() options.add_argument(r--user-data-dir/path/to/chrome-profile) driver webdriver.Chrome(serviceservice, optionsoptions)这种方式相当于把整个浏览器档案打包了书签、历史、登录态、localStorage全在里面。缺点是同一时间只能有一个Chrome进程使用这个目录否则会锁冲突。如果要用多账号并行就得建多个不同的user-data-dir。5. 常见问题与排查技巧实录5.1 element not interactable这是自动化登录里出现频率最高的报错之一。字面意思是“元素不可交互”但背后原因五花八门元素其实是被遮挡了比如一个透明的遮罩层盖在上面。元素虽然存在但是不可见比如display:none、visibility:hidden。元素在iframe里你还没切进去。页面正在加载中元素刚被创建但还没完成布局。排查思路是先把元素信息打出来看看element driver.find_element(By.ID, username) print(element.is_displayed()) print(element.is_enabled()) print(element.location_once_scrolled_into_view)如果is_displayed是False说明元素确实不可见。这时候看是不是需要先触发某个事件让它显示出来或者用execute_script滚动到可视区域再操作driver.execute_script(arguments[0].scrollIntoView({block:center});, element)如果元素本身被遮罩层挡住可以先把这个遮罩层隐藏掉overlay driver.find_element(By.CSS_SELECTOR, div.loading-mask) driver.execute_script(arguments[0].style.display none;, overlay)5.2 定位不到元素但浏览器里明明有另一个高频问题打开浏览器手动操作元素就在那里但是Selenium find_element就是找不到。原因通常有几种元素在iframe里需要先切换。元素在Shadow DOM里Selenium默认找不到。元素是动态渲染的还没来得及加载。页面可能有多个相同条件的元素但你用了find_element拿的却是第一个隐藏的那个。针对动态渲染把find_element改成WebDriverWait EC.presence_of_element_located或者visibility_of_element_located大部分情况都能解决。针对Shadow DOM需要用JavaScript穿透进去。举个实际例子shadow_host driver.find_element(By.CSS_SELECTOR, my-custom-widget) input_el driver.execute_script( return arguments[0].shadowRoot.querySelector(input[typetext]); , shadow_host) input_el.send_keys(hello)Shadow DOM的定位是比较偏门的东西但遇到现代Web Component组件不会这招还真过不去。5.3 验证码那只拦路虎怎么办验证码是自动化登录里最不想遇到、又最躲不掉的东西。我一般不指望Selenium能“智能识别”图片验证码因为验证码存在的意义就是阻止自动化。实际工程里我更推荐用下面几种思路测试环境直接关闭验证码或配置万能验证码。调用专门的打码平台API。用OCR库做简单识别比如tesseract但成功率有限。对于滑块验证码用OpenCV分析缺口位置、pyautogui做拖拽。这些方案各有适用条件。如果你只是自用脚本我建议先联系目标系统的管理员看看能不能在测试环境拿到一个验证码白名单入口这才是最省力的方案。硬闯验证码时间和成本都不划算。5.4 操作太快导致失败与显式等待Selenium跑得比人快但前端JS不一定跟得上。最常见的情况是登录按钮刚点下去页面还在发Ajax请求你就已经开始找登录后的元素了浏览器告诉你找不到。这里的关键不是“多等等”而是“等对条件”。推荐使用显式等待把“等什么”写得清清楚楚WebDriverWait(driver, 20).until( EC.presence_of_element_located((By.CSS_SELECTOR, .user-panel)) )显式等待的EC函数很多我常用的是这几个visibility_of_element_located元素可见且非空。element_to_be_clickable元素可见且可点击。presence_of_element_located元素存在于DOM中。invisibility_of_element_located元素不可见或不存在通常用来等Loading消失。url_containsURL包含指定内容适合判断页面跳转。text_to_be_present_in_element元素包含特定文本适合判断登录后的欢迎语。5.5 窗口句柄与多tab切换有些登录流程是在新窗口或新tab中完成的比如扫码登录、第三方授权登录。登录完成后Selenium还停留在旧页面这时候你得手动切换过去。# 获取所有窗口句柄 handles driver.window_handles # 切到最新打开的那个窗口 driver.switch_to.window(handles[-1])切回来也有对应的driver.close() driver.switch_to.window(handles[0])这里有个细节window_handles的顺序不是固定的第二次获取的顺序可能和第一次不一样。所以在多窗口场景下建议先用title或url判断一下当前窗口是不是自己想要的再决定是否切换。根据我个人经验自动化登录这个场景十次脚本跑挂有七次都是细节问题驱动版本不匹配、iframe没切换、等待条件不对、Cookie恢复顺序出错。这些坑单独看都很小但叠在一起就非常消耗耐心。遇到问题别急着硬调先把页面结构、DOM状态、网络请求分析一遍再动手改代码效率反而高很多。希望这篇文章能帮你在自动化登录这条路上少走点弯路。