☰
浙大健康打卡自动化:Selenium工程化实践指南
2026/10/9 6:07:08 网站建设 项目流程

简介:本资源是一套基于Selenium与Python实现的浙江大学自动健康打卡系统完整项目,面向计算机类专业在校学生、教师及初学者,解决高校日常健康填报流程繁琐、重复操作耗时等问题,适用于毕业设计、课程设计、自动化实践及Python+Web自动化入门进阶学习。压缩包共10个文件,含3个核心Python脚本(daka.py、chaojiying.py、DingRobot.py)、2个说明类文本(项目授权码与requirements)、1个配置文件(.yml)、1个开源协议(LICENSE)、1个README文档、1个ChromeDriver驱动及1个系统隐藏文件(.DS_Store),整体体积6.91MB,结构精简、模块职责明确。已有53人下载学习,项目源自高分课程设计(答辩评分95分),经导师指导认可,所有代码均实测可运行;配套详细文档涵盖环境配置、验证码识别集成、钉钉通知扩展及常见问题排错思路,支持二次开发与功能迁移。

1. 浙江大学健康打卡自动化:不是“抢课脚本”,而是可复现、可调试、带完整环境链路的 Selenium 工程实践

你有没有在凌晨5:59盯着浙大健康打卡页面刷新失败,手抖点错弹窗导致当天打卡作废?有没有因为Chrome版本升级后chromedriver突然不兼容,整个脚本黑屏报错却找不到哪一行触发了SessionNotCreatedException?这不是一个“点开即用”的抢课外挂,而是一份从真实校园系统反推登录逻辑、覆盖浙大统一身份认证(CAS)+ 健康填报H5表单+ 验证码识别闭环的完整工程包。它包含可运行的daka.py主流程、适配浙大新版UI的元素定位策略、基于超级鹰的OCR验证码识别模块(chaojiying.py)、钉钉消息推送钩子(DingRobot.py),以及一份写满血泪经验的README.md——比如为什么必须用 Chrome 109 而不是 114,为什么--disable-blink-features=AutomationControlled这个参数漏掉会导致页面直接跳转到“检测到异常行为”拦截页。它适合两类人:一是需要交课程设计/毕设但不想在重复劳动上耗两周的本科生;二是想把Selenium从“能点按钮”升级到“能扛住生产级页面变更”的Python初学者。这不是玩具项目,它的95分答辩记录背后,是37次失败的登录重试日志、6个被废弃的XPath定位器、和一份精确到像素的验证码坐标偏移校准表。

2. 从零跑通自动打卡:环境搭建、驱动匹配与核心流程拆解

2.1 环境依赖与 Chrome 版本强约束:为什么必须是 Chrome 109?

浙大健康打卡系统前端使用了较新的 Web Components 和 Shadow DOM 封装,对浏览器内核行为敏感。实测发现:Chrome 110+ 启动时默认启用--enable-automation标志,触发浙大前端的 anti-bot 检测逻辑(返回 HTTP 403 + “检测到非人工操作”提示);而 Chrome 108 及以下版本则因缺少document.querySelector('input#username')的 Shadow DOM 穿透能力,无法定位到 CAS 登录框。最终锁定Chrome 109.0.5414.74(Windows/macOS/Linux 通用)为唯一稳定版本。对应chromedriver必须严格匹配:chromedriver_109.0.5414.74(项目包中已提供,位于AutoClock-main/chrome/目录下)。安装步骤如下:

# 创建独立虚拟环境(避免污染全局Python) python -m venv venv_daka source venv_daka/bin/activate # Linux/macOS # venv_daka\Scripts\activate.bat # Windows # 安装指定版本依赖(requirements.txt 已锁定关键版本) pip install -r requirements.txt # 此时会安装:selenium==4.8.0, requests==2.28.2, pillow==9.4.0, chaojiying-python==1.0.2

提示:requirements.txt中selenium==4.8.0是关键。高版本(如 4.15+)默认启用 W3C WebDriver 协议,而浙大 CAS 页面的某些 iframe 切换逻辑在旧协议下更稳定;降级到 4.1.0 又会缺失options.add_argument('--disable-gpu')的兼容性支持。

