news 2026/10/22 21:20:05

Python模拟登陆学信网查询四六级成绩,告别准考证号丢失

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python模拟登陆学信网查询四六级成绩,告别准考证号丢失

简介:这份Android示例项目实现了模拟登录学信网并查询四六级成绩,适合正在学习网络编程与数据解析的Android开发者。项目完整展示了从构造登录请求、携带Cookie维持会话,到解析返回HTML并提取成绩信息的全过程,涉及HttpURLConnection/OkHttp网络层、Jsoup页面解析、Gson数据处理及登录状态管理,同时包含EditText输入、按钮事件与结果展示等经典UI交互。压缩包共50个文件,以class、xml、java源码为主,辅以jar库(含jsoup-1.7.1与android-support-v4)、apk安装包及资源文件,整体仅1.46MB,结构紧凑便于快速导入学习。已有737人学习过这份源码,关注用户登录态处理与异常流控的同学可重点参考其网络层设计,还能从中掌握HTTPS安全传输、权限声明及try-catch容错等实战细节。

1. 一份能自动查四六级的模拟登陆源码,解决了“准考证号丢失”这个真实痛点

每年四六级出分季,最尴尬的瞬间不是没过,而是准考证号找不到了。这套Android模拟登陆学信网查询四六级源码,核心逻辑是把“登录学信网→发起成绩查询→解析结果页”这条链路用Python脚本自动化。它不是手机App成品,而是一套可复现的模拟登陆实现:不依赖OCR识别验证码、不用模拟器人工点击、更不需要逆向App抓包——一个requests会话加几次HTTP请求就能把成绩捞出来。适合三类人:弄丢准考证号又急着查分的考生、想拆解模拟登陆与Cookie机制的学习者、需要批量核对成绩的教务助理。

2. 学信网模拟登陆的认证链路与技术选型:先把账算清

在下载源码动手跑之前,得先把这条认证链路讲清楚。学信网账号体系走的是统一身份认证(CAS风格):用户先在passport登录页提交身份证号和密码,偶尔还要过一个人机验证码;认证通过后服务端种下一组会话Cookie,再带着一个ticket参数通过302跳转到学信档案(my.chsi.com.cn)的service地址。整个过程是三次握手式的,缺了任何一段都会出现“明明登录成功、但查询接口还报未授权”。

2.1 认证链路拆解:GET预取、POST提交与302跟随

模拟登陆的第一课不是写POST,而是先GET登录页。很多人的实现翻车就翻在直接往登录接口怼参数——统一认证页里往往藏着一两个hidden字段(比如CAS令牌这类东西),漏掉任何一项都会被告知“请求来源不合法”。常见做法是先抓登录页,把input[type=hidden]里的键值对原样取出来,再叠加上你手输的身份证号、密码、验证码字段,作为POST表单原样回发。

import requests from lxml import html LOGIN_BASE = "https://account.chsi.com.cn/passport/login" # service 参数指向学信档案的CAS回调地址,登录成功后必须跳到这 ARCHIVE_SERVICE = "https://my.chsi.com.cn/archive/j_spring_cas_security_check" session = requests.Session() session.headers["User-Agent"] = ( "Mozilla/5.0 (Linux; Android 12; Pixel 5) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/112.0.0.0 Mobile Safari/537.36" ) # 1) 先拉登录页,提取隐藏表单字段 resp = session.get(LOGIN_BASE, params={"service": ARCHIVE_SERVICE}, timeout=10) doc = html.fromstring(resp.text) form = {node.get("name"): node.get("value") for node in doc.xpath('//input[@type="hidden"]')} # 2) 追加用户凭据 form["username"] = "你的身份证号" form["password"] = "你的密码" form["captcha"] = "" # 无验证码时留空,有验证码则后续填充

