软考成绩自动查询小助手:Python + Selenium 实现定时监控
软考成绩自动查询小助手:Python + Selenium 实现定时监控
软考成绩自动查询小助手:Python + Selenium 实现定时监控
还在每天手动刷新软考官网看成绩出了没?不如写个脚本替你盯着,一旦发布立刻通知你!
本文将分享一个基于 Python + Selenium 的自动化查询工具,并重点讲解如何对敏感信息进行安全处理。
📌 背景与痛点
每年软考(计算机技术与软件专业技术资格)成绩公布时,考生们总忍不住反复登录官网刷新页面,既耗费精力又容易错过第一时间。
其实,这种重复性工作完全可以用自动化脚本代替——只要我们能让脚本自动登录、定时查询,并在检测到成绩时及时提醒即可。
本脚本正是为此而生,它使用 Selenium 模拟浏览器操作,配合 schedule 库实现定时循环查询,一旦发现“成绩已发布”或“合格”字样,就会在日志中高亮提示。
🛠️ 技术栈与核心依赖
- Python 3.7+
- Selenium – 浏览器自动化驱动
- Edge WebDriver – 与 Microsoft Edge 浏览器配合(亦可改用 Chrome)
- schedule – 轻量级定时任务调度
- logging – 日志记录,方便追踪运行状态
安装命令:
pip install selenium schedule
同时请确保下载与本地 Edge 版本匹配的 Edge WebDriver,并将其所在目录加入系统 PATH。
🔐 敏感性处理:如何保护账号密码与本地路径
原代码中直接硬编码了用户名、密码和本地用户数据目录,这存在严重的安全隐患(例如代码泄露或版本控制意外提交)。
改进方案:将敏感信息移至环境变量或外部配置文件,并在代码中动态读取。
1. 使用环境变量(推荐)
在系统或虚拟环境中设置:
export RUANKAO_USERNAME="your_id"
export RUANKAO_PASSWORD="your_password"
export RUANKAO_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 并确保可执行。
- 设置环境变量(或直接在代码中临时替换):
export RUANKAO_USERNAME="你的证件号" export RUANKAO_PASSWORD="你的密码" export RUANKAO_USER_DATA_DIR="./edge_profile" # 任意本地路径 - 运行脚本:
python query_ruankao.py - 首次运行会打开浏览器,若未登录,则脚本会停留在滑动验证码处,手动完成滑动后按回车继续。
- 之后脚本会定时自动查询,日志会输出每次的结果。
⚠️ 注意事项
- 验证码处理:每次登录都可能需要手动滑动,若会话保持良好(通过
user-data-dir),则后续无需重复登录。 - 页面结构变化:软考官网可能会更新 HTML 结构,届时需调整 CSS 选择器或 XPath。
- 查询间隔:建议不要过于频繁(至少 1 分钟),避免给服务器造成压力。
- 运行环境:需有图形界面(Windows / Linux with X11 / macOS),因为 Selenium 需要真实浏览器。
- 隐私安全:切勿将包含真实账号的代码上传至公开仓库。
📝 脱敏后的完整代码
以下代码已将所有敏感信息替换为环境变量读取,并添加了详细的注释。你可以直接复制使用,只需正确设置环境变量即可。
import os
import logging
import time
import schedule
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException, TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.edge.options import Options
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
# ---------- 从环境变量读取敏感配置 ----------
USERNAME = os.environ.get("RUANKAO_USERNAME")
PASSWORD = os.environ.get("RUANKAO_PASSWORD")
USER_DATA_DIR = os.environ.get("RUANKAO_USER_DATA_DIR", "./edge_profile") # 默认相对路径
if not USERNAME or not PASSWORD:
raise ValueError("请设置环境变量 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")
def get_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})")
return driver
def is_logged_in(driver):
"""检测是否已登录(通过检查当前URL或页面元素)"""
current_url = driver.current_url
if "sign/in" in current_url:
return False
try:
# 查找用户头像菜单,如果存在则认为已登录
driver.find_element(By.CSS_SELECTOR, ".layui-nav-item img.layui-nav-img")
return True
except NoSuchElementException:
return False
def login(driver):
"""执行登录(若已登录则跳过)"""
logging.info(f"正在访问登录页: {LOGIN_URL}")
driver.get(LOGIN_URL)
time.sleep(2)
if not is_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)
except TimeoutException:
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("账号密码已填写")
except NoSuchElementException as e:
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)
except NoSuchElementException:
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("已点击登录按钮")
except NoSuchElementException as e:
logging.error(f"找不到登录按钮: {e}")
raise
time.sleep(5)
if "sign/in" in driver.current_url:
logging.error("登录失败,请检查账号密码或验证码")
return False
else:
logging.info("登录成功")
return True
else:
logging.info("检测到已登录,无需重复登录")
return True
def query_score(driver):
"""访问成绩查询页面,处理考区列表页的跳转"""
logging.info(f"正在访问成绩查询页面: {QUERY_URL}")
driver.get(QUERY_URL)
time.sleep(5)
# 检查是否进入考区列表页
page_source = driver.page_source
if "机构名称" in page_source and "报名有效时间" in page_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_source
except Exception as e:
logging.error(f"点击'浙江'进入按钮失败: {e}")
return False
else:
logging.info("当前页面不是考区列表页,直接检查成绩状态")
# 检查成绩状态
if "成绩暂未发布" in page_source:
logging.info("⏳ 成绩暂未发布,继续等待...")
return False
elif "合格" in page_source or "分数" in page_source or "成绩" in page_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("请手动查看浏览器中的成绩页面。")
return True
else:
if "sign/in" in driver.current_url:
logging.warning("会话过期,需要重新登录")
return None
else:
logging.info("未知状态,可能页面未完全加载或结构已变化")
return False
def job(driver):
"""定时任务"""
logging.info("========== 执行定时查询 ==========")
try:
if "sign/in" in driver.current_url:
logging.warning("检测到在登录页面,尝试重新登录...")
if not login(driver):
logging.error("重新登录失败,停止任务")
return
result = query_score(driver)
if result is None:
logging.warning("会话过期,尝试重新登录...")
if login(driver):
query_score(driver)
else:
logging.error("重新登录失败")
except Exception as e:
logging.error(f"查询任务异常: {e}")
if __name__ == "__main__":
driver = get_driver()
try:
if not login(driver):
logging.error("首次登录失败,退出")
driver.quit()
exit()
query_score(driver)
schedule.every(CHECK_INTERVAL_MINUTES).minutes.do(job, driver)
logging.info(f"定时任务已启动,每 {CHECK_INTERVAL_MINUTES} 分钟查询一次")
while True:
schedule.run_pending()
time.sleep(1)
except KeyboardInterrupt:
logging.info("用户中断,关闭浏览器")
finally:
driver.quit()
📢 最后的话
这个脚本已经在我自己的环境中稳定运行了几天,完美实现了“无人值守,成绩秒知”的目标。当然,它也存在一些局限性(如依赖浏览器界面、验证码需人工),但作为一个辅助工具已经足够实用。
如果你也正在等待软考成绩,不妨一试。记得保护好你的账号信息,用环境变量或配置文件隔离敏感数据。
如果你有更好的改进建议(例如接入邮件/微信通知),欢迎在评论区交流!
Happy Coding & Good Luck! 🍀
更多推荐

所有评论(0)