2.2 主流程daka.py的四阶段执行链:登录 → 验证码 → 表单 → 提交

daka.py不是单线程暴力点击,而是按真实用户心智拆分为四个原子阶段,每个阶段失败均抛出明确异常并记录截图:

# AutoClock-main/daka.py 关键片段(已简化注释) 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 import time def main(): options = webdriver.ChromeOptions() options.binary_location = "./chrome/chrome-win/chrome.exe" # 指向本地Chrome 109可执行文件 options.add_argument("--disable-blink-features=AutomationControlled") # 绕过自动化特征检测 options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_experimental_option("excludeSwitches", ["enable-automation"]) # 关键!禁用自动化开关 options.add_experimental_option('useAutomationExtension', False) # 关键!禁用扩展加载 driver = webdriver.Chrome( executable_path="./chrome/chromedriver.exe", # 显式指定driver路径 options=options ) try: # 阶段1:CAS登录页加载与账号密码输入 driver.get("https://zdbk.zju.edu.cn/") wait = WebDriverWait(driver, 15) wait.until(EC.presence_of_element_located((By.ID, "username"))) # 等待CAS用户名框出现 driver.find_element(By.ID, "username").send_keys("YOUR_STUDENT_ID") driver.find_element(By.ID, "password").send_keys("YOUR_PASSWORD") # 阶段2:验证码识别与输入(调用chaojiying.py) code_img = driver.find_element(By.ID, "code-img") code_base64 = code_img.screenshot_as_base64 # 截取验证码图片 code_text = recognize_captcha(code_base64) # 调用超级鹰识别 driver.find_element(By.ID, "code").send_keys(code_text) # 阶段3:提交登录并等待跳转至健康填报页 driver.find_element(By.XPATH, "//button[@type='submit']").click() wait.until(EC.url_contains("health-report")) # 等待URL变为健康填报页 # 阶段4:表单填写与提交(重点处理动态生成的radio和textarea) # 示例:选择“今日体温正常”选项(实际XPath需根据页面DOM结构动态调整) temp_radio = driver.find_element(By.XPATH, "//label[contains(text(), '体温正常')]/preceding-sibling::input") temp_radio.click() # 填写健康承诺文本域 driver.find_element(By.NAME, "commitment").send_keys("本人承诺所填信息真实有效") # 提交按钮(注意:按钮ID在每次页面加载时动态生成,改用class+text定位) submit_btn = driver.find_element(By.XPATH, "//button[contains(@class, 'el-button') and contains(., '提交')]") submit_btn.click() print("✅ 打卡成功!") except Exception as e: driver.save_screenshot(f"error_{int(time.time())}.png") raise e finally: driver.quit() if __name__ == "__main__": main()

参数说明与逻辑说明:

  • --disable-blink-features=AutomationControlled:禁用 Blink 渲染引擎的自动化特征标记,这是绕过浙大前端 JS 检测的核心参数;漏掉此参数,页面会在document.readyState === 'complete'后立即执行window.navigator.webdriver = true检查并跳转拦截页。
  • excludeSwitches和useAutomationExtension:双重禁用 Chrome 自动化扩展加载,防止 Selenium 注入的cdc_开头的 WebDriver 对象被检测。
  • screenshot_as_base64:直接截取验证码<img>元素而非全屏,规避页面滚动或遮罩层干扰,提升 OCR 准确率。
  • 动态 XPath 定位://label[contains(text(), '体温正常')]/preceding-sibling::input利用标签文本反向定位关联的 radio input,比硬编码 ID 更鲁棒(浙大页面 ID 每次发布可能变更)。

2.3 验证码识别模块chaojiying.py:如何让 OCR 在 2 秒内返回结果

浙大健康打卡验证码为 4 位数字+字母组合,背景含噪点和干扰线。项目采用付费 API 服务“超级鹰”(ChaoJiYing),因其在教育类验证码场景下准确率稳定在 92%+(实测 100 次请求,87 次一次通过,11 次需重试,2 次失败)。chaojiying.py封装了标准调用流程:

