Selenium自动化登录实战:控件定位、验证码与登录态复用
2026/9/12 20:36:40 网站建设 项目流程

做UI自动化的人,多半都是从“自动化登录”这一步开始的。不管你用Selenium是要做爬虫、做回归测试、还是做定时任务,登录永远是第一个绕不开的关口。但真正上手之后你会发现,难点从来不是“把账号密码填进去、点一下登录按钮”,而是登录前后的这一堆破事:验证码怎么处理、下拉框是div拼的怎么选、文件上传怎么搞、下载文件怎么等它完事、登录态怎么保住。

这篇文章我把这些年用Selenium做自动化登录踩过的坑、沉淀下来的套路,用实例一点点拆开讲。不管是刚装好Selenium还不会写第一行脚本的新手,还是已经写过不少用例但老被复杂控件卡住的老手,都能在这里面找到直接能拿走的东西。

1. 自动化登录的整体思路与前置准备

1.1 自动化登录到底在自动化什么

如果你只把“自动化登录”理解成自动填账号密码,那就低估它了。一个完整的自动化登录链路,至少包含下面几块:

  • 打开目标页面,可能还要先走一遍前置跳转。
  • 定位账号输入框、密码输入框、登录按钮,甚至还有“记住我”这种勾选框。
  • 处理登录前的滑块、点选文字、短信验证码等各类验证。
  • 登录后可能触发弹窗、新手引导、文件上传、下载更新包。
  • 校验是否登录成功,比如判断页面跳转URL、某个元素是否出现。
  • 把登录态保存下来,供后续步骤复用。

也就是说,自动化登录是前半场,后半场是登录成功之后那些操作能不能顺畅跑下去。很多人的脚本卡住,不是卡在登录本身,而是卡在了“登录完之后那个上传控件”“登录完之后那个非原生下拉框”上。这也是我把这些都塞进一篇文章里的原因。

1.2 环境准备与浏览器驱动注意事项

Selenium本身只是一个自动化协议库,它要控制浏览器,需要一个中间层——浏览器驱动。Python环境下标准安装配置是:

pip install selenium

然后去下载对应浏览器的驱动。比如Chrome浏览器用ChromeDriver,Firefox用GeckoDriver,Edge用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(service=service)

这样每次跑脚本前都会自动检测本地浏览器版本,匹配对应的驱动,省掉很多环境层面的烦恼。

还有一个容易被忽略的设置是浏览器启动参数。做自动化登录的时候,如果不想每次弹出一个浏览器窗口,可以加“无头模式”:

from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--headless=new") options.add_argument("--window-size=1920,1080") options.add_argument("--disable-blink-features=AutomationControlled") driver = webdriver.Chrome(service=service, options=options)

注意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[type='submit']") 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[name='username']、input[placeholder*='手机号']这种写法,比一段又臭又长、依赖页面层级顺序的XPath可靠得多。

2.2 非原生下拉框的定位:div+ul+li组合

现在前端框架越来越多,很多页面上的“下拉框”根本不是select标签,而是用div、ul、li自己拼出来的。你用Selenium默认的Select类去选,会发现根本切不过去,因为Select类只认原生select元素。

遇到这种div+ul+li组合的下拉框,思路要转变:它本质上不是一个“选择控件”,而是一串可点击的元素。操作逻辑拆开就是:

  1. 先点击触发展开下拉框的div。
  2. 等待ul列表项出现。
  3. 从li列表里找到目标文本,点击它。

举个例子,页面上有个“用户类型”,标签是div,点开之后是一组li:

from 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 type="file">,这种最简单,直接用send_keys把本地文件路径填进去,不用真正去点系统文件选择框:

file_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']") file_input.send_keys("/path/to/avatar.jpg")

但很多现代前端框架会把这个input元素隐藏掉,显示层是一块自定义区域。这时候你可以直接对隐藏的input执行send_keys,因为Selenium操作的是DOM,不要求元素可见:

hidden_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']") driver.execute_script("arguments[0].style.display = 'block';", hidden_input) hidden_input.send_keys("/path/to/file.zip")

如果页面用的是拖拽上传的组件,没有input[type='file'],那处理起来就麻烦一些。一个常用的思路是用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[type='file']"); 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甚至不知道文件下到哪儿了。

标准做法是:

  1. 配置浏览器下载目录,固定下载路径。
  2. 点击下载按钮。
  3. 轮询检查目录下是否有文件出现,且文件大小在持续增长。
  4. 等文件大小连续几次不变,或者扩展名从.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, timeout=60): 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, key=lambda 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", encoding="utf-8") as f: json.dump(cookies, f, ensure_ascii=False, indent=2)

恢复的时候注意一个关键点:必须先打开目标域名下的任意一个页面,才能add_cookie。因为cookie是绑定域名的,Selenium不允许在空页面状态下给某个域名添加cookie。

driver.get("https://example.com") with open("cookies.json", "r", encoding="utf-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(service=service, options=options)

这种方式相当于把整个浏览器档案打包了,书签、历史、登录态、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[type='text']"); """, 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_contains:URL包含指定内容,适合判断页面跳转。
  • 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状态、网络请求分析一遍,再动手改代码,效率反而高很多。希望这篇文章能帮你在自动化登录这条路上少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询