这段代码的逻辑是先构造一个有移动端UA的全局Session,再GET一次登录页取出CAS留下的hidden字段。之所以要单拉一次,是因为很多统一认证页会把令牌绑定在会话里,你不预先GET一次,它后面校验来源时就拿不到有效token。参数方面,params里的service必须与登录后跳转目标一致,否则CAS回调校验会失败。UA建议固定成一台真实机型的字符串,学信网对空UA和常见Python库UA的拦截概率明显更高。

提交阶段要用allow_redirects=False,这是为了观察302的Location去向,而不是让requests自动吞掉跳转。

LOGIN_POST = "https://account.chsi.com.cn/passport/login" def do_login(session, form): resp = session.post(LOGIN_POST, data=form, allow_redirects=False, timeout=10) # 认证成功:服务器返回302并给出CAS回调地址 if resp.status_code == 302: location = resp.headers.get("Location") if location: session.get(location, timeout=10) # 跟随到学信档案 return True # 失败:可能是验证码错误或凭据无效 if "验证码" in resp.text: print("验证码不对,需要重新取一次登录页") return False

关键点在于手动跟随Location而不是让requests自动跳。原因有两个:一是有些反爬逻辑会检查Referer,手动缓存一下302地址能更清晰地控制请求头;二是调试时你能直观看到这次登录到底被踢到哪个service。若这里返回的状态码不是302,八成是用户名密码错或验证码过期——先看返回页面里嵌着的错误提示,比瞎改UA省时间。

2.2 为什么用Python做核心,而不是纯Kotlin/Java

这批源码大概率是Python脚本为主体、Android侧用Termux或封装App来调用,而不是用OkHttp硬写。原因很实在:模拟登陆涉及会话保持、Cookie解析、HTML结构定位、验证码人工介入,Python的requests和lxml在这个领域几乎是标配,而Java/Kotlin生态里最强的Jsoup在动态表单与复杂回调场景下要写的胶水代码多得多。

维度Python(requests+lxml)Kotlin(OkHttp+Jsoup)
会话与CookieSession自动管理需要手动维护CookieJar
表单与重定向allow_redirects控制也要写拦截器
HTML解析XPath/正则都很顺手Jsoup选择器可用但容错差
调试速度改一行重跑编译打包循环长
在Android上的落地方式Termux或后端封装直接集成进App

在Android设备上跑这套脚本,最省事的落地方式是装Termux,Python环境就在手机里,脚本原样运行;如果目标是做一个能点按钮的App,更常见的做法是把这个Python脚本包一层Flask接口,App只发HTTP请求拿结果。直接全用Kotlin重写也不是不行,但维护成本会把“查个分”这件事变成“修一个App”,不划算。

2.3 为什么不依赖OCR识别验证码

网上很多模拟登陆教程会拿深度学习OCR去认那四位数字字母,实际效果是60%的场景顺利、40%的时间在翻车——学信网的验证码字体扰动、噪点、干扰线一换,识别准确率立刻打回原形。这套源码的聪明之处在于默认不跟验证码硬刚,而是走“登录态持久化”:首次登录时人工看一眼验证码输入一次,登进去后把Cookie落到本地json缓存,之后的查询全部带着本地Cookie走,完全绕开验证码。

这也顺便解决了“模拟登陆查四六级”场景里真正的高频痛点:考完试的同学只需要人肉登一次、把Cookie留在机器上,后面每次出分季节就能直接跑查询脚本,不用再回忆密码和验证码。代码逻辑上就是查数据前先读本地Cookie文件判断是否还有效,失效才重新触发完整登录。

3. 源码三件套直读:登录器、查询器与HTML解析器

把这套源码拆开看,核心就是三个文件/模块:登录器负责拿Session,查询器负责拼参数打成绩接口,解析器负责从HTML里把分数抠出来。三者各干各的,接口之间只传字符串和字典,这样哪怕学信网改版,也只需要动解析器一个文件。下面按模块逐个过。

3.1 登录器模块:Session构造、登录与Cookie落盘

登录器对外就暴露一个login()和load_or_login()。前者走完整的POST表单流程,后者先检查本地cookie缓存,能用就直接复用。完整起见,我把验证码图片下载也放进来——验证码必须用同一个Session下载,前面2.1说过,这是最容易翻车的一步。

