简介:本资源是一套基于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用户名密码时,服务端会校验JSESSIONID与CASTGCCookie 的有效性。实测表明:仅靠requests.Session()保存Set-Cookie头无法维持完整会话,因为 CAS 在重定向链中会写入多个 domain 不同的 Cookie(.zju.edu.cn与zjuam.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、_sign、timestamp均由前端 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–116 | https://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 # macOS3.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字段为400,msg为"体温不能超过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-dejavu4.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
本文还有配套的精品资源,点击获取