ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python Selenium浏览器自动化实战:从环境搭建到高级定位技巧

Python Selenium浏览器自动化实战:从环境搭建到高级定位技巧 做Python开发这几年我算是和浏览器自动化打交道比较深的人。很多人一听到Selenium就以为它是爬虫专用工具其实它在自动化测试、数据采集、办公流程自动化里都能用甚至可以作为智能助手操作浏览器的底层能力应用面比想象中广得多。这篇文章就围绕Python Selenium自动化浏览器实战来写从最容易被卡住的环境搭建开始一直聊到定位复杂控件、枚举页面元素这些进阶玩法既照顾刚入门的新手也给已经在写脚本的同行提供一些能直接用的经验。整篇内容的核心关键词无非三个Python、Selenium、浏览器自动化但真正决定自动化脚本能不能跑得稳的往往是环境配置和细节处理。1. 环境搭建与浏览器驱动配置1.1 先装对Python你这台机器就成功了一半Selenium本身是Python的第三方库所以环境搭建的第一步是把Python基础环境准备好。我见过太多人在这一步被劝退问题多半出在安装时没有勾选Add Python to PATH。如果你在命令行敲python --version得到的是类似python was not found; run without arguments to install from the Microsoft Store的提示那说明你的Python要么没装要么装完没有被写进系统环境变量。这时候不要急着从Microsoft Store装建议直接去Python官网下载安装包安装过程中注意勾选Add Python to PATH然后重新开一个命令行窗口再验证一次。版本选择上我的个人建议是直接用Python 3.10或3.11这两个版本足够稳定第三方库的兼容性也好。Selenium库本身没有太多版本限制但新项目没必要选太老的Python。装好之后建议顺手把pip源换成国内源既能提升下载速度又能减少超时重试的次数。命令行执行pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple然后安装Seleniumpip install selenium装完之后验证一下版本pip show selenium能正常显示版本信息就说明Selenium已经装进当前Python环境了。如果你同时在做多个Python项目建议给当前项目单独建一个虚拟环境避免不同项目对依赖版本的要求互相冲突。我一般用python -m venv .venv建环境激活以后再把Selenium装进去这样后面写项目脚本也不会污染全局环境。1.2 浏览器驱动最容易踩坑的一环Selenium的本质是用代码去驱动浏览器它和浏览器之间还需要一个“翻译官”这个翻译官就是浏览器驱动。Chrome对应ChromeDriverEdge对应EdgeDriverFirefox对应GeckoDriver。这里最让人头疼的是版本匹配问题Chrome浏览器和ChromeDriver的版本必须精确对应大版本号不一致就没法正常工作。我一开始手动下载ChromeDriver的时候就碰到过SessionNotCreatedException提示大段英文核心意思其实是“你的ChromeDriver版本只支持某个Chrome版本但当前浏览器版本对不上”。后来我彻底放弃手动管理改用webdriver-manager这个库它会自动读取你本机浏览器的版本号然后下载匹配的驱动文件彻底告别版本匹配的地狱pip install webdriver-manager代码里也不需要关心驱动文件放在哪里了from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service driver webdriver.Chrome(serviceService(ChromeDriverManager().install())) driver.get(https://example.com)另外插一句如果你用的是Selenium 4.6以上的版本新版的Selenium Manager会自动处理驱动下载理论上直接写webdriver.Chrome()就能跑起来。但国内网络环境下Selenium Manager自动下载驱动偶尔能碰上网络问题所以我还是更信任通过webdriver-manager这个库来走。驱动这块只要稳住了后面的自动化脚本才谈得上有意义。2. 页面元素定位与基础操作2.1 八大定位方式别只会用XPathSelenium定位元素的核心就是两个方法find_element定位单个元素find_elements定位一组元素。至于按什么条件去定位官方给了八种策略实用性差异很大。定位方式写法典型应用场景稳定性idfind_element(By.ID, username)登录框、唯一输入控件最高namefind_element(By.NAME, q)表单字段、搜索框较高class_namefind_element(By.CLASS_NAME, btn)按样式类收集同名元素中tag_namefind_element(By.TAG_NAME, a)遍历所有链接中link_textfind_element(By.LINK_TEXT, 登录)精确匹配超链接文本中partial_link_textfind_element(By.PARTIAL_LINK_TEXT, 登)模糊匹配超链接文本中低css_selectorfind_element(By.CSS_SELECTOR, #app .item)结构清晰、层级明确高xpathfind_element(By.XPATH, //div[classitem])复杂条件、按文本内容定位中高我的定位策略优先级是有id就用id没有id就看name、class这类稳定属性再不够就用CSS选择器最后才考虑XPath。XPath虽然灵活但它依赖DOM树的结构页面一旦调整层级表达式可能直接失效。而CSS选择器在性能和稳定性上通常更优。另外提醒一句尽量不要用下标去定位列表里的元素比如xpath: (//div[classitem])[1]这种。虽然它能跑但列表顺序一变就会抓错数据。比较稳妥的做法是给目标元素找“特征”比如文本内容、自定义属性或者先定位列表容器再在里面遍历子元素。2.2 基础交互从点击、输入到页面跳转定位到元素之后最常用的操作就是点击和输入。一个典型的登录动作代码看起来是这个样子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(admin) password_input driver.find_element(By.NAME, password) password_input.send_keys(123456) login_btn driver.find_element(By.CSS_SELECTOR, button.login) login_btn.click()这里有几个细节值得说。第一send_keys是往输入框里追加内容如果输入框有默认值需要先调一次clear()再输入不然会追着旧内容拼接。第二按钮能不能点不光要看它是否存在还得看它是否可交互所以我在点击之前经常用element_to_be_clickable这个条件去等。第三定位到元素以后如果想读取它的属性就调get_attribute(href)想读取可见文本就取.text属性两者不要搞混。页面级别的操作也要顺手掌握。driver.get()打开页面driver.back()后退driver.forward()前进driver.refresh()刷新driver.maximize_window()最大化窗口全部用完以后用driver.quit()关闭驱动并释放资源。如果你打开了多个标签页还需要通过driver.window_handles获取所有句柄再用driver.switch_to.window()切换。2.3 iframe和弹窗两个易被忽视的暗坑页面里嵌入iframe时Selenium默认只能在主文档里找元素直接定位iframe内部的元素一定会报NoSuchElementException。处理方式是先切换到对应iframe操作完再切回主文档driver.switch_to.frame(frame_id) # 操作iframe内部的元素 driver.switch_to.default_content()弹窗又是另一个典型问题。普通弹窗可以用driver.switch_to.alert.accept()处理但很多人遇到的其实是页面内部的弹层遮罩它本质上是DOM元素切不到需要你手动点击关闭按钮或者等待其自动消失。这类弹层经常挡住按钮导致click()报ElementClickInterceptedException遇到这种情况先检查页面上有没有这种“透明遮罩”。3. 高级技巧自定义下拉框与元素元数据3.1 真正的硬骨头divulli组合下拉框Selenium内置的Select类只能处理原生select和option结构这在今天的前端环境里越来越不够用了。很多管理系统尤其是后台项目前端用的是Element UI、Ant Design这类组件库下拉框实际是divulli组合出来的一组模拟控件。用Select类去选要么拿不到选项要么直接报错。我自己的处理思路是不去想“怎么模拟Select”而是把它当成“点击弹层再选择列表项”的交互流程。核心步骤有三个点击触发下拉的div控件让列表展开。等待列表项可见尤其要留意选项是否由异步接口返回。如果页面是异步加载数据直接拿列表定位基本会扑空。根据可见文本或属性定位目标li并点击。我封装了一个函数目前在项目里一直复用def select_custom_option(driver, trigger_locator, option_text, list_locator, option_locator): trigger WebDriverWait(driver, 10).until( EC.element_to_be_clickable(trigger_locator) ) trigger.click() WebDriverWait(driver, 10).until( EC.visibility_of_element_located(list_locator) ) target WebDriverWait(driver, 10).until( EC.element_to_be_clickable(option_locator(option_text)) ) target.click()调用的时候传入定位信息即可。比如页面里有一个城市选择控件我可以这样写select_custom_option( driver, (By.CSS_SELECTOR, .select-trigger), 北京, (By.CSS_SELECTOR, ul.dropdown-menu), lambda text: (By.XPATH, f//li[contains(text(), {text})]))用的时候有两点需要注意。第一有些组件在点击之后列表项还是隐藏的但DOM里已经存在这时我一般把等待条件从visibility_of_element_located换成element_to_be_clickable以确保元素既在DOM中又处于可点击状态减少误判。第二如果目标li被页面上方的浮动遮罩挡住直接点击就可能报拦截异常我会先尝试用execute_script(arguments[0].click(), target)强制点击绕过遮挡但要记住这只是兜底方案优先还是等遮罩消失后再正常点击毕竟真实用户的操作路径也应该是先等页面干扰消失。3.2 只存定位元数据别缓存WebElement对象这是我在长时间维护自动化脚本之后总结出来的一个工程化经验。很多人习惯把定位到的WebElement对象直接存在变量里流程简单时没问题可一旦页面发生刷新、跳转或者局部重绘这个旧对象就“失效”了再操作它会报StaleElementReferenceException。这个异常在爬取动态表格、执行重复任务时会频繁出现。解决思路很直接不要在长周期流程里缓存元素对象而是只存“在哪里能找到这个元素”的定位元数据。所谓定位元数据就是(By.ID, username)这样的定位逻辑存成一个By加对应value的元组。每次要用元素的时候就通过定位元数据重新去DOM里查找一次。我通常会把页面里所有需要操作的元素定位集中管理类似一个配置文件的样子LOCATORS { username: (By.ID, username), password: (By.NAME, password), login_btn: (By.CSS_SELECTOR, button.login), table_rows: (By.XPATH, //table[iddata]/tbody/tr), next_page: (By.LINK_TEXT, 下一页), } def find(driver, key, timeout10): by, value LOCATORS[key] return WebDriverWait(driver, timeout).until( EC.presence_of_element_located((by, value)) )这样每次操作前重新查询虽然多了一次查找的过程但稳定性提升非常明显。数据采集场景里我要批量读取表格每一行每行都由多个单元格组成如果缓存整行的WebElement翻页或者DOM刷新后就会失效而只存定位元数据、每次重新定位页面无论怎么刷新都能稳定工作。更进一步如果业务复杂、需要维护的页面元素多到上百个我还会把这张定位配置表抽成YAML或JSON文件。页面改版时不用改Python代码只调整配置文件里的定位信息就行维护成本大幅下降。这也正是“页面元素枚举”的另外一种理解方式与其在代码里散落一堆魔法字符串不如把它们全部体现成可枚举、可配置、可追踪的定位元数据。4. 等待机制、异常排查与完整实战4.1 等待机制怎么选sleep别滥用初学Selenium的时候最常见的做法是在关键操作之间塞一个time.sleep(3)给页面一点加载时间。这种方法最大的问题在于不管页面3秒内是否已经加载完成脚本都会傻等满3秒自动化跑得又慢又不稳定。页面快了是浪费页面慢了又不够纯属碰运气。更好的做法是用Selenium自带的等待机制分两类隐式等待和显式等待。隐式等待是全局设置设一次之后每次find_element查找元素时Selenium都会在设定的时间内持续轮询直到元素出现或者超时driver.implicitly_wait(10)显式等待更适合关键步骤配合expected_conditions使用可以精确等待某个条件成立比如元素可见、可点击、存在、包含某段文本等WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, submit)) )我的建议是全局用一次implicitly_wait(10)兜底复杂交互点再单独用WebDriverWait精确控制。不要把time.sleep完全扔掉偶尔在“等待一定时间缓存稳定”这种场景里它也救过命但绝大多数情况下都应该避免。4.2 高频异常排查速查表实战中出问题不可怕可怕的是不知道从哪个方向排查。下面这些异常是我在真实项目里高频遇到的挨个说一遍比较啰嗦整理成一张速查表会更直观异常类型常见原因处理方式SessionNotCreatedException浏览器驱动版本和浏览器版本不匹配用webdriver-manager自动匹配驱动NoSuchElementException定位表达式不对、元素未加载、在iframe里检查表达式、显式等待、切换iframeTimeoutException等待条件超时可能被验证码或登录拦截延长超时时间检查页面状态StaleElementReferenceExceptionDOM刷新旧元素引用失效不要缓存WebElement每次重新定位ElementClickInterceptedException其他元素遮住目标元素等待遮罩消失或强制JS点击ElementNotInteractableException元素被禁用、不可见、尺寸为0检查is_enabled和is_displayed先滚动到可视区域排查这类问题时我习惯先做一件事在浏览器开发者工具里手动执行一次目标操作确认元素在正常情况下是否可见、可点击。如果手动都点不动别怀疑Selenium先去处理页面本身的状态。4.3 一个能直接跑的完整实战案例把前文的知识串起来这里写一个实际可用的案例打开一个带筛选和后端接口的数据列表页面选择自定义下拉框的城市条件查询表格数据再抓取表格行内容保存到CSV文件。代码尽量保持可运行你可以根据自己的目标页面调整定位表达式。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 # 定位元数据统一管理 LOCATORS { city_trigger: (By.CSS_SELECTOR, .city-select .select-trigger), city_list: (By.CSS_SELECTOR, .city-select ul.dropdown-menu), search_btn: (By.CSS_SELECTOR, button.search), table_rows: (By.CSS_SELECTOR, table.data-table tbody tr), } def wait_clickable(driver, by, value, timeout10): return WebDriverWait(driver, timeout).until( EC.element_to_be_clickable((by, value)) ) def select_city(driver, city_name): trigger_by, trigger_value LOCATORS[city_trigger] wait_clickable(driver, trigger_by, trigger_value).click() list_by, list_value LOCATORS[city_list] WebDriverWait(driver, 10).until( EC.visibility_of_element_located((list_by, list_value)) ) target WebDriverWait(driver, 10).until( EC.element_to_be_clickable( (By.XPATH, f//li[contains(text(), {city_name})]) ) ) target.click() def main(): driver webdriver.Chrome() try: driver.get(https://example.com/data) driver.maximize_window() select_city(driver, 上海) search_btn_by, search_btn_value LOCATORS[search_btn] wait_clickable(driver, search_btn_by, search_btn_value).click() # 等待表格重新渲染这里用数据行出现来判定而不是固定sleep rows_by, rows_value LOCATORS[table_rows] WebDriverWait(driver, 10).until( EC.presence_of_element_located((rows_by, rows_value)) ) # 枚举表格行每次重新定位避免StaleElementReferenceException rows driver.find_elements(By.CSS_SELECTOR, table.data-table tbody tr) records [] for row in rows: columns row.find_elements(By.TAG_NAME, td) records.append([col.text for col in columns]) with open(output.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([字段1, 字段2, 字段3]) writer.writerows(records) finally: driver.quit() if __name__ __main__: main()这段代码把几个关键实践都体现出来了定位元数据集中管理而不是散落各处自定义下拉框通过触发、等待、选择三步处理等待条件基于业务状态而不是死等表格抓取通过重新定位规避失效问题。即便你只把其中一部分思路迁移到自己的项目里稳定性都会明显提升。我在实际项目里跑自动化最深的体会是页面改版和异步加载永远是最大的两个变数能对抗这两个问题的从来不是多高深的技巧而是把等待条件写对、把定位信息收敛好、把异常边界想清楚。后面如果你在实操中遇到更刁钻的定位问题不妨顺着这个思路去拆先观察DOM结构再确认等待条件最后再写定位表达式大部分疑难杂症都会有答案。
返回列表