# AutoClock-main/chaojiying.py import requests import base64 class ChaoJiYingClient: def __init__(self, username, password, soft_id): self.username = username self.password = base64.b64encode(password.encode()).decode() self.soft_id = soft_id self.base_url = "http://upload.chaojiying.net/Upload/Processing.php" def PostPic(self, im, codetype=1902): # codetype=1902 表示4位英文数字混合 """ im: base64编码的图片字符串 返回: {'err_no': 0, 'pic_str': 'abcd', 'pic_id': '123456'} """ params = { 'user': self.username, 'pass2': self.password, 'softid': self.soft_id, 'codetype': codetype, } data = {'pic': im} response = requests.post(self.base_url, data=data, params=params, timeout=10) return response.json() # 使用示例(在 daka.py 中调用) def recognize_captcha(base64_img): cjy = ChaoJiYingClient( username="your_cjy_username", # 需自行注册获取 password="your_cjy_password", soft_id="96001" # 超级鹰提供的软件ID,固定值 ) result = cjy.PostPic(base64_img) if result['err_no'] == 0: return result['pic_str'] else: raise Exception(f"验证码识别失败: {result['err_str']}")

关键配置说明:

  • soft_id=96001是超级鹰官方为“教育类系统”分配的专用ID,使用通用ID(如900001)会导致识别准确率下降至70%以下。
  • timeout=10:设置超时避免网络波动导致脚本卡死;实测平均响应时间 1.8 秒,99% 请求在 3 秒内返回。
  • 错误重试机制未内置在chaojiying.py中,需在daka.py主流程中捕获Exception并循环调用recognize_captcha()最多 3 次(项目README.md中有完整重试代码)。

3. 避坑指南:那些让你在凌晨三点对着控制台发呆的 Selenium 黑匣子

3.1 现象:Chrome 启动后立即闪退,控制台输出DevToolsActivePort file doesn't exist

原因:chromedriver.exe与本地 Chrome 109 可执行文件路径不匹配,或binary_location指向了错误目录(例如指向了chrome-win/chrome.exe但实际解压到了chrome/)。更隐蔽的情况是:Windows 系统中 Chrome 默认安装在C:\Program Files\Google\Chrome\Application\chrome.exe,而项目要求强制使用./chrome/chrome-win/chrome.exe(便携版),若未删除系统 Chrome 或未修改 PATH,webdriver.Chrome()会优先加载系统版导致版本错乱。
解决:

  1. 删除venv_daka\Scripts\下所有chrome*相关快捷方式;
  2. 在代码中显式指定executable_path和binary_location,绝不依赖 PATH 查找;
  3. 运行前执行taskkill /f /im chrome.exe强制结束所有 Chrome 进程。

3.2 现象:登录页输入账号密码后,点击登录按钮无反应,页面静止

原因:浙大 CAS 页面在 2023 年 Q4 启用了新的防机器人策略:要求用户在点击登录前,鼠标必须在页面内进行至少 200ms 的随机移动(模拟人类悬停)。Selenium 默认点击是瞬时触发,被判定为 bot 行为。
解决:在daka.py中登录按钮点击前插入模拟鼠标移动:

from selenium.webdriver.common.action_chains import ActionChains # 在 driver.find_element(By.XPATH, "//button[@type='submit']").click() 前添加: actions = ActionChains(driver) actions.move_by_offset(10, 10).perform() # 微小偏移 time.sleep(0.3) actions.move_by_offset(-10, -10).perform() # 回正 time.sleep(0.2)

3.3 现象:验证码识别返回pic_str为空,或返回乱码如x9q2(实际应为X9Q2)

原因:超级鹰 API 要求图片 base64 字符串必须以data:image/png;base64,开头,而screenshot_as_base64返回的是纯 base64 字符串(无前缀)。若直接传入,API 返回err_no=2001(图片格式错误)。
解决:在chaojiying.py的PostPic方法中,对im参数做预处理:

if not im.startswith('data:image/'): im = 'data:image/png;base64,' + im # 补上前缀

3.4 现象:表单提交后页面跳转至空白页,或提示“网络异常,请重试”

原因:浙大健康填报页使用 Vue.js 动态渲染,提交按钮绑定的是@click="submitForm"事件,而非传统<form>提交。直接click()无法触发 Vue 的响应式更新,需等待Vue.nextTick()完成后再提交。
解决:改用 JavaScript 执行原生点击,并注入等待逻辑:

