十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Selenium自动化整活靶场:电子神庙项目踩坑笔记

Selenium自动化整活靶场:电子神庙项目踩坑笔记 最近整理旧笔记时翻出一个挺抽象的Demo一个挂着金色大字“电子神庙”的本地网页布告栏上写着“自动售卖赎罪券扫码立付立得”。这其实是我当时为了练Selenium自动化专门搭出来的一个整活靶场。结果没想到就这么一个花里胡哨的小破站把Selenium领域的经典硬骨头全给踩了一遍selenium安装和浏览器驱动匹配、网页拼图验证怎么自动化、点击按钮就下载图片后怎么等文件真正落盘、selenium上传本地文件的正确姿势还有运行时被反爬识别、多浏览器并发隔离这些糟心事。这篇文章没有太多虚的全是实测过的代码和踩坑记录。项目背景就是虚构的“电子神庙”代码和站点都只在本地实验环境跑不映射任何现实组织或事件。适合正在学Python Selenium的初学者也适合想搞Web自动化回归测试的兄弟参考尤其是那些搜“selenium图片滑块验证”和“selenium怎样使文件下载完成之后才进行下一步”的人建议直接跳到对应的章节看。1. 电子神庙到底是什么项目背景与自动化目标1.1 一个整活靶场为什么值得测第一次看到“电子神庙自动售赎罪券”这个目标第一反应肯定是搞笑。但认真琢磨一下这个场景放到自动化测试里其实非常典型一个页面里有表单、有选择器、有滑块验证、有文件下载、有文件上传几乎覆盖了日常Web交互的所有常见形态。我当时搭的神庙页面很简单顶部是一行大字“电子神庙功德系统”中间列出三种赎罪券套餐普通版、尊享版、典藏版下面是一个购买表单要填写姓名和供奉金额。提交表单之前必须通过一个拼图滑块验证验证过了才能提交订单。提交订单之后页面会生成一张带防伪码的电子赎罪券图片点击按钮就能下载。最后还有一个“供奉功德箱”的入口允许上传一张善款截图。整个流程走下来像极了一个真实业务系统里的注册、下单、下载凭证、上传材料只不过换成了整活的皮。这种“不正经”的页面反而是最好的自动化练手对象因为交互丰富、状态多你不得不去处理各种等待和异常。1.2 赎罪券购买的用户旅程与自动化拆解我把自动化目标拆成了下面几条路径进入电子神庙首页检查页面的关键元素是否存在选择赎罪券等级填写姓名和供奉金额组装滑块验证轨迹通过拼图验证提交订单等待订单成功提示点击“下载赎罪券”按钮把图片保存到本地指定目录打开功德箱上传入口上传本地供奉文件最后校验下载文件存在、上传列表出现新文件名。这一套流程跑通之后其实就等价于一条完整的Web业务端到端回归用例。之后不管是谁改了页面布局还是换了按钮ID脚本都能在几分钟内告诉你“庙塌了”。1.3 为什么选PythonSelenium这套组合选型的时候也纠结过Playwright、Cypress这些新一代框架但最后我还是用回了Selenium。原因很实在网上搜出来的自动化问题十有八九都是Selenium体系的踩坑资料最多。而且Python生态处理滑块图片识别太顺手了OpenCV、Pillow直接拿来用配合Selenium驱动浏览器整个链路特别顺。再说很多人问的“selenium音乐下载器”本质上和这个项目的赎罪券下载一模一样打开页面、锁定下载链接或按钮、点击触发下载、等待文件完成。把神庙这个项目吃透其他下载场景基本都是换汤不换药。2. 开荒前的环境准备selenium安装与驱动版本那些坑2.1 五分钟装好Selenium基础环境最基础的安装其实就一行命令pip install selenium然后确保本机装了一个Chrome浏览器就可以开始跑了。很多人会在这一步卡住不是selenium装不上而是Python环境乱。建议用虚拟环境python -m venv .venv .venv\Scripts\activate # Windows # 或者 source .venv/bin/activate # Linux/macOS pip install selenium装完之后写个最简单的脚本验证环境from selenium import webdriver driver webdriver.Chrome() driver.get(http://localhost:8080) print(driver.title) driver.quit()如果这一句能正常弹窗并打印出页面标题基础环境就OK了。2.2 Chrome和ChromeDriver版本不匹配怎么办Selenium本身不是直接操控浏览器的它通过一个浏览器驱动ChromeDriver来发指令。驱动和浏览器版本必须匹配否则你会碰到一条非常经典的报错session not created: This version of ChromeDriver only supports Chrome version XX我第一次跑的时候就被这个干懵了明明代码没问题就是起不来。原因很简单Chrome自动更新到了新版本而手里的ChromeDriver还是旧的。解决方式有两种一是手动到ChromeDriver官方渠道下载和本机Chrome大版本一致的驱动把路径写进Service里二是用下面的方式自动匹配强烈推荐这种省心。2.3 webdriver-manager实现驱动自动管理直接用webdriver-manager这个库托管驱动它会自动检测本机Chrome版本并下载对应驱动pip install webdriver-managerfrom selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options webdriver.ChromeOptions() options.add_argument(--start-maximized) service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionsoptions)实际体验下来这个方案基本能解决90%的驱动版本问题。唯一要注意的是首次运行会下载驱动比较慢。如果公司内网没法访问外网那就只能手动下载驱动放到指定目录然后用Service指定路径。2.4 浏览器启动方式正常模式、Headless和无痕模式调试阶段建议正常模式启动能直观看到每一步操作脚本稳定之后如果要挂到CI上跑就用无头模式options.add_argument(--headlessnew) options.add_argument(--disable-gpu)无痕模式也可以考虑尤其是需要隔离Cookie和用户数据的场景options.add_argument(--incognito)还有几个常用参数都是遇到过问题的--no-sandboxLinux环境下没有用户权限隔离时会用到--disable-dev-shm-usageDocker容器里避免临时空间不足--user-agent需要模拟某类终端时设置。3. 自动购买赎罪券从定位元素到强制点击3.1 用显式等待代替无脑sleep新手写Selenium最爱用time.sleep(3)觉得等三秒钟页面肯定加载完。但这种方式又慢又脆页面加载快了白白多等两秒网络一慢三秒可能还不够脚本直接报错找不到元素。正确的做法是显式等待。页面里只要元素出现或可点击脚本立刻继续from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC buy_btn WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.CSS_SELECTOR, button.buy-btn)) ) buy_btn.click()我一般会封装一个简单的工具函数后面所有操作都复用def wait_clickable(driver, by, selector, timeout10): return WebDriverWait(driver, timeout).until( EC.element_to_be_clickable((by, selector)) )3.2 表单填写与按钮点击的细节处理神庙页面的表单有三个关键控件套餐单选按钮、姓名输入框、供奉金额输入框。定位的时候能用ID就用ID能用CSS选择器就用CSS选择器XPath反而容易因为页面微调而失效。# 选择尊享版套餐 driver.find_element(By.CSS_SELECTOR, input[nameplan][valuepremium]).click() # 填写姓名和金额 name_input driver.find_element(By.ID, believer-name) name_input.clear() name_input.send_keys(自动化测试员) amount_input driver.find_element(By.ID, donation-amount) amount_input.clear() amount_input.send_keys(998)这里有个小细节send_keys之前最好先clear()否则遇到输入框里本来有默认值的情况会出现“输入内容被拼接”的问题。另外如果输入框有格式化逻辑比如金额有限制直接输入长字符串可能会被截断所以输入完成后最好再读一下输入框的value值做校验。3.3 元素被遮挡或没绑事件时如何曲线救国用Selenium跑业务流程最烦的就是元素明明在页面上但点击时报一堆异常。我遇到过的典型情况有几种第一种元素被弹窗或浮层遮挡。这时候直接click会报element click intercepted解决办法是先尝试关闭弹窗或者用ActionChains把鼠标移过去再点。第二种元素不在浏览器可视区域内。Selenium为了保证点击真实会要求元素可见且可交互如果元素在页面底部它会自动滚动但有时候滚动不到手动来一下更稳driver.execute_script(arguments[0].scrollIntoView({block:center});, element)第三种后台JS没绑定好点击事件或者按钮的onclick需要某种前置状态。让Selenium直接执行JS强制点击driver.execute_script(arguments[0].click();, element)这种写法有点暴力但预处理一些特殊控件非常好用。注意不要所有地方都强制点击不然就失去了真实用户模拟的意义。4. 神庙门口的拼图验证图片滑块验证自动化思路4.1 滑块验证到底在验证什么滑块验证的核心机制其实不复杂后端或前端生成一张带缺口的背景图同时给出一块拼图用户必须把拼图拖到缺口位置才能通过。校验的时候服务端看的是两个东西最终坐标的误差范围以及拖拽过程中产生的轨迹数据。自动化滑块验证难在两个地方一是怎么找到拼图应该落到的x坐标二是怎么让拖拽轨迹看起来像人。我在电子神庙里放的滑块组件是自己写的就是为了可控地做验证码组件测试和自动化学习。这里必须把话说清楚本文所有自动化操作都是在我自己搭建的本地Demo站点上完成的目的是练习自动化测试和验证码组件自测。线上真实站点的验证码是安全防护的一部分未经授权使用自动化绕过轻则违反用户协议重则触犯法律。下面的内容仅供技术学习和受控环境测试使用。4.2 读取背景图并计算缺口距离神庙的滑块背景图是一张带缺口的风景图拼图块是一个小方块。最佳方案是用OpenCV做模板匹配。首先把背景图和拼图块拿到本地。背景图如果是img标签可以直接取src下载如果是canvas渲染的需要通过JS把canvas转成base64再写文件import base64 import cv2 import numpy as np from selenium import webdriver # 从canvas里抽背景图 canvas_base64 driver.execute_script( var canvas document.querySelector(#slider-bg); return canvas.toDataURL(image/png).replace(/^data:image\\/\\w;base64,/, ); ) img_bytes base64.b64decode(canvas_base64) with open(bg.png, wb) as f: f.write(img_bytes)然后用OpenCV做匹配def find_gap_x(bg_path, tpl_path, scale1.0): bg cv2.imread(bg_path) tpl cv2.imread(tpl_path) result cv2.matchTemplate(bg, tpl, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(result) # max_loc[0] 是缺口左上角的x坐标最终算的是拼图中心点 gap_x max_loc[0] * scale return gap_x有一个特别容易踩的坑页面显示图片的尺寸和原始图片物理尺寸不一定一致。如果页面里背景图宽400px实际图片分辨率是800px那算出来的坐标必须乘以0.5再进行拖拽。这个比例问题不搞定轨迹再像人也会差一大截坐标怎么拖都不会成功。还有一些验证码会在背景图上加干扰线模板匹配效果差可以先用高斯模糊或边缘检测预处理bg cv2.GaussianBlur(bg, (3, 3), 0)4.3 用ActionChains模拟真人拖拽轨迹拿到缺口坐标后直接一次性拖到位是最容易失败的因为正常人手拖滑块不可能一步到位。这里我用ActionChains模拟分段的拖拽过程from selenium.webdriver.common.action_chains import ActionChains import random, time slider driver.find_element(By.CSS_SELECTOR, .slider-button) ActionChains(driver).click_and_hold(slider).perform() time.sleep(0.2) movement x_offset steps track_generator(movement) for step in steps: ActionChains(driver).move_by_offset(step, random.uniform(-1, 1)).perform() time.sleep(random.uniform(0.02, 0.06)) time.sleep(0.3) ActionChains(driver).release().perform()轨迹生成函数要模拟“先慢-中间快-再慢”的特征def track_generator(distance): steps [] remaining distance while remaining 0: if remaining distance * 0.6: span random.randint(18, 30) elif remaining distance * 0.3: span random.randint(6, 16) else: span random.randint(2, 6) span min(span, remaining) steps.append(span) remaining - span return steps启动拖拽之前最好先让鼠标移动到滑块上再按下松开之前稍微顿一顿。这些细节看起来不起眼但对轨迹识别的结果影响非常大。4.4 失败重试与轨迹参数调优滑块验证失败之后页面一般会提示“拼图失败请重试”同时重置滑块。脚本不能傻等要做重试机制。我习惯把它包成一个循环for attempt in range(5): try: gap_x locate_gap() drag_slider(gap_x) WebDriverWait(driver, 5).until( EC.visibility_of_element_located((By.CSS_SELECTOR, .slider-success)) ) print(通过验证) break except Exception as e: print(f验证失败第{attempt 1}次重试) driver.refresh() time.sleep(1)调参的时候重点关注三件事坐标换算准不准、拖拽总步数是否过少、每一次移动之间有没有随机停顿。实测下来路径控制在8到15步、总时长在0.8到1.8秒之间是最接近人手习惯的。太快的轨迹本身就会触发风控。4.5 关于验证码自动化的边界再强调一次滑块验证的目的是保护网站和用户不是给自动化脚本当陪练。我的建议是想学这个技术就自己搭一个带滑块验证的Demo站来做实验。现在很多开源的前端组件库都自带滑块组件本地启动一个页面再开发一套自己的验证逻辑足够你研究清楚套路。等项目里需要给自研验证码组件做自动化测试的时候这套东西才会真正派上用场。5. 赎罪券图片下载点击下载与下载完成后再执行下一步5.1 为什么不能直接点击完就继续神庙页面提交订单之后会生成一张赎罪券图片页面上有一个“下载悔罪券”按钮。我用Selenium点击这个按钮之后最开始的做法是等一秒钟就去检查下载目录结果经常拿到一个0字节的临时文件或者干脆找不到文件。原因在于click()只是触发了浏览器的下载动作浏览器需要时间去请求文件、写入磁盘。尤其文件从服务器返回的过程中本地目录里会先出现一个以.crdownload结尾的临时文件文件真正写完之前大小一直在变化。如果脚本不等它写完就去做下一步操作必然出问题。5.2 设置浏览器下载目录的prefs配置为了让下载落到固定目录并且不让浏览器弹“另存为”对话框需要在ChromeOptions里设置prefsdownload_dir os.path.abspath(./downloads) os.makedirs(download_dir, exist_okTrue) options.add_experimental_option(prefs, { download.default_directory: download_dir, download.prompt_for_download: False, download.directory_upgrade: True, safebrowsing.enabled: True })这里比较关键的就是download.default_directory必须用绝对路径相对路径在部分版本的ChromeDriver里不生效。safebrowsing.enabled置为True可以避免下载未被识别文件时被安全浏览拦一道。5.3 点击按钮就下载图片的代码写法场景不同写法也不同。神庙页面是点击按钮触发下载download_btn WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, download-indulgence)) ) download_btn.click()如果是页面里的一个链接直接指向图片地址也可以通过Selenium拿到href后在下载器里处理img_url driver.find_element(By.ID, download-link).get_attribute(href)注意Selenium本身并没有内置“点击下载后自动等待完成”的功能很多人搜“selenium中点击按钮就下载图片的代码怎么写”其实难点不在点击而在点击之后的等待这个后面细说。5.4 轮询判断下载完成大小稳定法与.crdownload法正确等待下载完成的思路是轮询下载目录直到满足以下三个条件目录里有文件没有.crdownload或.tmp后缀的临时文件文件大小稳定不变。我封装的函数长这样import os import time def wait_for_download(download_dir, timeout60): end_time time.time() timeout while time.time() end_time: files [f for f in os.listdir(download_dir) if not f.startswith(.)] active_files [f for f in files if f.endswith(.crdownload) or f.endswith(.tmp)] done_files [f for f in files if not f.endswith(.crdownload) and not f.endswith(.tmp)] if done_files and not active_files: target os.path.join(download_dir, done_files[0]) size1 os.path.getsize(target) time.sleep(1) size2 os.path.getsize(target) if size1 size2 and size1 0: return target time.sleep(0.5) raise TimeoutError(等待下载超时)这里还有一个小技巧如果担心文件只下载到一半而且恰好没有临时后缀可以再校验一下文件头。比如赎罪券图片是PNG就检查前8个字节是不是PNG签名。这样在本地Demo环境跑几乎不会出现“以为完成实际半截”的尴尬。5.5 超时和重试的处理下载超时后脚本不应该直接崩溃而是清理可能存在的半成品文件再重试一次或切换下载通道try: path wait_for_download(download_dir) print(f下载完成{path}) except TimeoutError: for f in os.listdir(download_dir): os.remove(os.path.join(download_dir, f)) download_btn.click() path wait_for_download(download_dir, timeout90)这种“清理重试”的思路在批量下载场景里尤其重要不然整个测试队列会被一个失败下载卡死。6. 供奉功德箱selenium上传本地文件的正确姿势6.1 优先寻找input[typefile]标签繁琐的下单和下载都跑通了最后还剩一个上传步骤。神庙页面底部有个“供奉功德箱”区域点击按钮后会弹出文件选择窗口。Selenium对弹窗式的文件选择器是无能为力的但绝大多数网页上传控件的本质都是一个隐藏的input[typefile]标签外面套一个按钮做样式。所以第一步永远是去找这个input标签file_input driver.find_element(By.CSS_SELECTOR, input[typefile])如果页面里只有一个文件上传入口上面的选择器通常没问题。6.2 send_keys直接写入完整路径找到input之后不需要真的弹Windows窗口直接用send_keys把本地文件路径写进去就行import os file_path os.path.abspath(offering.jpg) file_input driver.find_element(By.CSS_SELECTOR, input[typefile]) file_input.send_keys(file_path)这个操作本质上是把文件路径填入了input的值浏览器会自动触发上传。注意两点第一路径必须使用绝对路径相对路径大概率不认第二Windows下路径分隔符用\\或者直接用os.path.abspath处理别手写C:\xxx。6.3 当上传控件不是一个简单input时怎么处理有些上传组件会把input[typefile]藏得特别深或者干脆没有这个标签点击后就是打开系统文件选择器。这种情况Selenium直接处理不了我通常用两个方案。第一个方案是通过JS构造一个DataTransfer对象模拟文件拖拽。如果上传区域支持drop事件可以直接把文件数据丢进去const input document.querySelector(input[typefile]) const dt new DataTransfer() dt.items.add(new File([test], offering.jpg, { type: image/jpeg })) input.files dt.files input.dispatchEvent(new Event(change, { bubbles: true }))Selenium里执行这段JS可以在很多支持拖拽上传的组件上蒙混过关。第二个方案是用pyautogui或AutoIT直接向文件选择窗口输入路径并回车import pyautogui, time upload_btn.click() time.sleep(1) pyautogui.write(os.path.abspath(offering.jpg)) pyautogui.press(enter)这种方式能用但非常依赖操作系统的图形界面而且对焦点要求高不宜大规模使用。6.4 上传后的结果校验上传完成后不能直接不管要验证文件是不是真的传上去了。神庙页面上传成功后功德箱列表里会出现新文件的名称。自动化脚本里可以做一次等待和判断WebDriverWait(driver, 10).until( EC.text_to_be_present_in_element((By.CSS_SELECTOR, .offering-list), offering.jpg) )更严谨一点可以去读取上传文件的元数据接口或者检查页面的预览区域image标签的src。总之上传之后的校验逻辑和下载之后的校验逻辑同样重要不然真到线上回归测试可能上传接口早就挂了你还浑然不知。7. 跑完整流程时踩过的坑从识别到并发7.1 Chrome提示Chrome正在受到自动测试软件的控制每次启动浏览器顶部都会出现“Chrome正在受到自动测试软件的控制”的提示条。这个条本身不影响脚本跑但会让有些页面判断你正在被自动化工具驱动。如果不想让人一眼看出来可以加两个参数options.add_argument(--disable-blink-featuresAutomationControlled)然后删掉webdriver标记driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}) })注意顺序这两个配置要在浏览器加载任何页面之前生效。实际项目里如果是给自己公司内部的测试环境写脚本不搞这些也完全没影响但模拟用户行为做演示或测试的时候这个遮罩还是干净一点好。7.2 滑块轨迹被识别为机器人的进阶对抗我的神庙Demo站点后期也加了一个“轨迹检测”的功能用来模拟真实平台的轨迹风控。也就是说光有缺口坐标不够轨迹数据如果呈现出机械特征照样判失败。在手势生成的细节上我还做了几件事让过程更接近真人先移动鼠标到滑块上方停顿0.1秒按下后再停顿0.2秒才开始拖拖的过程中除了横向速度变化纵向也加轻微抖动甚至偶尔会在某个位置停一下再继续。实测下来这种轨迹被后端标记为“可疑”的概率明显低于匀速拖动。但要再次强调这些都是为了测试自研验证码组件的健壮性。不要在真实网站上折腾这类对抗手段。7.3 页面懒加载导致元素不在DOM中神庙布告栏下面有一长串“往期善款记录”页面底部采用懒加载滚动到哪才渲染到哪。如果脚本一上来就去点“供奉功德箱”入口可能会因为元素还没渲染而报找不到。解决方式是先滚动页面再等待元素出现driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) offering_btn WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, offering-entry)) )这种“先滚动再等待”的组合拳在长页面、瀑布流页面里特别常用。另外要注意懒加载页面的DOM结构可能是动态插入的如果用XPath写死了层级一旦插入顺序变化就崩。最好的办法是给目标元素加稳定的data-testid或ID属性。7.4 多开浏览器跑批量赎罪券时的隔离问题把整个流程跑通之后我试着用多线程同时跑多个浏览器实例每个实例给不同的人购买赎罪券。结果发现浏览器实例之间Cookie和登录状态互相串原因是没有做用户数据目录隔离。解决办法是给每个实例指定独立的user-data-diroptions.add_argument(f--user-data-dir{os.path.abspath(f./chrome-profile-{thread_id})})用这种方式隔离用户数据之后多线程同时跑就不会互相干扰了。另外一个细节是webdriver-manager首次下载驱动的并发问题多线程同时启动时可能抢同一个临时文件最好在脚本Startup阶段预先调用一次ChromeDriverManager().install()把驱动提前准备好。7.5 一套思路延伸到更多自动化场景这个神庙项目跑通之后我会拿同一套代码去处理别的事。比如之前提到“selenium音乐下载器”从搜索引擎下载页面里点翻页、点下载、等待文件完成本质上就是神庙流程里“点击按钮下载图片”部分的复刻。还有日常做报表导出的自动化也可以用这套思路登录后台、按条件查询、点击导出、等待Excel文件落地、把文件移动到共享目录。我自己的感受是Selenium这套东西看着简单真正让脚本稳定跑完一个完整业务流程坑基本都藏在“等待”和“环境”上。元素没出现你等了、下载没完成你等了、上传路径对吗、浏览器驱动版本对吗把这些细节收拾利索剩下的就是业务逻辑堆代码。电子神庙这种整活项目最大的价值就是用最低的成本把这些坑一次性全踩完。
返回列表