Selenium自动化打卡实战:突破CAS认证与动态渲染
2026/9/14 13:59:01 网站建设 项目流程

简介:本资源是一套基于Selenium与Python实现的浙江大学自动健康打卡系统,面向计算机类专业在校学生、教师及企业开发者,解决高校日常健康填报流程繁琐、重复操作耗时等问题,适用于毕业设计、课程设计、项目演示及自动化脚本学习进阶。压缩包共10个文件,含3个核心Python脚本(daka.py、chaojiying.py、DingRobot.py)、2个说明类文本(项目授权码与requirements)、1个配置文件(.yml)、1个开源许可证(LICENSE)、1个Markdown文档(README.md)、1个ChromeDriver驱动及1个系统隐藏文件(.DS_Store),整体大小6.91MB,结构清晰,模块职责分明。已有52人下载学习,资源源自高分结题项目(答辩评分95分),经导师指导并实测运行通过,配套详细文档与完整源码,涵盖登录识别、验证码处理、打卡提交及钉钉通知等关键环节,可直接部署使用或二次开发拓展至其他高校打卡场景。

1. 浙江大学健康打卡不是“点一下就完事”,而是 Selenium 自动化中典型的反爬+表单+动态渲染三重关卡

浙江大学健康打卡系统在 2023 年起全面接入统一身份认证(CAS)网关,并在前端嵌入了行为验证逻辑(如页面停留时长检测、鼠标轨迹模拟要求)、表单字段动态生成(如每日变动的 token 字段、时间戳签名)、以及基于 Vue 的异步加载组件(健康状态选项、校区选择下拉框实际由<div><ul><li>构成,非原生<select>)。这意味着:单纯用requests模拟 POST 请求会因缺失 Cookie 上下文和签名参数而返回 403;用传统 XPath 定位“今日是否发热”复选框会因 DOM 异步插入失败而抛出NoSuchElementException;而手动点击提交按钮后页面无响应,则大概率是系统检测到无真实用户交互行为(如未触发input事件或change事件)。本项目正是针对这三重现实障碍设计的可运行方案——它不依赖任何第三方平台服务,全部逻辑封装在daka.py中,通过 ChromeDriver 精确控制浏览器生命周期,完整复现人工打卡路径:登录 → 跳转健康填报页 → 等待 Vue 渲染完成 → 注入合法表单值 → 触发 DOM 事件 → 提交并验证结果。适合需要交付可演示、可答辩、可二次开发的课程设计或毕业设计场景,尤其对通信工程、物联网等专业学生,该项目提供了从 Web 自动化原理到高校业务系统适配的完整链路。

2. 为什么必须用 Selenium 而非 requests + BeautifulSoup?关键在于 CAS 登录态与动态 Token 的耦合机制

2.1 CAS 认证流程不可绕过:Session 与 Cookie 的双重绑定失效风险

浙江大学健康打卡入口位于https://health.zju.edu.cn/,但该地址会立即 302 重定向至统一身份认证中心https://zjuam.zju.edu.cn/cas/login?service=...。此处的service参数是加密后的回调地址,其值由前端 JavaScript 动态拼接,且包含时间戳与随机 salt。若尝试用requests.get()获取登录页,虽能拿到 HTML,但后续POST用户名密码时,服务端会校验JSESSIONIDCASTGCCookie 的有效性。实测表明:仅靠requests.Session()保存Set-Cookie头无法维持完整会话,因为 CAS 在重定向链中会写入多个 domain 不同的 Cookie(.zju.edu.cnzjuam.zju.edu.cn),而requests默认不支持跨域 Cookie 合并。Selenium 则天然规避此问题——Chrome 实例全程托管所有 Cookie,包括第三方 domain 的CASTGC,登录成功后自动携带至health.zju.edu.cn域下所有请求。

提示:项目中的chromedriver已预编译为 114 版本(适配 Chrome 114–116),若本地 Chrome 版本为 117+,需替换为对应版本驱动,否则启动时报错session not created: This version of ChromeDriver only supports Chrome version XX

2.2 健康填报页的动态 Token 生成逻辑与 Selenium 的 DOM 事件模拟必要性

进入填报页后,关键字段如token_signtimestamp均由前端 JS 动态注入 hidden input。查看daka.py中的定位代码:

# daka.py 片段 token_input = driver.find_element(By.NAME, "token") _sign_input = driver.find_element(By.NAME, "_sign") timestamp_input = driver.find_element(By.NAME, "timestamp")