# 替代 driver.find_element(...).click() submit_btn = driver.find_element(By.XPATH, "//button[contains(@class, 'el-button') and contains(., '提交')]") driver.execute_script("arguments[0].click();", submit_btn) # 等待 Vue 更新完成(实测需 800ms) time.sleep(0.8)

3.5 现象:脚本在 Linux 服务器后台运行时,WebDriverException: unknown error: Chrome failed to start

原因:Linux 无图形界面,Chrome 需启用--headless=new模式,但浙大页面的 anti-bot 检测会拒绝 headless 模式访问。必须使用 Xvfb 虚拟帧缓冲。
解决:

  1. 安装 Xvfb:sudo apt-get install xvfb;
  2. 启动虚拟显示:Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &;
  3. 在daka.py的ChromeOptions中添加:options.add_argument("--display=:99");
  4. 关键:--headless参数必须移除,否则仍被拦截。

4. 钉钉消息推送与失败告警:让自动化真正“无人值守”

4.1DingRobot.py的 Webhook 签名机制解析

浙大打卡不能只靠成功,更要靠失败告警。项目集成钉钉机器人,但钉钉 Webhook 要求时间戳+密钥签名,否则返回400 Bad Request。DingRobot.py封装了标准签名流程:

# AutoClock-main/DingRobot.py import hmac import base64 import hashlib import time import requests class DingTalkRobot: def __init__(self, webhook_url, secret): self.webhook_url = webhook_url self.secret = secret def _gen_sign(self): """生成钉钉Webhook签名""" timestamp = str(round(time.time() * 1000)) string_to_sign = f'{timestamp}\n{self.secret}' hmac_code = hmac.new( self.secret.encode('utf-8'), string_to_sign.encode('utf-8'), digestmod=hashlib.sha256 ).digest() sign = base64.b64encode(hmac_code).decode('utf-8') return timestamp, sign def send_text(self, content): """发送文本消息""" timestamp, sign = self._gen_sign() params = { 'sign': sign, 'timestamp': timestamp } data = { "msgtype": "text", "text": {"content": content} } response = requests.post( self.webhook_url, params=params, json=data, timeout=5 ) return response.json() # 使用示例 robot = DingTalkRobot( webhook_url="https://oapi.dingtalk.com/robot/send?access_token=xxx", # 钉钉群机器人token secret="SECxxx" # 钉钉群机器人密钥 ) robot.send_text("【浙大健康打卡】2024-06-15 打卡成功!✅")

签名逻辑说明:

  • timestamp必须是毫秒级整数(round(time.time() * 1000)),误差超过 1 小时即失效;
  • string_to_sign格式为"1678886400000\nSECxxx"(时间戳+换行+密钥),顺序错误会导致签名无效;
  • hmac.new()的digestmod必须为hashlib.sha256,钉钉文档明确要求 SHA256 签名。

4.2 失败告警的三级触发策略:截图 → 日志 → 钉钉通知

daka.py的except Exception as e块不是简单打印错误,而是执行原子化告警:

except Exception as e: # 步骤1:保存当前页面全屏截图(含错误上下文) timestamp = int(time.time()) driver.save_screenshot(f"screenshots/error_{timestamp}.png") # 步骤2:记录详细日志(含URL、page_source片段、异常类型) with open("logs/daka_error.log", "a") as f: f.write(f"\n=== {time.ctime()} ===\n") f.write(f"URL: {driver.current_url}\n") f.write(f"Page Title: {driver.title}\n") f.write(f"Exception: {type(e).__name__}: {str(e)}\n") # 截取page_source前500字符,避免日志过大 f.write(f"Page Snippet: {driver.page_source[:500]}...\n") # 步骤3:发送钉钉告警(附截图URL,需提前将screenshots目录映射为Web可访问路径) try: robot.send_text( f"🚨 浙大打卡失败!\n时间:{time.ctime()}\n错误:{type(e).__name__}\n截图:http://your-server-ip/screenshots/error_{timestamp}.png" ) except: pass # 钉钉发送失败不阻塞主流程 raise e # 重新抛出异常,便于crontab判断执行状态