import json from pathlib import Path COOKIE_FILE = Path("chsi_cookies.json") def load_or_login(session, identity, password): # 有本地Cookie先试着复用 if COOKIE_FILE.exists(): session.cookies.update(json.loads(COOKIE_FILE.read_text())) probe = session.get("https://my.chsi.com.cn/archive/", timeout=8) if "退出登录" in probe.text: return session COOKIE_FILE.unlink() # 会话失效,删掉再重登 # 重新走完整登录流程 resp = session.get(LOGIN_BASE, params={"service": ARCHIVE_SERVICE}, timeout=10) doc = html.fromstring(resp.text) form = {i.get("name"): i.get("value") for i in doc.xpath('//input[@type="hidden"]')} form.update({"username": identity, "password": password}) captcha_src = doc.xpath('//img[contains(@id,"captcha")]/@src') if captcha_src: img = session.get(LOGIN_BASE + captcha_src[0], timeout=10) Path("captcha.png").write_bytes(img.content) form["captcha"] = input("打开 captcha.png 输入四位验证码: ") else: form["captcha"] = "" if do_login(session, form): COOKIE_FILE.write_text(json.dumps(session.cookies.get_dict())) return session

这里load_or_login把“要不要登一次”的决策做成幂等逻辑:Cookie存在就先用探测页验证,页面里出现“退出登录”字样说明会话活着;否则清掉缓存强制重登。验证码用input()人工输入是刻意保留的兜底设计——识别模型再强也扛不住验证码换样式,与其赌OCR不如让人看一眼。Cookie落盘用的是session.cookies.get_dict(),重新登录后旧Cookie文件会被新会话的Cookie覆盖,这是合理的,不用手动清理。

3.2 查询器模块:准考证号查询与免准考证号查询

四六级成绩查询在学信网体系里有两条路。第一条是常规的CET查询接口,输入15位准考证号加姓名就能拿成绩单页;第二条是登录学信档案后,从学信档案的成绩单入口直接按身份证维度调取历史成绩,这条路不需要准考证号——对弄丢准考证号的人来说,这是源码里价值最高的部分。

CET_QUERY_URL = "https://cet.neea.edu.cn/cet/query" ARCHIVE_SCORE_URL = "https://my.chsi.com.cn/archive/cjcx/" def query_by_ticket(session, zkzh, name): """常规查询:准考证号+姓名""" params = {"zkzh": zkzh, "xm": name} resp = session.get(CET_QUERY_URL, params=params, timeout=15) return resp.text def query_from_archive(session): """免准考证号查询:登录态有效时学信档案直接吐成绩列表""" resp = session.get(ARCHIVE_SCORE_URL, timeout=15) return resp.text

常规接口的params就是准考证号和姓名两个字段,字段名和官网表单保持一致最稳妥,不要画蛇添足加pageSize之类不存在的参数。免准考证号那条路依赖学信档案页面结构,如果返回结果里一个“成绩单”链接都没出现,那就是登录没生效,先回头查登录器,别在查询器里瞎调。

3.3 解析器模块:XPath定位容器,正则抠数字

学信网成绩页属于历史遗留HTML,class命名不统一、层级嵌套混乱,只用XPath经常踩到“今天能抓到明天抓不到”的坑。我一般会两层配合:XPath负责把整块成绩卡片切出来,再用正则从卡片文本里提取总分和听、读、写译三项。

import re from lxml import html def parse_scores(page_source: str) -> list: doc = html.fromstring(page_source) cards = doc.xpath('//div[contains(@class, "score-card")]') if not cards: # 结构改版时的快速失败 return [] results = [] for card in cards: text = " ".join(html.tostring(card, encoding="unicode").split()) total = re.search(r"总分[^\d]{0,10}(\d{2,3})", text) listening = re.search(r"听力[^\d]{0,10}(\d{2,3})", text) reading = re.search(r"阅读[^\d]{0,10}(\d{2,3})", text) writing = re.search(r"写作[^\d]{0,10}(\d{2,3})", text) results.append({ "total": int(total.group(1)) if total else None, "listening": int(listening.group(1)) if listening else None, "reading": int(reading.group(1)) if reading else None, "writing": int(writing.group(1)) if writing else None, }) return results