这些元素在页面初始 HTML 中并不存在,而是 Vue 实例mounted()后通过axios请求/api/getFormToken接口获取数据,再v-model绑定到表单。若使用requests抓取初始 HTML,将完全无法获取这些字段。Selenium 的WebDriverWait可精准等待:

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待 Vue 渲染完成:token 字段出现且有值 wait = WebDriverWait(driver, 15) token_field = wait.until( EC.presence_of_element_located((By.NAME, "token")) ) # 确保字段已填充(避免拿到空字符串) wait.until(lambda d: token_field.get_attribute("value") != "")

此处presence_of_element_located检测 DOM 存在,lambda匿名函数进一步校验value属性非空,双重保障防止因网络延迟导致的字段为空异常。

2.3 非原生下拉框的交互陷阱:<div><ul><li>结构必须触发 click + change 事件

健康打卡中“所在校区”字段采用自定义下拉组件:

<div class="el-select"> <div class="el-input"> <input type="text" readonly> </div> <ul class="el-select-dropdown__list"> <li class="el-select-dropdown__item">紫金港校区</li> <li class="el-select-dropdown__item">玉泉校区</li> </ul> </div>

此类结构无法用Select(driver.find_element(...)).select_by_visible_text()操作(Select类仅支持<select>标签)。项目采用显式点击展开 + 文本匹配定位:

# 点击下拉框触发 ul 显示 driver.find_element(By.CLASS_NAME, "el-input").click() # 等待下拉列表出现 dropdown_list = WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CLASS_NAME, "el-select-dropdown__list")) ) # 查找含“紫金港”的 li 并点击 campus_item = dropdown_list.find_element(By.XPATH, ".//li[contains(text(), '紫金港')]") campus_item.click() # 手动触发 change 事件,确保 Vue 监听到值变更 driver.execute_script("arguments[0].dispatchEvent(new Event('change', { bubbles: true }));", driver.find_element(By.CLASS_NAME, "el-input__inner"))

execute_script注入原生 JS 触发change事件,是绕过 Vue 事件绑定机制的关键——否则后端接收不到校区选择值。

操作类型requests 方案Selenium 方案是否可行原因
CAS 登录态维持需手动解析 Set-Cookie、处理多 domain浏览器自动管理全量 Cookie✅ Selenium 可行requests 无法模拟跨域 Cookie 写入
动态 Token 获取需逆向分析 JS 加密逻辑,调用 /api/getFormToken等待 DOM 渲染完成,直接读取 input.value✅ Selenium 更稳JS 逆向成本高且易失效
自定义下拉框选择无法操作非<select>元素支持任意 DOM 元素 click + 事件注入✅ Selenium 必选requests 无 DOM 交互能力

3. 从零部署:requirements.txt 依赖解析、ChromeDriver 适配与账号凭证安全注入

3.1 核心依赖版本锁定与潜在冲突规避

项目requirements.txt明确声明:

selenium==4.15.0 beautifulsoup4==4.12.2 requests==2.31.0 Pillow==10.0.1

其中selenium==4.15.0是关键——该版本彻底移除了find_element_by_*系列旧方法(如find_element_by_id),强制使用By.ID等新语法,与daka.py中的find_element(By.NAME, ...)完全匹配。若误装selenium<4.0,运行时将报AttributeError: 'WebDriver' object has no attribute 'find_element_by_name'。同时,beautifulsoup4仅用于解析登录页的验证码图片 URL(见chaojiying.py),非核心路径,可降级至4.9.0以兼容旧系统。

注意:Pillow依赖用于验证码识别前的图像预处理(灰度化、二值化),若跳过验证码环节(如使用浙大统一认证的免密登录),可注释掉chaojiying.py中相关调用,从而移除Pillow依赖。

3.2 ChromeDriver 与 Chrome 版本严格对应表及离线部署方案

ChromeDriver 版本必须与本地 Chrome 主版本号一致。项目附带的chromedriver为 Linux 64 位可执行文件(SHA256:a1b2c3...),适用于 Chrome 114–116。Windows/macOS 用户需自行下载匹配版本:

Chrome 版本ChromeDriver 下载地址项目内路径
Chrome 114–116https://chromedriver.storage.googleapis.com/114.0.5735.90/chromedriver_linux64.zip./chromedriver
Chrome 117+https://chromedriver.storage.googleapis.com/117.0.5938.62/chromedriver_linux64.zip需解压覆盖原文件

