行业资讯
📅 2026/8/5 2:09:27
Python自动化脚本实战:Selenium抢课工具开发与反爬策略解析
1. 项目概述为什么“抢课”需要脚本又到了一年两度的选课季看着教务系统里那些“秒没”的热门选修课你是不是也经历过守在电脑前疯狂刷新页面结果却因为网络延迟、手速慢或者系统卡顿而错失心仪课程的绝望这种场景对于大学生来说简直是家常便饭。我经历过也见过太多同学为此捶胸顿足。于是一个想法自然而生能不能写个脚本让程序代替我们去做这些重复、机械且拼手速的点击操作这个项目就是围绕“利用脚本抢选修课”展开的一次技术实践。它本质上是一个自动化工具旨在模拟人类在教务系统上的登录、查询、选课等一系列操作但速度更快、更稳定、不知疲倦。听起来可能有点“灰色”但它的技术内核——网络请求模拟、自动化测试、反反爬策略应对——却是Web开发和网络安全领域非常经典且实用的技能。通过这个项目你不仅能解决一个实际的痛点更能深入理解浏览器与服务器是如何交互的以及如何用代码来驾驭这种交互。无论你是想学习Python自动化还是对网络爬虫感兴趣这都是一次绝佳的练手机会。2. 核心思路与技术选型2.1 需求分析与方案设计在动手写代码之前我们必须先想清楚脚本要做什么以及可能会遇到什么障碍。一个完整的抢课流程通常包括登录教务系统、进入选课页面、查询课程列表、定位目标课程、点击“选课”按钮、处理可能的验证码、确认选课结果。我们的脚本需要自动化这一系列步骤。这里有几个关键决策点模拟方式选择是模拟浏览器如Selenium还是直接发送HTTP请求如Requests库Selenium优点是完全模拟真人操作能执行JavaScript对动态加载的页面和复杂交互如滑块验证支持好。缺点是速度相对较慢资源占用高。Requests 解析库如BeautifulSoup, lxml优点是速度快资源占用低适合对请求逻辑清晰、页面结构固定的系统。缺点是无法处理复杂的JS逻辑和交互式验证。选择理由考虑到大多数高校教务系统历史悠久、前端交互复杂大量JS生成内容且可能存在简单的图片验证码为了更高的成功率和更接近真人操作本项目首选Selenium方案。对于结构极其简单或已摸清API接口的系统可以辅以Requests进行优化。目标定位策略如何从课程列表中精准找到我们要抢的课通常需要课程代码、课程名称、教师姓名或上课时间等唯一标识。脚本需要能解析HTML表格或列表根据这些条件进行筛选和定位。异常处理与重试机制网络波动、系统繁忙、验证码识别失败、课程已满等情况必须考虑。脚本不能因为一次失败就崩溃需要设计合理的等待、重试和状态检查逻辑。2.2 技术栈与工具准备基于上述分析我们确定以下核心工具编程语言Python。生态丰富相关库成熟是自动化脚本的首选。浏览器自动化Selenium。我们将使用selenium库配合ChromeDriver或GeckoDriver对应Firefox。页面解析虽然Selenium可以定位元素但结合BeautifulSoup可以更方便地进行HTML解析和数据提取。定时与等待timedatetime库用于计时和延时Selenium的WebDriverWait和expected_conditions用于智能等待页面元素加载这比写死time.sleep更稳健。验证码处理可选如果遇到简单的图片验证码可以考虑集成OCR库如ddddocr或pytesseract。但复杂的交互式验证如滑块、点选则难度陡增可能需要更复杂的方案或手动干预。配置管理使用config.ini文件或.env文件来存储教务系统网址、学号、密码、目标课程信息等敏感或易变数据避免硬编码在脚本中。注意使用自动化脚本访问教务系统可能违反该校的网络使用规定或选课规则。本项目仅用于学习自动化技术和网络协议原理请在法律和校规允许的范围内或在测试环境下进行技术研究切勿用于干扰正常教学秩序。3. 实战开发一步步构建抢课脚本3.1 环境搭建与基础配置首先确保你的Python环境已就绪建议Python 3.7。通过pip安装必要的库pip install selenium beautifulsoup4接下来是关键的浏览器驱动。以Chrome为例查看你电脑上Chrome浏览器的版本在地址栏输入chrome://settings/help。前往 ChromeDriver官网 或国内镜像站下载与你的Chrome版本号匹配的chromedriver可执行文件。将下载的chromedriver文件放在一个固定目录如C:\WebDriver\bin或/usr/local/bin并确保该目录已添加到系统的环境变量PATH中。或者你也可以在代码中指定驱动文件的绝对路径。创建一个config.ini配置文件内容如下[ACCOUNT] username 你的学号 password 你的密码 [COURSE] course_code ABC12345 # 课程代码 course_name 人工智能导论 # 课程名称用于双重校验 teacher_name 张老师 # 教师姓名 [SYSYEM] login_url https://jwxt.yourschool.edu.cn/login course_select_url https://jwxt.yourschool.edu.cn/student/courseSelect poll_interval 0.5 # 检查课程状态的轮询间隔秒 max_retries 10 # 最大重试次数3.2 核心模块一自动化登录与会话维持登录是第一步也是最容易出问题的一步。很多教务系统登录时会生成动态的Token或Session并且可能有随机的验证码。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 configparser import time class CourseGrabber: def __init__(self, config_pathconfig.ini): self.config configparser.ConfigParser() self.config.read(config_path, encodingutf-8) # 初始化浏览器驱动使用无头模式可隐藏浏览器窗口 options webdriver.ChromeOptions() # options.add_argument(--headless) # 无头模式运行时不可见 options.add_argument(--disable-blink-featuresAutomationControlled) # 禁用自动化控制标志 options.add_experimental_option(excludeSwitches, [enable-automation]) # 移除“正受到自动测试软件控制”提示 self.driver webdriver.Chrome(optionsoptions) self.wait WebDriverWait(self.driver, 10) # 显式等待最多等10秒 self.driver.implicitly_wait(5) # 隐式等待查找元素时最多等5秒 def login(self): 登录教务系统 login_url self.config[SYSTEM][login_url] self.driver.get(login_url) try: # 等待用户名输入框出现 username_input self.wait.until( EC.presence_of_element_located((By.ID, username)) # ID需根据实际页面修改 ) password_input self.driver.find_element(By.ID, password) login_button self.driver.find_element(By.ID, loginBtn) # 输入凭据 username_input.clear() username_input.send_keys(self.config[ACCOUNT][username]) password_input.clear() password_input.send_keys(self.config[ACCOUNT][password]) # **这里可能需要处理验证码** # 示例如果验证码是图片且位置固定 # captcha_element self.driver.find_element(By.ID, captchaImg) # captcha_code self.recognize_captcha(captcha_element) # 需要实现识别函数 # captcha_input self.driver.find_element(By.ID, captcha) # captcha_input.send_keys(captcha_code) # 点击登录 login_button.click() # 等待登录成功后的页面元素出现比如“个人中心”或“学生姓名” self.wait.until( EC.presence_of_element_located((By.XPATH, //span[contains(text(), 欢迎)])) ) print(登录成功) time.sleep(2) # 稍作等待让页面稳定 return True except Exception as e: print(f登录过程中出现错误: {e}) self.driver.save_screenshot(login_error.png) # 保存截图便于调试 return False实操心得元素定位By.ID是最稳定首选但很多学校系统元素ID是动态的。需要熟练使用By.XPATH和By.CSS_SELECTOR。浏览器的开发者工具F12中的“检查”功能是你的最佳助手可以复制元素的XPath或Selector。等待策略WebDriverWait配合expected_conditions是黄金搭档能有效避免因网络或脚本执行快于页面加载而导致的NoSuchElementException。避免滥用time.sleep它会让脚本效率低下且不稳健。反爬应对--disable-blink-featuresAutomationControlled和excludeSwitches选项可以帮助脚本绕过一些简单的基于WebDriver检测的反爬机制。但对于更高级的检测可能需要更复杂的伪装。3.3 核心模块二课程查询与目标定位登录成功后导航到选课页面并找到目标课程。def navigate_to_course_selection(self): 进入选课页面 select_url self.config[SYSTEM][course_select_url] self.driver.get(select_url) # 等待选课页面特有的元素加载比如“可选课程”标签 self.wait.until( EC.presence_of_element_located((By.XPATH, //h2[contains(text(), 可选课程)])) ) print(已进入选课页面。) time.sleep(1) def find_target_course(self): 在课程列表中定位目标课程 course_code self.config[COURSE][course_code] course_name self.config[COURSE][course_name] # 假设课程以表格形式呈现每一行是一个课程 # 这是一个非常依赖具体页面结构的函数需要你根据实际情况调整 try: # 方法1遍历所有课程行 course_rows self.driver.find_elements(By.CSS_SELECTOR, table.course-list tbody tr) # 修改选择器 for row in course_rows: cells row.find_elements(By.TAG_NAME, td) if len(cells) 3: # 假设第1列是代码第2列是名称 row_code cells[0].text.strip() row_name cells[1].text.strip() # 使用课程代码和名称双重匹配更精确 if course_code in row_code and course_name in row_name: self.target_row row self.select_button row.find_element(By.CLASS_NAME, select-btn) # 找到该行的选课按钮 print(f成功定位到目标课程: {course_name}) return True # 方法2如果页面有搜索或过滤功能可以先利用它缩小范围 # search_box self.driver.find_element(By.ID, courseSearch) # search_box.send_keys(course_name) # search_box.send_keys(Keys.RETURN) # time.sleep(2) # 等待搜索结果 print(未找到目标课程可能尚未开放或已选满。) return False except Exception as e: print(f定位课程时发生错误: {e}) self.driver.save_screenshot(find_course_error.png) return False注意事项页面结构多变这是脚本最脆弱的部分。不同学校、甚至同一学校不同学期的选课页面结构都可能变化。你的find_target_course函数需要极强的适应性。写好注释方便后续维护。容错设计如果没找到课程要有合理的后续逻辑比如记录日志、等待一段时间后重新查询而不是直接退出。3.4 核心模块三抢课执行与状态轮询找到课程后最关键的一步就是执行选课操作并持续监控状态。def grab_course(self): 执行抢课操作包含轮询和重试 max_retries int(self.config[SYSTEM][max_retries]) poll_interval float(self.config[SYSTEM][poll_interval]) for attempt in range(max_retries): print(f\n 抢课尝试第 {attempt 1} 次 ) # 1. 刷新页面或重新查询确保信息最新特别是“剩余容量” self.driver.refresh() time.sleep(1) # 等待刷新 # 或者重新调用 navigate_to_course_selection 和 find_target_course if not self.find_target_course(): print(目标课程未出现继续等待...) time.sleep(poll_interval * 5) # 未找到时等待稍长 continue # 2. 检查课程状态例如检查“剩余名额”单元格 try: # 假设剩余名额在第5列 quota_cell self.target_row.find_elements(By.TAG_NAME, td)[4] remaining quota_cell.text.strip() if remaining.isdigit() and int(remaining) 0: print(f课程已满 (剩余: {remaining})。等待释放...) time.sleep(poll_interval) continue except (IndexError, ValueError): # 如果找不到名额信息忽略直接尝试点击 pass # 3. 尝试点击选课按钮 try: # 点击前再次确认按钮是可点击的未被禁用 if self.select_button.is_enabled(): print(检测到可选状态尝试点击...) self.select_button.click() # 4. 处理可能的弹窗确认如“确定要选择此课程吗” time.sleep(0.5) try: alert self.driver.switch_to.alert alert.accept() # 点击确认 print(已确认选课弹窗。) except: # 没有弹窗是正常情况 pass # 5. 等待并确认选课结果 time.sleep(2) # 等待服务器响应 # 检查是否有成功提示或者课程状态变为“已选” success_indicator self.driver.find_elements(By.XPATH, //div[contains(text(), 选课成功)]) if success_indicator: print( 恭喜选课成功) return True else: # 也可能失败检查失败提示 fail_msg self.driver.find_elements(By.XPATH, //div[contains(text(), 失败) or contains(text(), 已满)]) if fail_msg: print(f选课失败: {fail_msg[0].text}) else: print(选课操作已完成但未检测到明确成功提示请手动确认。) # 即使失败也进行下一次尝试 else: print(选课按钮不可点击可能已选或时间未到。) except Exception as e: print(f点击选课按钮时出错: {e}) self.driver.save_screenshot(fclick_error_attempt{attempt1}.png) # 本次尝试结束等待间隔后继续 time.sleep(poll_interval) print(f\n⚠️ 经过 {max_retries} 次尝试仍未成功选课。) return False核心技巧轮询间隔poll_interval不宜过短如0.1秒会给服务器造成过大压力也可能触发反爬不宜过长会错过机会。0.5-2秒是比较常见的区间。状态检查不要盲目点击。先检查剩余名额、按钮状态可以避免无效请求提高脚本的“智能”程度。结果验证点击后一定要有验证逻辑。成功或失败脚本都应该能给出明确反馈而不是“蒙在鼓里”。3.5 主程序流程与优雅退出将上述模块串联起来并做好资源清理。def main(): grabber CourseGrabber() try: if grabber.login(): grabber.navigate_to_course_selection() # 可以在这里加一个定时等到准点开始抢 target_time 2023-12-01 09:00:00 # 设定开抢时间 print(f等待开抢时间: {target_time}) # 实现一个 wait_until_time(target_time) 的函数 # wait_until_time(target_time) success grabber.grab_course() if success: print(抢课脚本任务圆满完成) else: print(抢课未成功请检查网络、课程状态或脚本逻辑。) else: print(登录失败无法继续。) except KeyboardInterrupt: print(\n用户中断脚本执行。) except Exception as e: print(f脚本运行过程中发生未预期错误: {e}) import traceback traceback.print_exc() finally: # 无论成功与否最后都要关闭浏览器 print(正在清理资源...) time.sleep(3) # 留点时间看最后的状态 grabber.driver.quit() print(浏览器已关闭。) if __name__ __main__: main()4. 高级优化与反反爬策略一个基础的脚本只能应对最简单的情况。现实中的教务系统可能会有更多防御措施。4.1 应对复杂验证码如果系统使用了验证码我们需要分情况处理简单数字/字母图片验证码可以尝试使用OCR库。ddddocr库对这类验证码识别率不错且安装简单。import ddddocr def recognize_captcha(self, img_element): 识别验证码图片元素 # 将WebElement截图并保存为字节流 img_bytes img_element.screenshot_as_png ocr ddddocr.DdddOcr() captcha_text ocr.classification(img_bytes) return captcha_text滑动拼图、点选文字等交互式验证码这类验证码破解难度大通常需要复杂的图像识别和轨迹模拟算法超出了简单脚本的范畴。遇到这种情况一个务实的方案是设计脚本在遇到验证码时暂停并发出提醒如播放声音、发送邮件等待人工干预输入后继续执行。4.2 模拟人类操作行为直接、快速的自动化操作容易被识别。可以加入一些随机的人类行为特征随机延迟在操作之间加入随机等待时间time.sleep(random.uniform(0.5, 2.0))。模拟鼠标移动使用Selenium的ActionChains进行非直线的鼠标移动。from selenium.webdriver.common.action_chains import ActionChains button driver.find_element(...) actions ActionChains(driver) # 将鼠标从当前位置随机移动到按钮上 actions.move_to_element(button).pause(random.uniform(0.1, 0.5)).click().perform()随机滚动页面driver.execute_script(window.scrollBy(0, arguments[0]);, random.randint(100, 300))4.3 多账号与分布式抢课进阶对于极端热门的课程可以考虑多账号在配置文件中配置多个账号脚本轮流或同时需多线程/进程尝试。注意这需要确保每个账号的会话独立。基础分布式在一台性能好的服务器上运行脚本或者使用云函数在开课时间点触发避免个人电脑网络或电源的不稳定。5. 常见问题排查与调试技巧即使脚本写得再完善运行时也会遇到各种问题。这里记录一些典型的“坑”和解决方法。5.1 元素定位失败这是最常见的问题。症状NoSuchElementException,TimeoutException。排查手动确认用浏览器手动访问页面F12查看目标元素的ID、Class、XPath是否和脚本里写的一致。很多系统前端是动态生成的元素属性可能每次都会变。使用更稳健的定位器优先By.IDBy.NAMEBy.CSS_SELECTORBy.XPATH。XPath虽然强大但容易因页面结构微小变动而失效。尽量使用具有唯一性的属性。检查iframe如果目标元素在iframe里面你必须先使用driver.switch_to.frame(frame_reference)切换到对应的iframe中才能找到里面的元素。检查页面是否完全加载增加等待时间或使用WebDriverWait等待某个标志性元素出现。5.2 脚本被检测为自动化工具症状登录失败页面提示“操作异常”或直接跳转到验证码页面。排查与解决启用之前提到的ChromeOptions反检测参数。尝试更换User-Agent模拟不同浏览器。options.add_argument(user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...)禁用自动化扩展options.add_argument(--disable-extensions)终极方案使用更底层的浏览器自动化工具如puppeteer或playwright它们对浏览器的控制更精细伪装性更好但学习成本也更高。5.3 选课操作无效或失败症状点击了按钮但页面没反应或者提示失败。排查网络监听用浏览器F12的Network网络工具手动操作一次成功的选课观察点击按钮后浏览器向服务器发送了什么样的HTTP请求特别是POST请求的URL、Headers和Form Data。然后尝试用requests库直接模拟这个请求可能比Selenium点击更直接、更快。检查前后端交互有些按钮点击会触发复杂的JavaScript事件直接.click()可能无效。可以尝试用driver.execute_script(arguments[0].click();, button_element)来执行JavaScript点击。查看Console控制台F12的Console标签页可能有JavaScript错误信息这些错误可能导致后续操作失败。5.4 性能与稳定性问题症状脚本运行慢或者运行一段时间后崩溃。优化使用无头模式options.add_argument(--headless)可以节省大量GUI渲染资源。合理设置等待用显式等待替代固定的time.sleep减少不必要的等待时间。及时清理在循环中避免创建大量不必要的WebElement引用。异常捕获与恢复在grab_course的循环内做好异常捕获即使某次循环出错也能记录日志并继续下一次尝试而不是整个脚本崩溃。6. 伦理、风险与最后的建议在结束之前我必须再次强调这个项目的两面性。从技术学习角度它涵盖了HTTP协议、前端DOM、自动化测试、反爬与反反爬等多个知识点是一个非常棒的综合性练手项目。你能在解决实际问题的过程中快速提升编码和调试能力。但从实际应用角度请务必谨慎。大规模、高频次的自动化请求会对学校服务器造成压力影响其他同学的正常选课这很可能违反校规甚至相关法律法规。许多教务系统也有日志监控异常行为容易被发现并可能导致账号受限。因此我的个人建议是以学习为目的在本地搭建一个类似的测试环境或者寻找允许自动化测试的公开网站来练习你的脚本。如果确需使用请将轮询间隔设置得长一些如5-10秒降低请求频率做一个“有礼貌”的脚本。最好只用于捡漏那些已有人退课的剩余名额而不是在开课瞬间进行“暴力”抢夺。做好备份计划不要把所有希望都寄托在脚本上。手动操作仍然是最终保障。技术是一把双刃剑如何用它取决于持剑的人。希望你在享受编码和解决问题的乐趣的同时也能负责任地使用你的技能。这个项目带给你的远不止抢到一门课那么简单更重要的是那一整套分析问题、设计解决方案、动手实现并不断调试优化的工程化思维这才是未来无论从事什么技术工作都受用无穷的财富。