正则里[^\d]{0,10}是为了容忍“总分”和数字之间可能夹着空格、冒号或标签碎片。注意先把页面用" "join碾平再正匹配,是为了一次性吸收换行符和多余空白,避免跨标签匹配失败。如果返回空列表,第一反应不是改正则,而是先dump一页HTML到本地看结构——正则解千愁的前提是结构还在老轨道上。

4. 跑通全流程:环境配置、模拟登陆与首次查分

前面讲的是模块逻辑,这一章直接按操作顺序走一遍:环境、登录、查询、打印结果。全程在手机Termux和电脑上都能跑,命令行步骤完全一致,区别只在依赖安装那段。

4.1 依赖安装:版本怎么锁才不翻车

requests和lxml两个库就够,不要为“性能”盲目装scrapy、playwright这类重武器。在Android Termux里先装Python,再装包:

pkg update && pkg install python python -m pip install requests lxml

电脑端建议建个虚拟环境再装,避免污染系统Python。版本上我没有刻意锁死,但建议requests固定在2.31以上、lxml固定在4.9以上——这两个版本序列对移动端UA和HTTPS TLS握手兼容性最稳。装完先跑一句查询验证环境:

python -c "import requests, lxml; print(requests.__version__, lxml.__version__)"

如果这里报module not found,多半是pip装到了别的Python解释器上。Termux下常见坑是系统装了两套Python,解决办法是直接用python -m pip install,而不是裸pip。

4.2 实操:模拟登陆一次,把Cookie留到本地

写一个最小可跑的main.py,把登录和查询串起来。简化结构,逻辑和前面模块一一对应:

import argparse import json import requests def main(): parser = argparse.ArgumentParser(description="学信网四六级查询") parser.add_argument("--identity", required=True, help="身份证号") parser.add_argument("--password", default="", help="学信网密码,已登录可留空") parser.add_argument("--zkzh", default="", help="准考证号,不填则走学信档案免证查询") parser.add_argument("--name", default="", help="姓名,与准考证号配对使用") args = parser.parse_args() session = requests.Session() if args.password: # 有密码走完整登录,把Cookie落盘 load_or_login(session, args.identity, args.password) else: # 没密码就尝试加载本地Cookie,失效会强制要求密码 load_or_login(session, args.identity, "PLACEHOLDER_FORCE_RELOGIN") if args.zkzh: page = query_by_ticket(session, args.zkzh, args.name) else: page = query_from_archive(session) print(json.dumps(parse_scores(page), ensure_ascii=False, indent=2)) if __name__ == "__main__": main()

运行顺序是:先跑一次带--identity和--password的登录流程,让程序把Cookie写到chsi_cookies.json;之后再用--password ""跑查询,程序发现本地Cookie有效就直接走查询。首次登录时终端会卡在input("打开 captcha.png 输入四位验证码: ")那儿,你需要另开一个终端打开captcha.png看图,输入后回车就通了。

注意:命令行里出现明文密码会留在shell历史里,正式使用建议把密码放到环境变量或配置文件里,脚本内用os.environ读取。

4.3 查询实操:两条查询路径的参数差异

有准考证号时:

python main.py --identity 你的身份证号 --password 你的密码 --zkzh 准考证号 --name 姓名

没准考证号时则去掉--zkzh:

python main.py --identity 你的身份证号 --password 你的密码

第二条走学信档案路径,返回的是该身份证名下所有历史四六级成绩,解析器会逐条输出。输出是带缩进的JSON,总分、听力、阅读、写作与翻译一目了然。到这里,“模拟登陆→查询→解析”的完整闭环就算通了。第一次跑通之后,你会发现后续再查分根本不需要重复登录——这就是Cookie持久化带来的效率。