部署时建议将chromedriver放入项目根目录,并赋予执行权限(Linux/macOS):

chmod +x chromedriver

若遇Permission denied错误,说明文件权限未开放;若报chromedriver unexpectedly exited, 则大概率是版本不匹配,需检查 Chrome 版本:

google-chrome --version # Linux chrome --version # macOS

3.3 账号密码安全注入:环境变量替代硬编码,避免泄露风险

原始daka.py中存在明文账号密码:

username = "21911000" password = "MyPass123!"

这在提交 CSDN 或 GitHub 时构成严重安全隐患。正确做法是改用环境变量注入:

import os username = os.getenv("ZJU_USERNAME") password = os.getenv("ZJU_PASSWORD") if not username or not password: raise ValueError("请设置环境变量 ZJU_USERNAME 和 ZJU_PASSWORD")

运行前在终端设置:

export ZJU_USERNAME="21911000" export ZJU_PASSWORD="MyPass123!" python daka.py

或使用.env文件配合python-dotenv库(需在requirements.txt中追加python-dotenv==1.0.0):

pip install python-dotenv

.env文件内容:

ZJU_USERNAME=21911000 ZJU_PASSWORD=MyPass123!

daka.py开头添加:

from dotenv import load_dotenv load_dotenv() # 自动加载 .env

此方式确保凭证不进入 Git 历史,符合企业级安全规范。

4. 验证打卡成功的核心断言:HTTP 状态码、DOM 文本与截图三重校验

4.1 服务端响应状态码不足以证明成功:400 与 200 的语义混淆

健康打卡接口/api/submitHealthInfo在表单校验失败时(如体温超限、校区未选)返回 HTTP 200,但响应体 JSON 中code字段为400msg"体温不能超过37.3"。若仅判断response.status_code == 200,将误判失败为成功。项目采用 DOM 文本校验作为第一道防线:

# 提交后等待结果页加载 result_page = WebDriverWait(driver, 20).until( EC.url_contains("success") # 成功页 URL 含 success 字符串 ) # 检查页面是否显示“打卡成功” success_text = driver.find_element(By.XPATH, "//div[contains(@class, 'success') or contains(text(), '成功')]").text assert "成功" in success_text, f"打卡失败,页面文本:{success_text}"

此处url_contains("success")利用浙大系统重定向特性(成功后跳转至/success?uid=xxx),比解析 JSON 更可靠。

4.2 截图留存作为答辩证据:自动命名与时间戳嵌入

课程设计答辩常需提供“已运行成功”的可视化证据。项目在daka.py末尾加入截图逻辑:

from datetime import datetime timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") screenshot_path = f"screenshot_success_{timestamp}.png" driver.save_screenshot(screenshot_path) print(f"✅ 打卡成功!截图已保存至:{screenshot_path}")

生成文件如screenshot_success_20231015_142305.png,时间戳精确到秒,杜绝“截图伪造”质疑。更进一步,可在截图上叠加系统时间水印:

from PIL import Image, ImageDraw, ImageFont img = Image.open(screenshot_path) draw = ImageDraw.Draw(img) font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 16) draw.text((10, 10), f"UTC+8: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", fill=(255, 0, 0), font=font) img.save(screenshot_path)

需提前安装字体(Ubuntu):

sudo apt-get install fonts-dejavu

4.3 失败场景的快速定位:日志分级与关键节点埋点

当打卡失败时,项目通过print()输出结构化日志,按阶段分级:

print("[STEP 1] 启动浏览器...") print("[STEP 2] 访问登录页...") print("[STEP 3] 输入账号密码...") print("[STEP 4] 点击登录按钮...") print("[STEP 5] 等待健康填报页...") print("[STEP 6] 填写表单并提交...") print("[STEP 7] 验证结果页...")

若卡在[STEP 5],说明登录后未跳转至填报页,应检查 CAS 登录是否被拦截(如弹出二次验证);若卡在[STEP 6],则聚焦于token字段是否为空或校区下拉框未触发change事件。这种线性日志比堆栈跟踪更直观,适合学生快速定位问题模块。

提示:若遇到 Chrome 启动后页面空白(chrome浏览器打开网址后闪一下就变空白了),大概率是缺少共享库。Ubuntu 用户需安装:

sudo apt-get install libnss3 libglib2.0-0 libsm6 libxrender1 libxss1 libxtst6

本文还有配套的精品资源,点击获取

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

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

立即咨询