1. 项目缘起:从手动点击到自动化评估
每到学期末,最让人头疼的事情之一就是登录学校的URP教务系统,给各门课程的老师完成教学评估,然后才能查看自己的期末成绩。这个流程本身不复杂,但架不住它步骤繁琐、网络拥堵,而且必须在规定时间内完成。手动操作时,你得一遍遍刷新登录页面,忍受验证码的折磨,然后机械地点击几十个“非常满意”的选项,最后才能看到那个心心念念的成绩单。这个过程不仅耗时,还容易因为网络卡顿或误操作而中断,体验极差。
于是,一个很自然的想法就冒出来了:能不能让程序自动完成这一切?用脚本模拟人的操作,自动登录、自动完成评估、自动获取并保存成绩。这不仅能解放双手,还能确保流程的稳定性和可重复性。这就是我们这次要聊的项目:利用Selenium这个强大的浏览器自动化工具,实现URP教务系统的全流程自动化操作。Selenium的核心价值在于它能像真人一样驱动浏览器,点击、输入、下拉、跳转,所有网页交互都能被精准控制,是处理这类基于Web的、有固定流程的重复性任务的绝佳选择。
这个项目适合所有被类似流程困扰的学生,也适合对Python自动化、网页爬虫(在合规范围内)感兴趣的朋友。它不涉及破解或攻击系统,只是将你手动操作的步骤用代码复现一遍,本质上是一种“机器人流程自动化”(RPA)。通过这个项目,你不仅能学会Selenium的基本操作,更能掌握一套解决实际问题的自动化思维:如何分析网页结构、如何定位元素、如何处理登录验证、如何应对页面跳转和加载等待。接下来,我们就一步步拆解,看看如何把这个想法变成现实。
2. 核心工具链选型与环境搭建
工欲善其事,必先利其器。要实现URP系统的自动化,我们需要一套稳定、可靠的工具链。核心自然是Selenium,但围绕它的一系列配套工具和库的选择,直接决定了脚本的健壮性和开发效率。
2.1 为什么是Selenium WebDriver?
市面上自动化工具不少,比如Playwright、Puppeteer等后起之秀也很强大。但在这个场景下选择Selenium,有几点核心考量:
- 生态成熟与社区支持:Selenium是历史最悠久、社区最庞大的Web自动化框架之一。这意味着你在遇到任何稀奇古怪的问题时,几乎都能在Stack Overflow或中文技术社区找到解决方案。URP系统这类国内高校广泛使用的系统,其前端技术栈(如古老的jQuery、复杂的表单)可能比较“独特”,Selenium庞大的用户基数意味着更高的踩坑概率和更丰富的填坑经验。
- 多语言支持:Selenium支持Java、Python、C#等多种语言。我们选择Python,是因为其语法简洁,库生态丰富,特别适合快速开发和脚本编写。用Python+Selenium的组合,可以让我们更专注于业务逻辑,而非语言细节。
- 对真实浏览器的完美模拟:Selenium WebDriver通过浏览器厂商提供的原生驱动(如ChromeDriver)来直接控制浏览器。这意味着脚本运行时,就是一个真实的、完整的浏览器实例在运行。这对于需要处理JavaScript渲染、Cookie会话、甚至是一些简单图形验证码(后续会讨论策略)的场景至关重要。URP系统的登录和评估页面充满了动态加载的元素和表单提交,一个无头浏览器或简单的HTTP请求库(如requests)很难完美模拟。
2.2 环境搭建详细步骤
这里我们以Windows系统、Chrome浏览器为例,给出最稳妥的搭建流程。
第一步:安装Python确保你的电脑上安装了Python 3.7或更高版本。可以从Python官网下载安装包,安装时务必勾选“Add Python to PATH”,这样可以在命令行中直接使用python和pip命令。
第二步:安装Selenium库打开命令行(CMD或PowerShell),输入以下命令:
pip install selenium这条命令会从PyPI(Python包索引)下载并安装最新稳定版的Selenium库。
第三步:下载与浏览器版本匹配的WebDriver这是最容易出错的一步。Selenium本身只是一个控制指令的发送者,它需要对应浏览器的“驱动程序”(WebDriver)来实际操控浏览器。
- 打开你的Chrome浏览器,在地址栏输入
chrome://settings/help,查看你的Chrome版本号(例如:版本 128.0.6613.138)。 - 访问ChromeDriver的官方下载站点(通常搜索“ChromeDriver下载”即可找到)。关键点来了:你必须下载与你的Chrome浏览器主版本号完全一致的ChromeDriver。例如,Chrome是128.x.x.x,你就必须下载128.x.x.x系列的ChromeDriver。版本不匹配会导致Selenium无法启动浏览器或运行时崩溃。
- 下载对应版本的
chromedriver.exe文件。
第四步:配置WebDriver路径下载的chromedriver.exe需要让Selenium能找到。有两种常用方法:
- 方法A:添加到系统PATH:将
chromedriver.exe文件放在一个固定的目录(例如C:\WebDriver\),然后将这个目录路径添加到系统的环境变量PATH中。这样,Selenium可以在任何位置启动它。 - 方法B:在代码中指定路径(推荐):对于单项目脚本,更简单直接的方法是在Python代码中指定驱动文件的绝对路径。这样避免了环境配置的麻烦,项目迁移也更方便。
注意:很多新手在这里会卡住,提示“
'chromedriver' executable needs to be in PATH”。如果遇到,请严格按照上述步骤检查浏览器版本和驱动版本是否一致,以及路径是否正确。
第五步:验证安装创建一个简单的Python脚本test_selenium.py:
from selenium import webdriver from selenium.webdriver.chrome.service import Service # 指定你的chromedriver.exe的绝对路径 driver_path = r'C:\path\to\your\chromedriver.exe' service = Service(executable_path=driver_path) # 创建浏览器驱动对象 driver = webdriver.Chrome(service=service) # 打开百度首页 driver.get("https://www.baidu.com") print(driver.title) # 应该打印出“百度一下,你就知道” # 等待几秒查看效果 import time time.sleep(3) # 关闭浏览器 driver.quit()运行这个脚本,如果成功弹出一个Chrome浏览器窗口并打开了百度页面,控制台打印出标题,那么恭喜你,Selenium环境搭建成功。
3. 逆向工程:手动操作步骤的代码化拆解
在动手写代码之前,我们必须像侦探一样,把整个手动操作流程“逆向工程”,弄清楚每一个步骤背后对应的网页操作是什么。这是自动化成功与否的关键,盲目写代码只会陷入无尽的调试泥潭。
3.1 流程步骤映射
我们先梳理出从打开浏览器到打印成绩的完整手动步骤,并思考其代码实现:
- 打开URP系统登录页:手动打开浏览器,输入网址。代码对应
driver.get(“登录页URL”)。 - 输入用户名和密码:手动在输入框点击并打字。代码需要先定位到用户名和密码的输入框元素,然后使用
send_keys()方法输入文本。 - 处理验证码(如果存在):这是最大的变数和难点。URP系统可能使用数字、字母、算式或滑动验证码。我们需要观察其类型。如果是简单的数字字母图片,可以考虑(在合规前提下)手动识别一次并输入,或者研究其验证码接口是否有规律(但通常没有)。更务实的做法是:在脚本中设置暂停,等待用户手动输入验证码。这虽然牺牲了部分自动化程度,但保证了脚本的100%可行性和安全性,避免了触碰复杂的图像识别或逆向工程。
- 点击登录按钮:定位登录按钮元素,执行
click()操作。 - 等待页面跳转并进入评估模块:登录成功后,页面会跳转到学生主页。我们需要从主页的菜单或链接中找到“教学评估”或类似的入口。这需要分析跳转后的页面HTML结构,定位到那个特定的链接并点击。
- 遍历所有待评估课程并完成评估:
- 进入评估页面:点击评估入口后,通常会进入一个列表页,列出所有本学期需要评估的课程。
- 逐门课程评估:对列表中的每一门课程,点击“评估”或“进入评估”按钮。
- 填写评估表单:评估页面通常是一个有多项选择题(如“非常满意”、“满意”等)的表单。我们需要定位到每一个单选按钮(通常是
<input type=”radio”>),并选中对应的选项(如第一个选项“非常满意”)。这里可能涉及遍历多个问题。 - 提交单门课程评估:填写完毕后,定位并点击“提交”按钮。提交后页面可能会刷新或弹窗提示,需要代码能处理这些反馈。
- 返回课程列表:提交一门后,通常需要返回课程列表页面,继续评估下一门。这里要注意页面跳转的逻辑,是开新窗口还是原页面刷新。
- 所有课程评估完成后,进入成绩查询页面:评估全部完成后,才能解锁成绩查询功能。从主导航栏找到“成绩查询”或“学生成绩”入口,点击进入。
- 选择查询学期并获取成绩列表:在成绩查询页面,可能需要选择学期(如“2023-2024学年 第2学期”),然后点击“查询”。页面会以表格形式展示所有课程的成绩。
- 定位并提取成绩数据:这是数据抓取环节。我们需要分析成绩表格的HTML结构(使用浏览器的开发者工具),定位到
<table>标签,然后逐行(<tr>)逐列(<td>)地提取课程名称、成绩、学分等信息。 - 格式化并打印/保存成绩:将提取到的数据整理成易读的格式(如列表、字典),然后在控制台打印出来,或者保存为CSV、Excel文件,方便后续查看和分析。
- 安全退出系统:所有操作完成后,最好点击“退出登录”链接,然后调用
driver.quit()关闭浏览器,释放资源。
3.2 关键难点与应对策略
- 页面加载等待:网络速度和服务器响应会导致页面元素加载有延迟。如果代码在元素还没出现时就尝试去点击或输入,会抛出
NoSuchElementException。必须使用“显式等待”。Selenium提供了WebDriverWait和expected_conditions模块,可以让代码智能地等待某个元素出现、可点击或可见后再执行操作,而不是用固定的time.sleep()傻等。 - 元素定位:这是Selenium的核心技能。我们需要使用浏览器的“开发者工具”(F12打开),通过“检查”功能来查看目标元素的HTML属性。常用的定位方式有:
find_element(By.ID, “id值”):通过元素的id属性定位,最精确。find_element(By.NAME, “name值”):通过name属性定位,常用于表单。find_element(By.CLASS_NAME, “class名”):通过CSS类名定位。find_element(By.XPATH, “xpath表达式”):通过XML路径定位,功能最强大也最灵活,可以处理没有ID、Name的复杂情况。find_element(By.CSS_SELECTOR, “css选择器”):通过CSS选择器定位,语法简洁。- 对于URP这类可能使用框架的系统,
XPATH和CSS_SELECTOR往往是主力。
- iframe处理:如果登录框或评估表单被嵌套在一个
<iframe>框架里,你必须先使用driver.switch_to.frame(“frame的id或name”)切换到该框架内,才能定位到里面的元素。操作完后,记得用driver.switch_to.default_content()切换回主页面。 - 弹窗和警报:提交评估后可能会有JavaScript弹窗(alert)。需要使用
driver.switch_to.alert来获取弹窗对象,然后调用.accept()(确定)或.dismiss()(取消)来处理。
4. 代码实战:从登录到成绩提取的完整实现
有了前面的分析,我们现在可以着手编写核心代码了。请注意,以下代码是一个高度概括的模板,你需要根据自己学校URP系统的实际HTML结构来调整定位器(如ID、XPath)。
4.1 项目结构与初始化
首先,我们创建一个Python文件,并导入必要的库,进行初始化设置。
import time from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException import pandas as pd # 用于后续保存数据到Excel # 1. 初始化浏览器驱动 def init_driver(): # 指定你的ChromeDriver路径 service = Service(executable_path=r'你的chromedriver.exe绝对路径') # 配置浏览器选项(可选) options = webdriver.ChromeOptions() # 如果想无头运行(不显示浏览器界面),取消下面这行的注释 # options.add_argument('--headless') # 防止被一些简单的反爬机制识别,可以添加用户代理等(非必需) options.add_argument('--disable-blink-features=AutomationControlled') options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option('useAutomationExtension', False) driver = webdriver.Chrome(service=service, options=options) # 设置全局隐式等待(非必须,推荐用显式等待) driver.implicitly_wait(10) # 单位:秒 return driver # 主函数 def main(): driver = init_driver() try: # 后续所有操作写在这里 login_to_urp(driver) # ... 其他函数调用 except Exception as e: print(f"程序运行出错: {e}") finally: # 确保最终关闭浏览器 driver.quit() if __name__ == "__main__": main()4.2 登录函数实现
登录函数需要处理页面加载、元素定位、输入和可能的验证码中断。
def login_to_urp(driver): login_url = "https://你的学校urp登录页地址" driver.get(login_url) # 使用显式等待,确保页面核心元素加载完成 wait = WebDriverWait(driver, 20) # 最多等待20秒 try: # 假设用户名输入框的id是‘username’,密码框id是‘password’ username_input = wait.until( EC.presence_of_element_located((By.ID, "username")) ) password_input = driver.find_element(By.ID, "password") login_button = driver.find_element(By.ID, "loginBtn") # 假设登录按钮id # 输入你的账号密码(在实际使用中,建议从配置文件或安全输入读取,不要硬编码) your_username = "你的学号" your_password = "你的密码" username_input.clear() username_input.send_keys(your_username) password_input.clear() password_input.send_keys(your_password) # **处理验证码(关键步骤)** # 先尝试查找验证码输入框和图片 try: captcha_input = driver.find_element(By.ID, "captcha") # 验证码输入框ID captcha_image = driver.find_element(By.ID, "captchaImg") # 验证码图片ID # 如果找到了验证码元素,说明需要输入 print("检测到验证码,请在浏览器中查看并手动输入...") # 这里可以增加一个截图功能,方便用户查看 # captcha_image.screenshot('captcha.png') # 然后程序暂停,等待用户手动在浏览器中输入验证码 input("请在浏览器中输入验证码后,回到控制台按回车键继续...") except NoSuchElementException: # 没找到验证码元素,说明本次登录不需要验证码,直接继续 print("未检测到验证码,继续登录...") pass # 点击登录按钮 login_button.click() print("已点击登录按钮,等待跳转...") # 等待登录成功后的页面特征元素出现,比如显示用户名的区域 # 假设登录成功后首页有一个id为‘userInfo’的元素 wait.until(EC.presence_of_element_located((By.ID, "userInfo"))) print("登录成功!") except TimeoutException: print("登录页面加载超时或元素定位失败,请检查网络或页面结构。") raise # 抛出异常,让主函数捕获并退出4.3 完成教学评估函数
这是最复杂的部分,需要处理列表循环、表单填写和页面导航。
def complete_evaluation(driver): print("开始进行教学评估...") wait = WebDriverWait(driver, 15) # 1. 找到并点击“教学评估”入口 # 入口可能是一个链接,文本包含“教学评估”、“评估”等 # 使用XPath通过链接文本定位是一种常见方法 try: eval_link = wait.until( EC.element_to_be_clickable((By.XPATH, "//a[contains(text(), '教学评估')]")) ) eval_link.click() print("已进入教学评估模块。") except TimeoutException: # 如果XPath不行,尝试其他方式,比如通过菜单的ID或CLASS print("未找到标准评估入口,尝试其他定位方式...") # 这里需要你根据实际页面调整 # eval_link = driver.find_element(By.ID, “someMenuId”) # eval_link.click() # 2. 等待评估课程列表加载 # 假设课程列表在一个id为‘courseList’的table中 wait.until(EC.presence_of_element_located((By.ID, "courseList"))) # 3. 获取所有待评估课程的行 # 假设每一门课程是table中的一行<tr>,并且有一个“评估”按钮 course_rows = driver.find_elements(By.XPATH, "//table[@id='courseList']/tbody/tr") total_courses = len(course_rows) print(f"找到 {total_courses} 门待评估课程。") evaluated_count = 0 for index, row in enumerate(course_rows): try: # 重新定位当前行的“评估”按钮,因为页面可能刷新 # 按钮可能在当前行的某个<td>里的一个链接或button eval_button = row.find_element(By.XPATH, ".//a[contains(text(), '评估')]") course_name = row.find_element(By.XPATH, ".//td[2]").text # 假设第2列是课程名 print(f"正在评估第 {index+1}/{total_courses} 门课程: {course_name}") # 点击“评估”按钮,进入评估详情页 eval_button.click() # 等待评估表单加载 wait.until(EC.presence_of_element_located((By.ID, "evalForm"))) # 4. 填写评估表单 # 评估表通常由多个问题组成,每个问题是一组同名的radio button # 例如,所有“教师授课态度”的选项radio的name都是‘q1’ # 我们的策略是:找到所有问题组,对每一组选择第一个选项(通常是“非常满意”) question_groups = driver.find_elements(By.XPATH, "//div[contains(@class, 'question-group')]") for group in question_groups: # 在每个问题组里找到第一个radio button并点击 first_radio = group.find_element(By.XPATH, ".//input[@type='radio'][1]") # 有时候radio被<label>包裹,直接点击radio可能不行,需要点击关联的label # 获取radio的id,然后找到对应的label radio_id = first_radio.get_attribute('id') if radio_id: label = group.find_element(By.XPATH, f".//label[@for='{radio_id}']") label.click() else: # 如果没有id,尝试直接点击radio元素 first_radio.click() # 5. 提交评估 submit_button = driver.find_element(By.ID, "submitEval") submit_button.click() # 处理可能的成功提示弹窗 time.sleep(1) # 给弹窗一点时间弹出 try: alert = driver.switch_to.alert alert.accept() # 点击“确定” print(" 评估提交成功。") except: # 没有弹窗,可能是页面刷新或提示在页面上 pass # 6. 返回课程列表页,继续下一门 # 提交后页面可能自动跳回列表,也可能需要点击“返回”链接 back_link = driver.find_element(By.XPATH, "//a[contains(text(), '返回')]") back_link.click() # 等待列表页再次加载 wait.until(EC.presence_of_element_located((By.ID, "courseList"))) # 重新获取课程行列表,因为页面刷新了 course_rows = driver.find_elements(By.XPATH, "//table[@id='courseList']/tbody/tr") evaluated_count += 1 except Exception as e: print(f" 评估课程 {index+1} 时出现错误: {e}") # 出错后尝试刷新页面或返回列表重试 driver.back() wait.until(EC.presence_of_element_located((By.ID, "courseList"))) course_rows = driver.find_elements(By.XPATH, "//table[@id='courseList']/tbody/tr") continue # 跳过当前课程,继续下一个 print(f"教学评估完成。成功评估 {evaluated_count}/{total_courses} 门课程。")4.4 查询并打印成绩函数
评估完成后,就可以去获取成绩了。
def fetch_and_print_grades(driver): print("开始查询成绩...") wait = WebDriverWait(driver, 15) # 1. 导航到成绩查询页面 try: grade_link = wait.until( EC.element_to_be_clickable((By.XPATH, "//a[contains(text(), '成绩查询')]")) ) grade_link.click() print("已进入成绩查询页面。") except TimeoutException: print("未找到成绩查询入口。") return # 2. 选择学期(如果需要) # 假设有一个下拉框选择学期,其id为‘termSelect’ try: term_select = driver.find_element(By.ID, "termSelect") from selenium.webdriver.support.ui import Select select = Select(term_select) # 选择第一个选项(通常是当前学期),或者根据文本选择 select.select_by_index(0) # select.select_by_visible_text(“2023-2024学年 第2学期”) except NoSuchElementException: print("未找到学期选择框,可能默认显示当前学期。") # 3. 点击查询按钮 query_button = driver.find_element(By.ID, "queryBtn") query_button.click() # 4. 等待成绩表格加载 wait.until(EC.presence_of_element_located((By.ID, "gradeTable"))) # 5. 解析成绩表格 grade_table = driver.find_element(By.ID, "gradeTable") # 获取所有数据行,通常表头是第一个<tr>,数据从第二个开始 rows = grade_table.find_elements(By.XPATH, ".//tbody/tr") # 或者直接用: rows = driver.find_elements(By.XPATH, "//table[@id='gradeTable']/tbody/tr") grades_data = [] for row in rows: cols = row.find_elements(By.TAG_NAME, "td") if len(cols) >= 4: # 假设至少有4列:序号、课程名、学分、成绩 course_info = { '序号': cols[0].text, '课程名称': cols[1].text, '学分': cols[2].text, '成绩': cols[3].text, # 可以继续添加其他列,如“绩点”、“课程性质”等 } grades_data.append(course_info) # 6. 打印成绩 print("\n========== 成绩单 ==========") print(f"{'序号':<5}{'课程名称':<30}{'学分':<8}{'成绩':<6}") print("-" * 60) for item in grades_data: print(f"{item['序号']:<5}{item['课程名称']:<30}{item['学分']:<8}{item['成绩']:<6}") # 7. (可选)保存到文件 df = pd.DataFrame(grades_data) df.to_excel('本学期成绩.xlsx', index=False) print(f"\n成绩已保存至 '本学期成绩.xlsx'") return grades_data4.5 主流程整合
最后,我们将上述函数整合到主函数中,形成一个完整的流程。
def main(): driver = init_driver() try: # 步骤1:登录 login_to_urp(driver) # 登录后可能需要短暂等待页面完全稳定 time.sleep(2) # 步骤2:完成教学评估 complete_evaluation(driver) # 步骤3:查询并打印成绩 grades = fetch_and_print_grades(driver) print("\n所有自动化任务已完成!") except Exception as e: print(f"主流程执行出错: {e}") # 出错时可以截图保存现场,便于调试 driver.save_screenshot('error_screenshot.png') print("错误截图已保存为 'error_screenshot.png'") finally: # 步骤4:退出 input("按回车键退出并关闭浏览器...") driver.quit()5. 避坑指南与实战经验分享
写代码的过程很少一帆风顺,尤其是在对付像URP这样可能年久失修、前端代码风格不一的系统时。下面分享一些我踩过的坑和总结的经验,希望能帮你少走弯路。
5.1 元素定位失效:动态ID与XPath稳定性
URP系统的前端代码可能是多年前的产物,或者使用了某些框架自动生成元素ID。你可能会发现,今天用By.ID定位成功的按钮,明天就跑不通了,因为ID是动态变化的(例如包含时间戳或随机数)。
- 应对策略:
- 优先使用相对稳定的属性:
name、class(注意可能是多个class组合)通常比动态ID稳定。查看元素时,看看有没有固定的name=”xxx”。 - 善用XPath的文本和属性匹配:当ID和Name都不可靠时,XPath是你的王牌。可以通过元素的文本内容、邻近元素的特征来定位。
- 例如,定位一个文本是“提交”的按钮:
//button[text()=‘提交’]或//input[@value=‘提交’]。 - 定位“教学评估”这个链接:
//a[contains(text(), ‘教学评估’)]。contains函数非常有用,可以匹配部分文本。 - 通过兄弟节点、父节点等关系定位:比如一个输入框前面有个固定的
<label>标签写着“用户名”,那么可以这样定位输入框://label[text()=‘用户名’]/following-sibling::input。
- 例如,定位一个文本是“提交”的按钮:
- 不要过度依赖浏览器开发者工具直接复制的XPath:Chrome的“Copy -> Copy full XPath”生成的往往是绝对路径,像
/html/body/div[3]/div[2]/form/div[1]/input。这种路径极其脆弱,页面结构稍有变动(比如多了一个<div>)就会失效。一定要自己编写相对路径的XPath。
- 优先使用相对稳定的属性:
5.2 页面加载与异步等待的艺术
这是新手最常遇到的问题。代码执行速度远快于网络加载和浏览器渲染速度。
- 绝对不要滥用
time.sleep():这是最糟糕的等待方式。设短了,元素还没加载出来;设长了,白白浪费时间。脚本会变得又慢又不可靠。 - 显式等待是唯一推荐的方式:使用
WebDriverWait配合expected_conditions。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait = WebDriverWait(driver, 10) # 超时时间10秒 element = wait.until(EC.presence_of_element_located((By.ID, “someId”))) # 或者等待元素可点击 button = wait.until(EC.element_to_be_clickable((By.XPATH, “//button[text()=‘确定’]”)))presence_of_element_located:元素出现在DOM中即可,不一定可见。visibility_of_element_located:元素出现在DOM中并且可见。element_to_be_clickable:元素可见且可点击(如按钮)。- 根据场景选择:对于输入框,用
presence或visibility都可以;对于要点击的按钮,一定要用element_to_be_clickable,否则可能点击无效。
- 处理AJAX加载:有些内容是通过AJAX动态加载的(比如点击查询后,表格数据才加载)。等待表格出现,不能只等
<table>标签,还要等里面的<tr>或<td>有内容。可以这样写:wait.until(EC.visibility_of_element_located((By.XPATH, “//table[@id=‘dataTable’]//td”)))
5.3 验证码:自动化路上的“拦路虎”
如前所述,验证码是自动化登录的最大障碍。除了脚本中提到的“手动中断输入法”,还有以下思路,但请务必在合规和尊重系统安全的前提下考量:
- 观察规律:有些简单的系统,验证码会在页面的某个隐藏字段里,或者验证码图片的URL本身就包含了答案(虽然这很不安全)。用开发者工具的Network面板观察验证码图片的请求,看看响应头或URL参数。
- OCR识别(复杂且不稳定):对于简单的数字字母验证码,可以使用Tesseract等OCR库尝试识别。但需要处理图片噪声、裁剪,成功率不高,且URP的验证码可能做了干扰线、扭曲等防OCR处理。
- 第三方打码平台(不推荐):有付费API服务可以识别验证码。但这涉及隐私和安全风险,且违反大多数网站服务条款,强烈不推荐用于此类个人学习项目。
- 终极方案:半自动化:我们的示例代码采用的方案是最稳妥的——遇到验证码就暂停,让用户手动输入。虽然不够“全自动”,但保证了脚本的可用性和安全性。你可以优化一下用户体验,比如自动截图验证码区域并保存,然后提示用户查看图片并输入。
5.4 会话管理与异常恢复
脚本运行时间可能较长,网络波动或服务器临时问题可能导致操作中断。
- 使用Try-Except包围关键操作:就像示例代码中那样,对每一门课程的评估操作都用
try-except包裹,一旦某门课出错,可以记录日志,然后跳过继续下一门,而不是整个脚本崩溃。 - 定期保存状态:对于评估这种多步骤任务,可以在本地文件或变量中记录已经成功评估的课程ID或名称。如果脚本中途因异常退出,重启后可以先读取这个记录,跳过已完成的课程。
- 合理使用
driver.back()和刷新:在页面状态异常或元素找不到时,可以尝试返回上一页(driver.back())或刷新当前页(driver.refresh()),然后重新定位元素。
5.5 浏览器的“反自动化”检测
一些现代网站会检测浏览器是否被Selenium等工具控制。虽然URP系统通常没这么先进,但了解这一点有备无患。我们的初始化代码中已经添加了一些选项来规避简单的检测:
options.add_argument(‘--disable-blink-features=AutomationControlled’) options.add_experimental_option(“excludeSwitches”, [“enable-automation”]) options.add_experimental_option(‘useAutomationExtension’, False)这些设置可以移除Chrome浏览器中一些被自动化工具控制的标志。如果仍然被检测到,可能需要更复杂的伪装,如修改WebDriver的属性(driver.execute_cdp_cmd),但这属于更高级的话题,且可能随着浏览器版本更新而失效。
6. 脚本的优化与扩展方向
一个能跑通的脚本只是开始,一个健壮、易用、可维护的脚本才是目标。完成基础功能后,可以从以下几个方向优化你的项目:
6.1 配置化与模块化
- 分离配置信息:将URL、用户名、密码、XPath定位表达式等易变信息从代码中抽离出来,放到配置文件(如
config.ini、config.yaml或settings.py)中。这样,当URP系统改版或你需要评估不同学期的课程时,只需修改配置文件,而不用动核心代码。 - 函数模块化:将登录、评估、查询成绩等功能封装成独立的函数或类方法,提高代码的可读性和复用性。主流程变得非常清晰。
6.2 增强健壮性
- 更完善的日志系统:使用Python的
logging模块替代简单的print。可以设置不同日志级别(INFO, WARNING, ERROR),并将日志输出到文件,方便事后排查问题。 - 重试机制:对于网络请求失败、元素短暂不可见等临时性错误,可以实现一个重试装饰器。例如,某个点击操作失败后,自动重试2-3次。
- 超时时间动态调整:根据网络状况和服务器响应,动态调整
WebDriverWait的超时时间,而不是一个固定值。
6.3 功能扩展
- 多学期成绩批量导出:修改成绩查询函数,使其能够遍历选择多个学期,并将所有成绩合并到一个Excel文件中,每个学期一个工作表。
- 成绩分析与可视化:利用
pandas和matplotlib,对导出的成绩数据进行简单的分析,如计算平均绩点、绘制各科成绩分布直方图等。 - 定时任务:结合Windows的任务计划程序(Windows)或cron(Linux/Mac),让脚本在每学期末的特定时间自动运行,实现真正的“无人值守”。
- 图形用户界面(GUI):使用
tkinter或PyQt为脚本制作一个简单的图形界面,方便不熟悉命令行的同学使用,可以在界面上输入账号密码、选择操作等。 - 通知功能:脚本运行完成后,可以通过邮件、钉钉机器人、Server酱等方式,将成绩结果摘要发送到你的手机,实现闭环。
6.4 伦理与合规性重申
最后,也是最重要的一点,我们必须反复强调自动化脚本使用的边界:
- 目的正当:这个脚本仅用于个人学习Python和Selenium自动化技术,以及提升个人处理重复性事务的效率。
- 遵守规则:使用前,请务必阅读你所在学校关于教务系统使用的相关规定。确保你的自动化操作不会对服务器造成异常压力(例如,避免高频、并发请求)。
- 责任自负:任何因使用自动化脚本导致的问题(如账号异常、评估数据错误等),需由使用者自行承担。脚本不应被用于任何干扰系统正常运行、窃取他人信息或谋取不正当利益的用途。
- 尊重版权:URP系统的前端代码、设计等属于其开发者或学校,本脚本仅模拟用户交互,不涉及对任何受版权保护内容的非法获取或复制。
自动化是一把双刃剑,用好了是提升效率的神器。希望这个详细的指南不仅能帮你搞定URP系统的自动化,更能让你掌握一套分析和解决类似Web自动化问题的通用方法论。在实际动手的过程中,耐心和细致的观察调试比任何高级技巧都重要。祝你调试顺利,早日摆脱手动点击的烦恼。