Python自动化评教脚本:从HTTP会话到CSRF令牌的完整实现
1. 从手动到自动:评教这件事,为什么值得“折腾”?
每到学期末,高校里总会上演一出“评教大戏”。学生们被要求在教务系统里,对每一位任课老师进行打分和评价。这事儿听起来简单,但实际操作起来,却是个不折不扣的“体力活”。想象一下,一个学生要面对十几门课程,每门课的评价体系可能包含几十个评分项和主观题,全部填完,少说也得花上半小时。更别提那些需要反复登录、页面卡顿、提交失败的“常规操作”了。对于学生来说,这成了期末复习之外的又一重负担;对于学校而言,评教数据的回收率、有效性和真实性,也常常因此大打折扣。
于是,“教务系统自动评教”这个想法,就在这种背景下应运而生了。它本质上是一个自动化脚本,旨在模拟人工操作,自动完成登录教务系统、选择课程、填写评价表单、提交结果这一系列流程。这听起来有点像“外挂”,但其初衷并非为了作弊或破坏规则,而是为了解决一个真实存在的效率痛点。很多同学开发这类脚本,最初的想法很简单:把时间从重复、机械的点击和填写中解放出来,去做更有价值的事情,比如复习备考。当然,这里必须强调,任何自动化工具的使用,都必须严格遵守学校的相关规定,不得用于恶意刷分或干扰正常教学秩序。本文探讨的技术实现,更多是作为一种编程学习和自动化流程设计的案例。
从技术角度看,实现一个自动评教脚本,会涉及到几个核心领域:网络请求的模拟(HTTP/HTTPS)、网页数据的解析(HTML/DOM)、表单的自动填充与提交,以及可能遇到的验证码识别、登录状态维持等反自动化对抗措施。这不仅仅是一个简单的“按键精灵”,它要求开发者对Web前端与后端的交互逻辑有清晰的理解。接下来,我将以一个典型的、基于Python的自动化方案为例,手把手拆解其实现原理、核心步骤以及那些“踩过才知道”的坑。
2. 核心原理拆解:浏览器在背后都干了些什么?
在动手写代码之前,我们必须先搞清楚,当我们在浏览器里手动评教时,到底发生了什么。这个过程,就是我们编写脚本需要精准模拟的“剧本”。
2.1 会话(Session)与Cookie:你的“临时身份证”
当你第一次输入学号和密码登录教务系统时,服务器验证通过后,会生成一个唯一的“会话标识”(Session ID),并通过响应头中的Set-Cookie字段发送给你的浏览器。浏览器会保存这个Cookie。此后,你在该网站内的每一次点击、跳转、提交表单,浏览器都会自动在请求头中带上这个Cookie(通常是Cookie: JSESSIONID=xxxxxx)。服务器通过这个Cookie来识别:“哦,是刚才那个已经登录的用户”。这就是HTTP协议无状态特性下,维持用户登录状态的核心机制。
注意:我们的自动化脚本必须完整地模拟这一过程。这意味着不能只模拟一次登录请求就完事,必须在后续的所有请求中,手动维护并携带这个关键的Cookie。在Python的
requests库中,使用Session对象可以非常方便地做到这一点,它会自动处理Cookie的存储和发送。
2.2 表单提交与CSRF令牌:防止“跨站攻击”的盾牌
现代教务系统为了安全,几乎都会使用CSRF(跨站请求伪造)令牌。你会发现,评教页面的表单里,除了你看得见的评分选项和文本框,往往还藏着一个(或多个)名为_csrf、csrf_token或类似名称的隐藏输入框(<input type="hidden" ...>)。它的值是一串随机字符串。
这个令牌是服务器在生成表单页面时动态创建的。当你提交表单时,必须将这个令牌原封不动地随表单数据一起提交。服务器会校验你提交的令牌是否与它之前发给你的那个匹配。如果不匹配,请求会被直接拒绝。这有效防止了恶意网站伪造你的身份提交表单。
对于自动化脚本来说,这意味着一个关键步骤:在提交评教数据前,必须先访问评教页面,从HTML源码中把这个隐藏的CSRF令牌值“挖”出来。我们通常使用像BeautifulSoup或lxml这样的HTML解析库来完成这项工作。
2.3 请求的观察与分析:开发者工具是“眼睛”
所有上述信息——登录的API地址、提交评教的URL、需要提交的表单字段名、CSRF令牌的位置——都需要我们通过浏览器的开发者工具(按F12打开)来观察和获取。
具体操作流程如下:
- 登录过程:打开教务系统登录页,在开发者工具的“网络”(Network)选项卡中,勾选“保留日志”(Preserve log)。然后手动输入账号密码登录。在网络请求列表中,你会看到登录时产生的一系列请求。找到那个
POST类型的、看起来是提交登录信息的请求(通常URL包含login、doLogin等关键词)。点击这个请求,查看其“标头”(Headers)和“负载”(Payload)。这里记录了请求的URL、方法(POST)、以及提交的表单数据(如username=你的学号&password=加密后的密码)。 - 评教过程:登录后,进入评教页面。同样观察网络请求,找到加载评教表单的那个
GET请求,查看其响应内容,找到CSRF令牌。然后,手动选择评分、填写意见,点击提交。再次观察网络请求,找到提交评教的那个POST请求,查看其提交的数据格式。你会发现,数据通常是一个键值对集合,键名对应前端的name属性,值就是你选择的分数或填写的文字。
只有通过这样细致的观察,我们才能编写出能精准模拟人工操作的脚本。不同学校的教务系统(如强智、清元优软、金智等)接口和字段名可能完全不同,但分析方法是通用的。
3. 实战构建:一个Python自动评教脚本的诞生
基于以上原理,我们可以开始构建脚本。这里我选择Python,因为它拥有极其丰富和强大的网络请求与解析库。核心工具是requests(处理HTTP请求)和BeautifulSoup(解析HTML)。
3.1 环境准备与基础架构
首先,安装必要的库:
pip install requests beautifulsoup4脚本的基础骨架如下:
import requests from bs4 import BeautifulSoup import time class AutoEvaluation: def __init__(self, base_url, username, password): self.base_url = base_url.rstrip('/') # 教务系统基础URL,如 http://jwxt.xxx.edu.cn self.username = username self.password = password self.session = requests.Session() # 创建会话对象,自动管理Cookie self.session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' }) # 设置一个常见的浏览器UA,避免被简单屏蔽 def login(self): """模拟登录流程""" # 1. 首先访问登录页,可能获取登录所需的初始token或验证码信息 login_page_url = f"{self.base_url}/login.jsp" resp = self.session.get(login_page_url) # 这里可能需要解析页面中的隐藏token,具体看学校系统 # soup = BeautifulSoup(resp.text, 'html.parser') # token = soup.find('input', {'name': 'lt'})['value'] # 举例 # 2. 构造登录数据并提交 login_api_url = f"{self.base_url}/doLogin.action" # 登录接口,需根据实际情况修改 login_data = { 'username': self.username, 'password': self.password, # 'lt': token, # 如果有的话 # 'execution': 'e1s1', # '_eventId': 'submit' } login_resp = self.session.post(login_api_url, data=login_data) # 3. 验证登录是否成功(简单方法:检查响应内容或后续访问个人页面) if "用户名或密码错误" in login_resp.text: print("登录失败,请检查账号密码。") return False print("登录成功!") return True def get_evaluation_list(self): """获取待评教课程列表""" # 访问评教入口页面 eval_entry_url = f"{self.base_url}/evaluation/toEvaluation.action" resp = self.session.get(eval_entry_url) soup = BeautifulSoup(resp.text, 'html.parser') # 解析课程列表,通常在一个table或ul列表中 course_list = [] # 假设课程链接在 class 为 'eval-course' 的a标签里 for link in soup.select('a.eval-course'): course_name = link.text.strip() course_url = link['href'] # 处理相对URL if course_url.startswith('/'): course_url = self.base_url + course_url course_list.append({'name': course_name, 'url': course_url}) print(f"找到 {len(course_list)} 门待评教课程。") return course_list def evaluate_course(self, course_info): """对一门课程进行自动评教""" course_url = course_info['url'] print(f"开始评教课程:{course_info['name']}") # 1. 访问课程评教页面,获取表单和CSRF令牌 resp = self.session.get(course_url) soup = BeautifulSoup(resp.text, 'html.parser') # 查找CSRF令牌 csrf_token_elem = soup.find('input', {'name': '_csrf'}) # 名称可能不同 if not csrf_token_elem: # 尝试其他常见名称 csrf_token_elem = soup.find('input', {'name': 'csrfToken'}) or soup.find('input', {'name': 'token'}) if csrf_token_elem: csrf_token = csrf_token_elem['value'] else: csrf_token = '' print("警告:未找到CSRF令牌,可能评教失败。") # 2. 解析评教表单的所有评分项 # 假设评分项是radio input,name类似 'score1', 'score2'... form_data = {} if csrf_token: form_data['_csrf'] = csrf_token # 模拟填写:这里我们假设对所有评分项都选择最高分(例如5分) # 你需要根据实际页面的input名称来修改 for i in range(1, 11): # 假设有10个评分项 radio_name = f'score{i}' radio_input = soup.find('input', {'name': radio_name, 'value': '5'}) # 找值为5的选项 if radio_input: form_data[radio_name] = '5' else: # 可能是下拉菜单或其他形式,需要具体分析 pass # 填写主观评价(可选) comment_box = soup.find('textarea', {'name': 'suggestion'}) if comment_box: form_data['suggestion'] = '老师讲课认真,内容丰富,受益匪浅。' # 通用好评 # 3. 找到表单提交的URL和方式 form_elem = soup.find('form', id='evalForm') or soup.find('form') # 找到评教表单 if not form_elem: print("未找到评教表单,跳过该课程。") return False submit_url = form_elem.get('action') if submit_url.startswith('/'): submit_url = self.base_url + submit_url elif not submit_url.startswith('http'): # 处理相对路径 submit_url = course_url.rsplit('/', 1)[0] + '/' + submit_url # 4. 提交评教数据 submit_resp = self.session.post(submit_url, data=form_data) if "评教成功" in submit_resp.text or "提交成功" in submit_resp.text: print(f"课程 [{course_info['name']}] 评教成功!") return True else: print(f"课程 [{course_info['name']}] 评教可能失败,请检查。") # 可以保存响应文本用于调试 # with open('error.html', 'w', encoding='utf-8') as f: # f.write(submit_resp.text) return False def run(self): """主运行流程""" if not self.login(): return time.sleep(1) # 等待一下,模拟人工操作间隔 courses = self.get_evaluation_list() for course in courses: self.evaluate_course(course) time.sleep(2) # 每评一门课间隔2秒,避免请求过快被服务器限制 if __name__ == '__main__': # 使用时替换为你的实际信息 BASE_URL = 'http://jwxt.yourschool.edu.cn' USERNAME = '你的学号' PASSWORD = '你的密码' evaluator = AutoEvaluation(BASE_URL, USERNAME, PASSWORD) evaluator.run()这是一个高度简化的框架,实际学校的系统要复杂得多。但它清晰地展示了核心流程:创建会话 -> 登录 -> 获取课程列表 -> 循环处理每门课程(获取令牌、构造数据、提交)。
3.2 关键环节的深度剖析与适配
上面的框架代码中有几个关键点需要根据实际情况进行深度适配,这也是最容易出问题的地方。
1. 密码的加密与编码很多教务系统不会明文传输密码。你需要在开发者工具中仔细观察登录请求的“负载”(Payload)。密码字段(password)的值可能是一串看似乱码的字符串。这通常是前端JavaScript进行了加密(如MD5、SHA1、BASE64或自定义算法)。我们的脚本必须模拟同样的加密过程。解决方法有两种:
- 方法A(推荐):找到前端加密的JS代码,用Python实现相同的加密逻辑。这需要一定的JavaScript逆向能力。
- 方法B(取巧):如果加密不是强随机性的(比如只是简单的MD5),你可以先手动登录一次,从开发者工具中复制出加密后的密码字符串,直接硬编码到脚本里。但注意,如果密码加密使用了随机盐(salt),此方法失效。
2. 动态加载的课程列表有些系统的评教课程列表不是直接写在HTML里,而是通过Ajax异步加载的(JSON格式)。这时,get_evaluation_list方法就不能再解析HTML了,而需要找到那个加载列表数据的API接口(通常在“网络”选项卡中看到一个返回JSON的XHR请求),然后直接用session.get()或session.post()去请求这个接口,解析返回的JSON数据。
3. 复杂表单结构与评分逻辑评分表可能非常复杂:有单选、多选、滑块、矩阵量表(多个题目共用一套选项)。你需要仔细分析每个评分项对应的HTML元素。例如,矩阵量表中,每个问题的选项可能name相同,但value不同,或者通过name中的下标来区分。构造form_data字典时,必须确保键值对与浏览器提交的完全一致。一个有效的调试方法是:先用脚本打印出解析到的所有input/select/textarea的name和value,然后手动在浏览器提交一次,对比网络请求中的表单数据,查漏补缺。
4. 验证码(CAPTCHA)的识别这是自动化最大的拦路虎。如果登录或评教环节有验证码,脚本的复杂度将急剧上升。处理方案有:
- 人工介入:在需要验证码时,脚本暂停,将验证码图片下载到本地或显示出来,等待用户手动输入,然后继续执行。
- OCR识别:使用开源的OCR库(如
ddddocr、pytesseract)进行识别,但对付扭曲、干扰线多的验证码效果不佳。 - 打码平台:调用第三方付费打码API,由人工平台协助识别,可靠性高但有成本。
- 绕过策略:观察验证码是否在同一个会话中会重复使用,或者是否有其他无需验证码的备用接口(可能性极低)。
4. 进阶挑战与反自动化对抗策略
当你以为脚本可以完美运行时,很可能会遇到以下问题,这都是教务系统为了阻止自动化操作而设置的“路障”。
4.1 请求频率限制与行为检测
服务器可能会监控异常请求。如果你的脚本以极快的速度(比如每秒数次)连续提交评教,很可能会触发风控,导致IP被临时封锁或会话失效。
应对策略:
- 增加随机延迟:在关键操作(如登录后、访问每个课程页面前、提交后)使用
time.sleep()增加等待时间,并且延迟时间最好在一定范围内随机(如time.sleep(random.uniform(1, 3))),模拟人的操作间隔。 - 模拟更真实的浏览行为:在访问评教页面之前,可以先随机访问几个其他页面(如成绩查询、课表页面),让请求序列看起来更“自然”。
- 使用代理IP池:如果IP被封锁,可以考虑使用代理IP。但对于个人评教而言,通常不需要如此复杂。
4.2 前端JavaScript动态计算参数
有些系统会在提交前,通过JavaScript动态计算一些参数并添加到表单中,比如时间戳的哈希值、鼠标移动轨迹的加密值等。这些值单纯通过解析静态HTML是获取不到的。
应对策略:
- 使用无头浏览器:这是解决复杂动态网页自动化的终极方案。工具如
Selenium、Playwright或Puppeteer可以控制一个真实的浏览器(如Chrome)来加载页面、执行JavaScript、模拟点击和输入。它能完美处理所有前端动态生成的内容。缺点是运行速度慢、资源占用高。
使用无头浏览器时,同样需要注意延迟和避免检测(有些网站会检测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 driver = webdriver.Chrome() driver.get(login_url) driver.find_element(By.ID, 'username').send_keys(username) driver.find_element(By.ID, 'password').send_keys(password) driver.find_element(By.ID, 'submitBtn').click() # ... 后续操作webdriver属性)。
4.3 登录状态丢失与异常处理
网络波动、服务器错误、会话超时都可能导致脚本运行中断。一个健壮的脚本必须有完善的异常处理和状态检查。
应对策略:
- 加入重试机制:对于网络请求,使用
try...except包裹,并在失败后重试几次。 - 定期检查登录状态:在评教几个课程后,可以访问一下“个人中心”页面,检查页面内容是否包含用户名,以此判断会话是否依然有效。如果失效,则需要重新调用
login()方法。 - 详细的日志记录:将每一步操作、遇到的异常、服务器返回的关键信息都记录到日志文件或控制台,便于出错时排查。
5. 伦理、风险与最佳实践
在技术实现的兴奋之余,我们必须冷静地讨论使用自动评教脚本的边界。
1. 遵守规则是第一前提务必仔细阅读你所在学校关于教务系统使用的管理规定。明确禁止自动化操作的系统,切勿使用脚本,以免触犯校规,导致账号被封禁甚至更严重的处分。本文的技术分享仅用于学习和研究HTTP协议、Web自动化以及Python编程,请勿用于任何违规用途。
2. 评教数据的真实性与价值评教的根本目的是帮助老师改进教学、让学校了解教学情况。全部打满分或随意填写的自动化评教,产出的数据是无效甚至有害的,扭曲了真实的反馈。即使使用脚本,也建议在关键的主观评价部分,根据课程实际情况进行手动填写,或者至少设置一个合理的、有波动的评分逻辑,而不是千篇一律的满分。
3. 脚本的保管与使用此类脚本包含了你的学号和密码(即使经过加密处理)。务必妥善保管代码,不要上传到公开的代码仓库(如GitHub),以免个人信息泄露。可以在代码中通过读取环境变量或外部配置文件的方式来传入敏感信息。
4. 一个折中的“半自动”方案如果你希望节省时间,又希望保留一定的真实性,可以考虑“半自动”方案:让脚本自动完成登录、跳转到每门课的评教页面、并自动填充好所有固定的客观题评分(例如都填“非常同意”),然后暂停,弹出主观题输入框让你手动填写意见,或者让你快速检查一下客观题评分是否需要微调,确认后再由脚本提交。这样既提升了效率,又保证了核心反馈的真实性。
实现这样一个自动评教脚本,是一个绝佳的Web逆向工程和自动化实战项目。它迫使你去理解HTTP协议、会话管理、前端与后端的交互、反爬虫策略等一整套知识。无论最终你是否用它来“偷懒”,这个构建过程本身,带来的技术提升都是实实在在的。在动手的过程中,你遇到的每一个错误、解决的每一个问题,都是比单纯使用脚本更宝贵的经验。
