软考成绩自动查询小助手:Python + Selenium 实现定时监控
软考成绩自动查询小助手:Python + Selenium 实现定时监控
- 软考成绩自动查询小助手:Python + Selenium 实现定时监控
- 📌 背景与痛点
- 🛠️ 技术栈与核心依赖
- 🔐 敏感性处理:如何保护账号密码与本地路径
- 1. 使用环境变量(推荐)
- 2. 使用配置文件(如 `.env` 或 `config.ini`)
- 🧩 功能模块详解
- 1. 浏览器驱动初始化(`get_driver`)
- 2. 登录流程(`login`)
- 3. 成绩查询(`query_score`)
- 4. 定时任务(`job`)
- 5. 主程序
- 🚀 使用步骤
- ⚠️ 注意事项
- 📝 脱敏后的完整代码
- 📢 最后的话
软考成绩自动查询小助手:Python + Selenium 实现定时监控
还在每天手动刷新软考官网看成绩出了没?不如写个脚本替你盯着,一旦发布立刻通知你!
本文将分享一个基于 Python + Selenium 的自动化查询工具,并重点讲解如何对敏感信息进行安全处理。
📌 背景与痛点
每年软考(计算机技术与软件专业技术资格)成绩公布时,考生们总忍不住反复登录官网刷新页面,既耗费精力又容易错过第一时间。
其实,这种重复性工作完全可以用自动化脚本代替——只要我们能让脚本自动登录、定时查询,并在检测到成绩时及时提醒即可。
本脚本正是为此而生,它使用Selenium模拟浏览器操作,配合schedule库实现定时循环查询,一旦发现“成绩已发布”或“合格”字样,就会在日志中高亮提示。
🛠️ 技术栈与核心依赖
- Python 3.7+
- Selenium– 浏览器自动化驱动
- Edge WebDriver– 与 Microsoft Edge 浏览器配合(亦可改用 Chrome)
- schedule– 轻量级定时任务调度
- logging– 日志记录,方便追踪运行状态
安装命令:
pipinstallselenium schedule同时请确保下载与本地 Edge 版本匹配的 Edge WebDriver,并将其所在目录加入系统 PATH。
🔐 敏感性处理:如何保护账号密码与本地路径
原代码中直接硬编码了用户名、密码和本地用户数据目录,这存在严重的安全隐患(例如代码泄露或版本控制意外提交)。
改进方案:将敏感信息移至环境变量或外部配置文件,并在代码中动态读取。
1. 使用环境变量(推荐)
在系统或虚拟环境中设置:
exportRUANKAO_USERNAME="your_id"exportRUANKAO_PASSWORD="your_password"exportRUANKAO_USER_DATA_DIR="/path/to/profile"然后在 Python 中通过os.environ.get()获取。
2. 使用配置文件(如.env或config.ini)
若不想设置环境变量,也可用configparser读取本地配置文件(需将配置文件加入.gitignore)。
本文示例将采用环境变量方式,既简单又安全。
🧩 功能模块详解
1. 浏览器驱动初始化(get_driver)
- 加载 Edge 选项,关闭自动化控制特征(防止被网站识别为爬虫)。
- 启用用户数据目录(
user-data-dir),用于持久化保存登录会话和 Cookie,避免每次重复登录。 - 执行 JS 隐藏
navigator.webdriver属性,进一步降低检测风险。
2. 登录流程(login)
- 访问登录页,检测当前是否已登录(通过判断 URL 或页面元素)。
- 若未登录,则:
- 点击“切换账号密码登录”(因为默认可能为扫码)。
- 输入用户名和密码。
- 触发滑动验证码(
div.tncode),此时脚本暂停并等待用户手动滑动。 - 用户完成后按回车继续,点击登录按钮。
- 登录成功后,后续查询可直接复用会话。
⚠️为何验证码必须手动?
滑动验证码通常包含复杂的行为轨迹分析,自动破解成本高且违反网站条款,故采用“半自动”方式——脚本触发验证,人工完成,既合规又稳定。
3. 成绩查询(query_score)
- 访问成绩查询页。
- 若页面显示“考区列表”,则自动点击“浙江”(可根据需要修改)进入具体考区。
- 检查页面源码中是否包含“成绩暂未发布”、“合格”或“分数”等关键词。
- 若找到成绩信息,则记录日志并返回成功标志;否则返回
False或None(表示会话过期)。
4. 定时任务(job)
- 每次执行前检查是否处于登录状态,若掉线则自动重登。
- 调用
query_score并根据返回值决定是否重试。
5. 主程序
- 首次启动时先登录。
- 立即执行一次查询。
- 通过
schedule.every(1).minutes.do(job, driver)设置固定间隔(本文设为 1 分钟,可调整)。 - 保持主循环运行,直到用户按
Ctrl+C退出。
🚀 使用步骤
- 克隆/创建脚本文件,将下方脱敏后的代码保存为
query_ruankao.py。 - 安装依赖:
pip install selenium schedule - 下载 Edge WebDriver并确保可执行。
- 设置环境变量(或直接在代码中临时替换):
exportRUANKAO_USERNAME="你的证件号"exportRUANKAO_PASSWORD="你的密码"exportRUANKAO_USER_DATA_DIR="./edge_profile"# 任意本地路径 - 运行脚本:
python query_ruankao.py - 首次运行会打开浏览器,若未登录,则脚本会停留在滑动验证码处,手动完成滑动后按回车继续。
- 之后脚本会定时自动查询,日志会输出每次的结果。
⚠️ 注意事项
- 验证码处理:每次登录都可能需要手动滑动,若会话保持良好(通过
user-data-dir),则后续无需重复登录。 - 页面结构变化:软考官网可能会更新 HTML 结构,届时需调整 CSS 选择器或 XPath。
- 查询间隔:建议不要过于频繁(至少 1 分钟),避免给服务器造成压力。
- 运行环境:需有图形界面(Windows / Linux with X11 / macOS),因为 Selenium 需要真实浏览器。
- 隐私安全:切勿将包含真实账号的代码上传至公开仓库。
📝 脱敏后的完整代码
以下代码已将所有敏感信息替换为环境变量读取,并添加了详细的注释。你可以直接复制使用,只需正确设置环境变量即可。
importosimportloggingimporttimeimportschedulefromseleniumimportwebdriverfromselenium.common.exceptionsimportNoSuchElementException,TimeoutExceptionfromselenium.webdriver.common.byimportByfromselenium.webdriver.edge.optionsimportOptionsfromselenium.webdriver.supportimportexpected_conditionsasECfromselenium.webdriver.support.uiimportWebDriverWait# ---------- 从环境变量读取敏感配置 ----------USERNAME=os.environ.get("RUANKAO_USERNAME")PASSWORD=os.environ.get("RUANKAO_PASSWORD")USER_DATA_DIR=os.environ.get("RUANKAO_USER_DATA_DIR","./edge_profile")# 默认相对路径ifnotUSERNAMEornotPASSWORD:raiseValueError("请设置环境变量 RUANKAO_USERNAME 和 RUANKAO_PASSWORD")LOGIN_URL="https://bm.ruankao.org.cn/sign/in"QUERY_URL="https://bm.ruankao.org.cn/report/query"CHECK_INTERVAL_MINUTES=1# 查询间隔(分钟)logging.basicConfig(level=logging.INFO,format="%(asctime)s - %(levelname)s - %(message)s")defget_driver():"""启动 Edge 浏览器,使用持久化用户目录"""options=Options()options.add_argument("--disable-gpu")options.add_argument("--no-sandbox")options.add_argument("--disable-blink-features=AutomationControlled")options.add_experimental_option("excludeSwitches",["enable-automation"])options.add_experimental_option("useAutomationExtension",False)options.add_argument("--start-maximized")options.add_argument(f"--user-data-dir={USER_DATA_DIR}")driver=webdriver.Edge(options=options)# 隐藏自动化特征driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")returndriverdefis_logged_in(driver):"""检测是否已登录(通过检查当前URL或页面元素)"""current_url=driver.current_urlif"sign/in"incurrent_url:returnFalsetry:# 查找用户头像菜单,如果存在则认为已登录driver.find_element(By.CSS_SELECTOR,".layui-nav-item img.layui-nav-img")returnTrueexceptNoSuchElementException:returnFalsedeflogin(driver):"""执行登录(若已登录则跳过)"""logging.info(f"正在访问登录页:{LOGIN_URL}")driver.get(LOGIN_URL)time.sleep(2)ifnotis_logged_in(driver):logging.info("未登录,开始执行登录流程...")wait=WebDriverWait(driver,10)# 1. 切换到账号密码登录try:change_btn=wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,"a.change_login")))driver.execute_script("arguments[0].click();",change_btn)logging.info("已切换到账号密码登录")time.sleep(1.5)exceptTimeoutException:logging.info("未找到切换按钮,可能已经在账号密码模式")# 2. 填写账号和密码try:username_input=wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,".layui-input.username")))password_input=driver.find_element(By.CSS_SELECTOR,".layui-input.password.pwd1")username_input.clear()username_input.send_keys(USERNAME)password_input.clear()password_input.send_keys(PASSWORD)logging.info("账号密码已填写")exceptNoSuchElementExceptionase:logging.error(f"找不到账号或密码输入框:{e}")raise# 3. 处理滑动验证码(需人工干预)try:tncode_div=driver.find_element(By.CSS_SELECTOR,"div.tncode")driver.execute_script("arguments[0].click();",tncode_div)logging.info("已触发滑动验证码,请在浏览器中手动完成滑动")input("完成滑动验证后,在此按回车继续...")time.sleep(1)exceptNoSuchElementException:logging.info("未找到滑动验证码元素,可能无需验证")# 4. 点击登录按钮try:submit_btn=driver.find_element(By.CSS_SELECTOR,".layui-btn.loginBtn")driver.execute_script("arguments[0].click();",submit_btn)logging.info("已点击登录按钮")exceptNoSuchElementExceptionase:logging.error(f"找不到登录按钮:{e}")raisetime.sleep(5)if"sign/in"indriver.current_url:logging.error("登录失败,请检查账号密码或验证码")returnFalseelse:logging.info("登录成功")returnTrueelse:logging.info("检测到已登录,无需重复登录")returnTruedefquery_score(driver):"""访问成绩查询页面,处理考区列表页的跳转"""logging.info(f"正在访问成绩查询页面:{QUERY_URL}")driver.get(QUERY_URL)time.sleep(5)# 检查是否进入考区列表页page_source=driver.page_sourceif"机构名称"inpage_sourceand"报名有效时间"inpage_source:logging.warning("检测到考区列表页,尝试点击'浙江'进入...")try:# 定位“浙江”对应的“进入”按钮zhejiang_entry=driver.find_element(By.XPATH,"//td[contains(text(),'浙江')]/following-sibling::td/a[contains(text(),'进入')]")driver.execute_script("arguments[0].click();",zhejiang_entry)logging.info("已点击'浙江'的'进入'按钮")time.sleep(3)page_source=driver.page_sourceexceptExceptionase:logging.error(f"点击'浙江'进入按钮失败:{e}")returnFalseelse:logging.info("当前页面不是考区列表页,直接检查成绩状态")# 检查成绩状态if"成绩暂未发布"inpage_source:logging.info("⏳ 成绩暂未发布,继续等待...")returnFalseelif"合格"inpage_sourceor"分数"inpage_sourceor"成绩"inpage_source:logging.info("🎉 成绩已发布!")try:score_element=driver.find_element(By.CSS_SELECTOR,".score-result")score_text=score_element.text logging.info(f"成绩详情:{score_text}")except:logging.info("请手动查看浏览器中的成绩页面。")returnTrueelse:if"sign/in"indriver.current_url:logging.warning("会话过期,需要重新登录")returnNoneelse:logging.info("未知状态,可能页面未完全加载或结构已变化")returnFalsedefjob(driver):"""定时任务"""logging.info("========== 执行定时查询 ==========")try:if"sign/in"indriver.current_url:logging.warning("检测到在登录页面,尝试重新登录...")ifnotlogin(driver):logging.error("重新登录失败,停止任务")returnresult=query_score(driver)ifresultisNone:logging.warning("会话过期,尝试重新登录...")iflogin(driver):query_score(driver)else:logging.error("重新登录失败")exceptExceptionase:logging.error(f"查询任务异常:{e}")if__name__=="__main__":driver=get_driver()try:ifnotlogin(driver):logging.error("首次登录失败,退出")driver.quit()exit()query_score(driver)schedule.every(CHECK_INTERVAL_MINUTES).minutes.do(job,driver)logging.info(f"定时任务已启动,每{CHECK_INTERVAL_MINUTES}分钟查询一次")whileTrue:schedule.run_pending()time.sleep(1)exceptKeyboardInterrupt:logging.info("用户中断,关闭浏览器")finally:driver.quit()📢 最后的话
这个脚本已经在我自己的环境中稳定运行了几天,完美实现了“无人值守,成绩秒知”的目标。当然,它也存在一些局限性(如依赖浏览器界面、验证码需人工),但作为一个辅助工具已经足够实用。
如果你也正在等待软考成绩,不妨一试。记得保护好你的账号信息,用环境变量或配置文件隔离敏感数据。
如果你有更好的改进建议(例如接入邮件/微信通知),欢迎在评论区交流!
Happy Coding & Good Luck!🍀