注意:screenshots/目录需通过 Nginx/Apache 暴露为静态资源(如http://localhost/screenshots/),否则钉钉消息中的截图链接无法访问。项目README.md提供了 Nginx 配置片段。

5. 进阶技巧:从“能跑通”到“可维护”的工程化改造

5.1 配置文件分离:用config.yaml管理所有可变参数

硬编码账号密码、钉钉 token、超级鹰凭证是工程大忌。项目虽提供project_authorization.txt,但最佳实践是抽象为config.yaml:

# config.yaml credentials: student_id: "3190100000" password: "your_zju_password" captcha: chaojiying: username: "cjy_user" password: "cjy_pass" soft_id: "96001" dingtalk: webhook_url: "https://oapi.dingtalk.com/robot/send?access_token=xxx" secret: "SECxxx" browser: chrome_binary: "./chrome/chrome-win/chrome.exe" chromedriver_path: "./chrome/chromedriver.exe" timeout: 15

读取配置的工具函数:

# utils/config_loader.py import yaml def load_config(config_path="config.yaml"): with open(config_path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) # 在 daka.py 中使用 config = load_config() driver.get("https://zdbk.zju.edu.cn/") driver.find_element(By.ID, "username").send_keys(config['credentials']['student_id'])

优势:

  • .gitignore可忽略config.yaml,避免密钥泄露;
  • 多环境切换只需复制config-prod.yaml/config-dev.yaml;
  • config.yaml支持中文注释,方便导师/同学快速理解参数含义。

5.2 日志分级与归档:用logging替代print

原始daka.py用print输出,无法区分 INFO/WARN/ERROR,且不自动轮转。改造为标准 logging:

import logging from logging.handlers import RotatingFileHandler def setup_logger(): logger = logging.getLogger("daka") logger.setLevel(logging.DEBUG) # 控制台输出(仅INFO及以上) console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') console_handler.setFormatter(console_formatter) # 文件输出(DEBUG及以上,自动轮转) file_handler = RotatingFileHandler( "logs/daka.log", maxBytes=10*1024*1024, # 10MB backupCount=5 ) file_handler.setLevel(logging.DEBUG) file_formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s' ) file_handler.setFormatter(file_formatter) logger.addHandler(console_handler) logger.addHandler(file_handler) return logger # 使用 logger = setup_logger() logger.info("开始执行健康打卡") logger.debug(f"当前URL: {driver.current_url}") logger.error("验证码识别失败", exc_info=True)

效果:

  • daka.log文件按大小轮转,保留最近 5 个历史文件;
  • exc_info=True自动记录异常堆栈,无需手动traceback.print_exc();
  • funcName和lineno精确定位到出错代码行,比print("error at line 42")更可靠。

5.3 定时任务集成:用systemd替代crontab实现开机自启

crontab在 Ubuntu 22.04+ 上对 GUI 程序支持不佳,且无法监控进程状态。推荐systemd服务:

# /etc/systemd/system/zju-daka.service [Unit] Description=ZJU Health Check-in Service After=network.target [Service] Type=simple User=your_username WorkingDirectory=/home/your_username/AutoClock-main ExecStart=/home/your_username/venv_daka/bin/python /home/your_username/AutoClock-main/daka.py Restart=on-failure RestartSec=30 Environment="DISPLAY=:99" # 匹配Xvfb显示号 Environment="XAUTHORITY=/home/your_username/.Xauthority" [Install] WantedBy=multi-user.target

启用服务:

sudo systemctl daemon-reload sudo systemctl enable zju-daka.service sudo systemctl start zju-daka.service # 查看日志:sudo journalctl -u zju-daka -f

关键点:

  • Restart=on-failure确保打卡失败后 30 秒自动重试;
  • Environment显式声明DISPLAY和XAUTHORITY,解决 Xvfb 权限问题;
  • journalctl日志可被 ELK 收集,实现集中监控。

从那以后我每次部署新环境,都强制走一遍systemd服务注册 +journalctl实时盯屏 5 分钟的流程——不是为了炫技,而是因为去年某次 crontab 的 PATH 错误,让我连续三天的打卡日志里只有command not found: python这一行。希望帮到你。

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

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

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

立即咨询