5. 避坑手册:验证码会话、IP风控与页面结构漂移

这套流程跑顺了很快,但每个人几乎都会在下面几条里至少踩一次。每一条都是实操现场的真实教训,按“现象→原因→解决”写清楚。

5.1 人工输入的验证码总是提示错误

现象:盯着验证码图看了半天,输入后返回页面还是“验证码不正确”,连续试三次都这样。

原因:最常见的是验证码图片与POST请求不在同一个会话里。有人是先打开浏览器登录页看图,再把验证码填到脚本里——那个验证码令牌已经绑定在浏览器的Session上,脚本的Session根本没拿到对应凭据。

解决:用脚本的Session把验证码图片下载到本地再输入。这样验证码所属会话与POST会话严格一致,成功率立刻拉到95%以上。也就是3.1那段的逻辑:session.get(LOGIN_BASE + captcha_src[0])把图片二进制写到本地,确认后回车提交。

captcha_img = doc.xpath('//img[contains(@id,"captcha")]/@src')[0] img_resp = session.get(LOGIN_BASE + captcha_img, timeout=10) Path("captcha.png").write_bytes(img_resp.content) captcha = input("打开 captcha.png 输入四位验证码: ")

逻辑说明:先通过XPath取验证码图片地址,再用同一个Session下载图片到本地打开查看,保证图片令牌和提交的Cookie对齐。这是模拟登陆里最容易忽略的会话一致性问题,没有之一。

5.2 连续查询几个准考证号后接口返回403或安全验证页

现象:单查一个成绩完全OK,连续查五个后突然返回403或者被要求滑块验证。

原因:学信网/考试院接口对同一Session在短时间内的请求次数有限速,连续高频请求直接触发风控。这不是账号被冻结,只是临时限流,过段时间会恢复。

解决:在查询循环里加随机间隔,单位是秒而非毫秒。

import random, time for zkzh in ticket_list: results.append(parse_scores(query_by_ticket(session, zkzh, name))) time.sleep(random.uniform(3, 7))

逻辑说明:随机sleep的作用是让请求时间分布接近真人操作,而不是固定间隔——固定间隔同样能被风控识别成脚本特征。参数3到7是根据经验得出的区间,过短无保护作用;过长批量任务会拖太久。

5.3 Cookie昨天还能用,今天突然报“请先登录”

现象:本地chsi_cookies.json明明还在,运行load_or_login也走了session.cookies.update,但查询结果还是被重定向到登录页。

原因:学信网的会话Cookie有时效,跨天或长时间闲置后服务端会使会话过期。本地文件里有Cookie不代表服务端还认。

解决:load_or_login里的探测不能只靠“文件存在”判断,必须发一个轻量请求看响应特征。最常见做法是访问学信档案首页并检查里面是否有“退出登录”字样,有就继续用,没有就删掉缓存强制重新登录。这也是3.1节那段探测逻辑的由来。另外检查一下设备系统时间——曾经遇到过手机时区不对导致Cookie签名校验失败,时间校正后就恢复了。

5.4 解析器返回空列表,但浏览器里能正常看到成绩

现象:同一个身份证在浏览器访问学信档案能看到成绩列表,但脚本parse_scores返回[],页面源码也存下来了。

原因:页面结构改版,class名换了,XPath表达式匹配不到容器节点。这种情况90%是前端结构调整,10%是登录态半失效导致页面里加载的是空壳模板。

解决:把存下来的HTML拖进浏览器DevTools,搜“score”或“总分”关键字符串,找到新的容器class并更新XPath。更新完解析器后,一定要保留一份当时页面的HTML快照做回归测试——改一次XPath就回归一次,能省掉后面无数轮翻车。

# 保存快照,便于解析异常时取证 Path("page_snapshot.html").write_text(page, encoding="utf-8")

