很多同学学爬虫第一站就是拿教务系统练手——查成绩、抢课、看课表需求真实且场景固定。可真动手时几乎人人都会卡在登录这一步请求参数一堆、密码不知道是怎么加密的、验证码还总识别错。我之前在调试某高校教务网时也折腾了好几天翻遍了各种帖子最后才把整套流程跑通。这篇博客就把我从抓包到成功模拟登录的完整过程整理出来包括每一步为什么要这么做、代码怎么拆、坑在哪里希望能帮你少走几天弯路。适合谁看刚入门想搞懂模拟登录原理的新手或者已经写过简单爬虫、但一遇到加密参数就发懵的开发者。内容会尽量说人话代码可以直接改改域名和参数就能用。下面开始正题。1. 教务系统登录的本质与整体思路1.1 登录流程拆解从浏览器到HTTP请求很多同学以为登录就是把用户名密码 POST 到服务器其实真实流程比这复杂得多。我调试的这个教务系统一次完整登录过程是这样的首先打开登录页面浏览器会向服务器发起一个 GET 请求服务器返回 HTML 页面同时会种下一个会话 Cookie通常是 JSESSIONID 之类的东西。这个 Cookie 是整个会话的身份证后续的所有操作都得带着它。然后页面加载过程中还会请求一些静态资源比如验证码图片。这个验证码图片是通过另一个接口动态生成的请求这个图片接口时服务器又会更新或追加 Cookie 信息常见的是给验证码加一个临时标识。接着当你点击登录按钮时浏览器会把用户名、密码、验证码以及其他隐藏字段一起打包成表单数据通过 POST 请求提交给服务器。这里有个关键点密码往往不是明文直接提交的很多系统会先用 JavaScript 对密码做一次加密MD5、SHA、RSA 之类然后再放进表单。服务器收到请求后会校验验证码、密码如果通过就会返回一个跳转地址或者写入新的会话标志。之后你再访问其他页面就会带着这个登录成功的会话状态服务器不再要求你重复登录。用一句话总结模拟登录的核心就是模拟浏览器发起的两到三次 HTTP 请求正确维护会话 Cookie并按服务器要求构造提交数据。我们后面的所有代码和调试都是围绕这几个环节展开的。1.2 为什么选择 requests Session 而不是其他方案理论上可以用很多工具模拟登录selenium、playwright 这类浏览器自动化框架也可以用 requests、httpx 这些纯 HTTP 库甚至可以用 scrapy 的 FormRequest。但我的建议是如果不是为了应付反爬特别强的系统优先用 requests Session。原因有三个。第一轻量。浏览器自动化框架要启动一个真实浏览器内存和 CPU 占用高跑起来慢。教务系统这类内网系统本身性能就一般用自动化浏览器频繁访问很容易被服务器识别为异常流量。而 requests 只有几毫秒的请求耗时对服务器压力小得多。第二可控。用 requests每一个请求、每一个参数、每一个 Cookie 都是显式可见的。登录后出了什么问题你可以打印出请求头和响应内容逐步排查。而用浏览器自动化很多细节被浏览器隐藏了出了问题反而不好定位。第三可扩展。requests 的 Session 对象自动管理 Cookie你只需要把登录后的 Session 继续用于后续请求就能保持登录状态。配合 BeautifulSoup、lxml 解析 HTML实现查成绩、看课表这些后续功能非常自然。当然如果遇到非常变态的验证码、滑块、点选验证码纯 requests 处理起来会很麻烦那时候可以引入指纹浏览器或者 ddddocr 这类识别库。但教务系统常见的是四字符数字或字母验证码用 ddddocr 就足够了。后面我会给出具体方案。2. 登录前的准备工作抓包与参数分析2.1 抓包工具与核心请求定位动手写代码之前一定先抓包。不抓包就写代码纯属瞎猜。我习惯用浏览器自带开发者工具F12 打开切到 Network 面板勾选 Preserve log保留日志然后在页面上手动登录一次观察浏览器发出的所有请求。重点看三个请求第一个是打开登录页面的 GET 请求。点击这个请求在 Headers 里能看到响应头是否包含 Set-Cookie如果有记下 Cookie 的名字。在 Request Headers 里能看到 User-Agent、Referer 等字段这些后面构造请求时最好原样带上避免被服务器拒绝。第二个是验证码图片接口。教务系统的验证码通常是一张带干扰线的图片请求这个接口后注意看响应头里有没有更新 Cookie以及返回的图片内容类型一般是 image/jpeg 或 image/png。在浏览器里验证码图片的 URL 后面往往带一个时间戳或随机数参数这是为了防止缓存。模拟登录时这个参数可以省略但最好也随机生成一个贴近真实浏览行为。第三个是登录提交的 POST 请求。这是最关键的。点击这个请求看 Payload 或 Form Data 部分能看到实际提交的字段名字。比如我在这个系统里看到的是username: 20190001 password: 5e14dd2e0b8c1a2d3f4a567890abcd12 captcha: 8a3f execution: e1s1注意password 是一串看起来像 MD5 的十六进制字符串根本不是明文。这说明登录前需要对密码做加密。而 execution 这个字段很像是某个加密算法的参数或者动态 token每次加载登录页可能都会变。这些都是需要重点分析的参数。抓包时有个小技巧在 Network 面板的过滤框里输入“login”或者“doLogin”能快速过滤出登录相关请求。另外点击某个请求后在“启动器”Initiator面板能看到这个请求是由哪个 JavaScript 函数触发的点进去可以找到加密逻辑所在的位置。2.2 验证码处理识别与绕过验证码是模拟登录里最麻烦的一环。虽然说有些系统在测试模式下可以关闭验证码但遇到不能关闭的就得老老实实处理。我的做法分三步。第一步把验证码图片下载到本地人工看一眼长什么样。常见的是四位数字或者数字加字母可能有干扰线、噪点。如果图片还算清晰就用现成的 OCR 库识别。第二步选识别库。我推荐 ddddocr它是个开源库专门针对这类乱码验证码做过训练识别率在 70%-90% 之间对四位数字验证码通常足够用。安装很简单pip install ddddocr。使用也简单import ddddocr ocr ddddocr.DdddOcr(show_adFalse) with open(captcha.jpg, rb) as f: img_bytes f.read() captcha_text ocr.classification(img_bytes) print(captcha_text)这里的 show_adFalse 是关闭它默认打印的广告信息。识别结果可能有字母大小写问题如果验证码本身不区分大小写就统一转成小写或大写再提交。第三步也是很多教程没说透的验证码识别的时机和 Cookie 是强绑定的。你请求验证码图片时服务器会把验证码答案保存在会话里同时通过 Cookie 标记这个会话。所以请求验证码的 Session 和提交登录的 Session 必须是同一个。如果用不同的 Session即使验证码识别对了服务器也认为你提交的是另一个会话的验证码照样返回验证码错误。另外如果 ddddocr 识别率太低可以把图片先做灰度化、二值化处理再去识别。简单方法是用 Pillowfrom PIL import Image img Image.open(captcha.jpg).convert(L) img img.point(lambda x: 255 if x 140 else 0) img.save(captcha_binary.jpg)很多干扰线在二值化后会被过滤掉识别率能提升不少。但要注意不要为了保护识别率而过度处理有时反而会丢失字符信息。3. 核心实现完整模拟登录代码与逐段解析3.1 构建会话与获取 Cookie有了前面的分析代码就顺理成章了。首先创建一个 Session 对象并模拟浏览器设置初始请求头。教务系统一般比较老对 User-Agent 没有严格检查但最好还是伪装成一个常见浏览器 UA避免某些安全组件拦截。import requests session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, })然后发送 GET 请求访问登录页面。这一步是为了让服务器种下初始 Cookie并获取隐藏字段。login_page_url https://jw.example.edu.cn/login resp session.get(login_page_url, timeout10) print(resp.status_code) print(session.cookies.get_dict())这里有个细节有的系统在登录页面 HTML 里直接嵌入了一个随机 token比如input typehidden namecsrf_token valuexxx或者像前面说的 execution 字段。我们需要用正则或者 BeautifulSoup 从页面里提取出来否则后面 POST 提交时缺字段会被拒绝。from bs4 import BeautifulSoup soup BeautifulSoup(resp.text, html.parser) execution soup.select_one(input[nameexecution])[value]如果你发现请求登录页响应里没有这个字段可能是登录页 URL 不对或者大部分逻辑在 JavaScript 中动态渲染。这时就需要看 —— 但大多数教务系统是服务端渲染的直接能在 HTML 里找到。拿到 Session 和初始页面后下一步就是请求验证码。验证码图片 URL 一般在登录页面里以img srccaptcha.jpg的形式存在需要拼完整路径。请求时注意要带上 Referer有些系统会检查。captcha_url https://jw.example.edu.cn/captcha.jpg img_resp session.get(captcha_url, headers{ Referer: login_page_url, }, timeout10) with open(captcha.jpg, wb) as f: f.write(img_resp.content)这一步完成后Session 里的 Cookie 应该更新了。打印一下确认print(session.cookies.get_dict())如果请求验证码前后 Cookie 没有变化也没关系只要后续提交登录还用同一个 Session服务器就能把验证码请求和登录请求关联起来。很多教务系统就是通过同一个连接中的 Cookie 来关联这两个动作的。3.2 密码加密逻辑实战这是整个模拟登录里最容易被卡住的地方。教务系统的密码加密花样很多常见的有这几种直接 MD5 加密password hashlib.md5(password.encode()).hexdigest()先 MD5 再加盐可能是固定盐或用户名参与。RSA 加密给一个公钥字符串用 JavaScript 加密后提交。HEX 混淆或自定义算法。要确定具体算法最靠谱的方法是去阅读登录页引用的 JavaScript 文件。在浏览器开发者工具的 Sources 面板里找到那些 .js 文件搜索“password”字段或者搜索“encrypt”“md5”“RSA”等关键词。也可以直接在 Console 里调用相关函数测试比如在登录页面控制台输入typeof md5如果返回 function说明页面里引入了 MD5 函数。以我调试的这个系统为例它的加密逻辑是在一个公共 JS 文件里定义了一个函数对密码做了两次迭代 MD5然后又和用户名的前几位拼接后再做了一次 MD5。具体细节不便展开但思路就是把完整的加密函数用 Python 复现出来。JavaScript 的 md5 和 Python 的 hashlib.md5 输出一致可以放心转换。如果遇到 RSA 加密也不用慌。RSA 加密需要公钥n, e一般在登录页源码或某个 JS 文件里能找到。Python 里可以用 rsa 库或 pycryptodome 库实现。但要注意 JS 加密时的填充方式常见的是 PKCS#1 v1.5只要确保 Python 侧填充方式一致结果就能对齐。我的调试方法是先在浏览器里用开发者工具在 Console 里执行一次加密得到一串加密结果然后在 Python 里用自己写的加密函数算一遍对比两个结果是否一样。如果一致说明算法复现正确如果不一致就去检查是不是拼接顺序错了、编码格式是不是 UTF-8 而不是 Unicode或者是否还有多次编码。下面是一个简化的示例代码结构展示如何将密码加密函数封装起来import hashlib def encrypt_password(password, username): # 假设系统先用密码做了两次 MD5再用用户名参与拼接 first hashlib.md5(password.encode(utf-8)).hexdigest() second hashlib.md5(first.encode(utf-8)).hexdigest() final_str second username[:2] final hashlib.md5(final_str.encode(utf-8)).hexdigest() return final实际系统里算法可能比这复杂也可能用到 AES/DES但核心思路不变读 JS、复现、对比验证。3.3 验证码识别与提交登录验证码识别加登录提交的完整流程我用代码串联一遍。注意这里大部分操作都在同一个 Session 下完成。import ddddocr from bs4 import BeautifulSoup import requests # 1. 创建session并访问登录页 session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 ... }) login_page_url https://jw.example.edu.cn/login resp session.get(login_page_url, timeout10) # 2. 解析隐藏字段 soup BeautifulSoup(resp.text, html.parser) execution soup.select_one(input[nameexecution])[value] # 3. 获取验证码 captcha_url https://jw.example.edu.cn/captcha.jpg img_resp session.get(captcha_url, headers{Referer: login_page_url}, timeout10) # 4. 识别验证码 ocr ddddocr.DdddOcr(show_adFalse) captcha_text ocr.classification(img_resp.content).strip() print(识别验证码:, captcha_text) # 5. 提交登录 login_post_url https://jw.example.edu.cn/login/doLogin data { username: 20190001, password: encrypt_password(my_password, 20190001), captcha: captcha_text, execution: execution, } login_resp session.post(login_post_url, datadata, headers{ Referer: login_page_url, }, timeout10) print(login_resp.status_code) print(login_resp.text[:500])提交成功后可以再看一下 Cookie 的变化通常登录成功后 Cookie 里会多出一些会话标识比如JSESSIONID更新或者新增login_flag之类的字段。如果返回的响应里有“欢迎你”或者“个人中心”字样说明登录成功。如果登录失败响应里通常会有错误提示比如“验证码不正确”“用户名或密码错误”。这些信息可以帮助我们快速定位问题。有的系统会把这些提示放在 JSON 里有的放在跳转后的页面 HTML 里需要仔细看。3.4 维持会话与后续请求登录成功后的 Session可以直接用来访问其他需要登录才能看的页面。比如查成绩score_url https://jw.example.edu.cn/student/score score_resp session.get(score_url, timeout10) print(score_resp.status_code) # 解析HTML表格提取成绩这里有个坑有些教务系统在登录成功后第一次访问其他页面时还会二次重定向到一个“正在登录”的中间页然后跳到目标页。我们可以设置allow_redirectsTruerequests 默认就是让它自动跟随重定向。如果登录状态是靠 Cookie 维持的不需要每次请求都重新提交登录信息。另外Session 对象在 Python 进程结束后就失效了。如果希望程序运行结束后仍能保持登录状态可以把 Cookie 导出到文件下次启动时再导入import pickle with open(cookies.pkl, wb) as f: pickle.dump(session.cookies, f) # 下次使用时 with open(cookies.pkl, rb) as f: cookies pickle.load(f) session.cookies.update(cookies)不过要注意教务系统的会话有效期一般有限可能半小时到几小时不等。过期之后Cookie 就失效了还是需要重新登录。4. 常见问题与排查技巧4.1 登录失败的几个典型原因我调试过程中遇到过的问题基本可以归为这几类一是验证码识别正确但仍然提示“验证码错误”。这种情况最常见的原因是 Session 不一致。比如有人把请求验证码用 requests.get提交登录又用了一个新的 requests.session导致服务器无法把验证码和登录请求关联。解决方法是保证两个请求使用同一个 Session 对象。二是密码加密结果不正确。有时候 JS 里加密函数是异步的或者 URL 编码有问题。检查方法是在浏览器控制台手动执行加密函数对比你 Python 里的结果。另外注意某些系统在加密前会把密码先进行 HTML 实体编码或 Unicode 编码比如将空格变成%20这类细节隐藏很深。三是缺少隐藏字段。页面里除了用户名密码可能还有若干 hidden 字段比如 execution、lt、eventId。有的字段是整个页面唯一标识不带就会登录失败。必须从登录页面动态提取不能写死因为每次请求的值都可能不同。四是 Cookie 或 UA 被服务器校验。老系统一般不校验 UA但有的负载均衡设备会检查。如果请求头里缺少 Referer也可能被拦截。我这边的经验是把所有请求都加上 Referer指向来源页面能省掉很多麻烦。4.2 请求频率与封禁规避有人为了调试一分钟内几十次发登录请求结果 IP 被临时封了。模拟验证码识别时也容易反复请求验证码图片导致触发防护。我的建议是每次请求之间做适当延时比如time.sleep(random.uniform(1, 2))。不要一次性测试十次以上连续登录。如果验证码识别率不高先本地保存几张图片测试识别而不是直接怼到服务器。如果被封了换个 IP 或者等待一段时间。学校网络环境里一般封禁时间是几分钟到几十分钟。另外有的教务系统会有限流策略对同一 Session 短时间内的请求过多会强制退出。所以拿到登录状态后爬取数据时也建议控制频率每抓一页间隔 1 到 3 秒。4.3 实用调试技巧分享几个我常用的调试手段能帮你快速定位问题先打印所有请求的 headers 和 cookies。我习惯在 requests 上做一层包装每次请求前打印 URL、数据和预期结果这样出错时能立刻看到是哪个环节不对。def request_with_log(func, *args, **kwargs): print(请求:, args[0] if args else kwargs.get(url)) resp func(*args, **kwargs) print(状态码:, resp.status_code) print(响应前100字:, resp.text[:100]) return resp再有遇到响应内容乱码时最好检查一下页面编码。有些老教务系统是 GBK 或 GB2312 编码如果 requests 没有正确解码可以通过resp.encoding gbk手动指定。解析 HTML 时用 BeautifulSoup 时要注意from_encoding参数防止乱码导致解析失败。还有如果服务器返回的不是 HTML 而是 JavaScript 弹窗这说明登录逻辑可能做了改动或者需要先执行某个前置请求。这时重新抓包仔细对比改动前后的请求差异。5. 扩展升级自动抢课、成绩查询与更多玩法登录成功后能干的事情就多了。最常见的是查成绩和抢课。查成绩其实就是访问成绩列表页然后解析表格。抢课稍微复杂一点因为选课涉及到事务处理请求参数更多而且要抢在开放瞬间提交。但核心依然是在这个稳定的会话上构造选课 POST 请求。如果要做一个完整的教务助手我建议把代码组织成三个模块登录模块负责维护 Session、验证码识别、登录状态检测。数据解析模块用 BeautifulSoup 或 Scrapy 的 Selector把成绩、课表、个人信息从 HTML 中提取成结构化数据。业务模块比如抢课、成绩通知提醒基于前两个模块实现。有同学问过为什么不用 Scrapy 直接模拟登录。其实也行Scrapy 里可以用 FormRequest 和 CookieJar但复杂度和学习成本高一些。先用 requests 跑通流程再迁移到 Scrapy 也不迟。另外有些系统现在改成了统一身份认证CAS登录流程多了一次跳转。这种情况下模拟登录要先访问 CAS 登录页提交认证信息拿到授权码再带着授权码去访问教务系统。抓包会多两步但原理一样。如果遇到 CAS核心就是跟住重定向链找到最终的 POST 参数。6. 安全与道德边界必须提醒一句模拟登录技术本身是中性工具但用在哪、怎么用是有边界约束的。对教务系统的自动化访问以下几点至少要遵守第一只访问你本人有权限访问的数据。不要尝试越权查看别人信息不要为了演示去爬取大量他人数据。第二控制请求频率。教务系统是服务于全校师生的过大的请求压力会影响其他正常用户。即使你有管理权限也应避免对服务器造成负担。第三以学习研究为目的。模拟登录的代码可以在本地跑通验证不建议直接部署到公网。也不要将自己的账号借给他人使用更不能通过自动化工具影响正常的教学管理流程。第四遵守学校网络管理规定。如果在使用过程中出现账号异常或者被警告应及时停止操作并向相关部门说明情况。这里也提醒一句这类技术学习应当基于合规环境切勿用于任何未经授权的系统。把技术用在提升效率、学习原理上模拟登录是一个很好的练手项目。用它做坏事只会给自己惹麻烦。我自己的习惯是每写一段爬虫代码前先想清楚如果这个脚本被公开我会不会感到不安如果会那就说明越界了。这次以教务网为例写了这么多其实剥掉外壳模拟登录的核心思路是通用的分析请求、维护会话、处理加密、解决验证码。把这一套玩熟以后遇到其他系统也就能举一反三了。如果在调试过程中遇到任何问题欢迎在评论区交流——毕竟爬坑经验都是互相踩出来的。
阅读完成 · 觉得有帮助?