代码说明:这段快照保存逻辑应该挂在查询和解析之间,一旦解析异常立刻就能拿现场去DevTools排查,而不是去线上重放。排查完再决定改XPath还是改正则,别盲改。

6. 进阶:把一次性模拟登陆脚本封装成可维护的查询工具

到这一步,登录、查询、解析都能跑通了,但脚本还是一次性的——参数靠命令行传,结果靠print输出。真正能长期用的工具,应该把这三件事封装成类,再输出结构化JSON。把脚本里的函数收拢成一个ChsiClient类,登录态和查询方法都挂在实例上。

class ChsiClient: def __init__(self, identity, password, cookie_file="chsi_cookies.json"): self.identity = identity self.password = password self.session = requests.Session() self.cookie_file = Path(cookie_file) def login(self): load_or_login(self.session, self.identity, self.password) def cet_query(self, zkzh, name): return parse_scores(query_by_ticket(self.session, zkzh, name)) def archive_query(self): return parse_scores(query_from_archive(self.session))

这样一个对象就能同时服务命令行和一个简单的Flask接口。命令行侧用argparse已经够用;如果后续要接App,只需要在这个类外面套一个Flask路由,把JSON结果原样返回。结构化输出比print重要——成绩数据要能进Excel或数据库,JSON是最低摩擦的形式。

验证方法也固定下来:跑一个已知准考证号考生的成绩,把脚本返回的总分、听力、阅读、写作与翻译四项数值和官网页面逐项对照,数值完全一致才算这次改动通过。

我从那以后每次改完解析器或登录逻辑,都强制把这个回测流程走一遍——先信任脚本,再用已知数据打脸自己,确认没误伤再落地。这套习惯救过我太多次,尤其是有一次学信网把成绩单页从表格改成卡片布局,我的新XPath就是靠回测才发现少解析了一项。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 22:49:33

桌面宠物游戏开发全流程:从技术选型到发售的实战拆解

1. 一款桌面宠物游戏从立项到发售的全流程拆解《Pixel Kitten Desktop》这个名字第一次出现在我视野里的时候,我正蹲在一个独立游戏开发群里看人聊天。有人甩了张截图,一只像素风的小猫趴在任务栏上打哈欠,底下配文“正式发售”。说实话&…

作者头像 李华
网站建设 2026/10/23 13:36:32

CEGUI 0.7.4输入法集成案例:从消息路由到候选词窗口

简介:针对CEGUI-0.7.4中文输入法场景的配套代码包,面向需要在游戏或工具界面中实现中文文本输入的开发者。由于完整SDK体量过大,作者仅整理输入法相关代码与资源,可在已有CEGUI-0.7.4环境(需支持中文显示)中…

作者头像 李华
网站建设 2026/10/22 12:24:43

GermOS嵌入式平台:为微生物实时监测设备打造统一调度系统

如果你和我一样,常年和菌落、培养皿、传感器、环境监测打交道,大概率碰到过同一个尴尬:设备买了好几台,每个厂家的数据格式都不一样,接口一个比一个难对接。去年我在调试一套微生物实时监测装置时,被底层软…

作者头像 李华
网站建设 2026/10/19 14:32:32

告别来回切窗口:Proxima CLI 12个命令让你在终端里指挥全部AI

【免费下载链接】Proxima Multi-AI MCP Server - Connect ChatGPT, Claude, Gemini & Perplexity to your coding tools without any API 项目地址: https://gitcode.com/gh_mirrors/proxima1/Proxima 点击查看 免费下载 Proxima 是一款 Multi-AI MCP Server&a…

作者头像 李华
网站建设 2026/10/22 6:15:40

MATLAB实现CGH全息图生成:角谱衍射与GS算法相位恢复函数库实践

简介:面向全息显示、计算光学成像等领域的开源MATLAB函数库,旨在降低计算机生成全息图(CGH)算法的编程门槛,适合从事数字全息、衍射光学元件设计的研究者与工程师。库内封装了从相位生成到传播重构的完整流程&#xff…

作者头像 李华