简介:这是一份面向高校学生与鸿蒙开发初学者的课程设计资源,围绕华为HarmonyOS平台,结合Java库JSoup实现教务信息查询功能,帮助开发者在安卓替代方案中掌握跨平台应用构建思路。压缩包共183个文件,约3.1MB,以38个java源码、64个xml布局与配置、50个png图片资源为主,另含12个json、7个jpg及gradle构建脚本、har包与jar依赖等,覆盖工程配置、界面资源与核心逻辑,目录结构完整便于按模块查阅。资源重点演示鸿蒙开发环境搭建、JSoup集成与HTML解析、网络请求与HTTPS安全处理、课程表与成绩等数据提取展示,并涉及分布式能力探索与测试调试思路,适合作为课程设计参考或鸿蒙入门练手项目。目前已有450人学习下载,可帮助读者理解如何将既有Java技术栈迁移到鸿蒙生态,提升跨平台开发能力。
1. 鸿蒙教务查询软件:用 JSoup 把课表和成绩抓进 HarmonyOS
期末前一周,室友还在用浏览器反复登录教务系统、手动抄课表和成绩,我已经把查询结果直接渲染在鸿蒙手机的应用里了。这个课程设计标题说的就是这件事:在 HarmonyOS 上做一个教务查询客户端,用 JSoup 解析教务系统返回的 HTML,把课表、成绩、考试安排结构化后展示出来。它解决的是「教务系统只有网页版、没有开放 API」这个现实问题,适合正在做鸿蒙课程设计、想找一个能跑通又有技术含量的题目的同学。核心链路只有三步:登录拿 Cookie、请求目标页面、JSoup 解析 DOM。难点不在鸿蒙 UI,而在登录态维持和 HTML 结构变化后的容错。下面按我实际做过的顺序拆开讲。
2. 先想清楚:为什么用 JSoup 而不是等官方接口
2.1 教务系统的现实约束决定了技术选型
绝大多数高校教务系统是十几年前的老架构,页面由服务端渲染,返回的是完整 HTML,没有 REST 接口,也没有 JSON。想拿数据只有两条路:一是模拟浏览器发请求,把 HTML 抓回来自己解析;二是用 WebView 加载页面再注入 JS 提取 DOM。前者轻、可控、省电,后者重但能绕过部分 JS 加密。
JSoup 属于前者。它是一个 Java 的 HTML 解析库,能像 jQuery 一样用 CSS 选择器定位元素,select("table#scoreList tr")这种写法直接对应页面结构。鸿蒙的应用开发主语言是 ArkTS,但 HarmonyOS 支持通过 Native API 或三方库方式调用 Java 能力,课程设计里更常见的做法是把 JSoup 的解析逻辑放在一个 Java 模块里,或者干脆用鸿蒙的网络能力拿到 HTML 字符串后,用移植版的解析逻辑处理。
选 JSoup 的核心理由是「页面结构即接口」。教务系统的表格、div、input的name属性相对稳定,改版频率低,用选择器抓取比逆向加密参数省事得多。热搜里常出现的 gradle 配置、gradle 离线包这些问题,在做这个项目时也会遇到——因为 JSoup 是 Java 生态的库,引入它就要处理依赖管理。
2.2 鸿蒙侧的网络请求与登录态维持
鸿蒙的网络能力用@ohos.net.http模块,发 POST 请求拿登录后的 Cookie 是第一步。教务系统登录一般是表单提交,字段通常是username、password、encoded之类,具体要看目标系统的登录页源码。
import http from '@ohos.net.http'; // 登录请求:表单格式提交,拿到 Set-Cookie async function login(baseUrl: string, user: string, pwd: string): Promise<string> { const httpRequest = http.createHttp(); const resp = await httpRequest.request(`${baseUrl}/loginAction.do`, { method: http.RequestMethod.POST, header: { 'Content-Type': 'application/x-www-form-urlencoded' }, extraData: `username=${user}&password=${pwd}&loginType=1`, // 关键:手动管理 Cookie,不依赖系统自动存储 expectDataType: http.HttpDataType.STRING }); // 从响应头里取 Set-Cookie,后续请求带上 const cookie = resp.header['set-cookie'] as string; httpRequest.destroy(); return cookie; }这段代码的逻辑是:用 POST 把账号密码按表单格式发出去,服务端验证通过后在响应头Set-Cookie里下发会话标识。参数说明上,Content-Type必须是application/x-www-form-urlencoded,否则服务端收不到字段;extraData拼接时要注意 URL 编码,密码里如果有&、=这类字符不编码会截断。拿到 Cookie 后,后续每次请求都要在 header 里带上Cookie: xxx,否则会被重定向回登录页。
提示:部分教务系统登录时会带一个动态的
encoded参数,由页面上的 JS 生成。遇到这种情况,先用 WebView 加载登录页,通过runJavaScript取出该值再提交,不要硬编码。
2.3 用 JSoup 解析课表 HTML 的最小示例
假设已经拿到课表页面的 HTML 字符串,解析逻辑如下。这里用 Java 写,因为 JSoup 原生是 Java 库,课程设计里可以放在独立模块,也可以参考社区移植方案在 ArkTS 侧调用。
import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; public class ScheduleParser { public static void parse(String html) { Document doc = Jsoup.parse(html); // 课表通常是 table,每行一门课,选择器按实际页面调整 Elements rows = doc.select("table#kbtable tr"); for (Element row : rows) { Elements cells = row.select("td"); if (cells.size() < 3) continue; // 跳过表头或空行 String courseName = cells.get(1).text().trim(); String classroom = cells.get(2).text().trim(); System.out.println(courseName + " @ " + classroom); } } }逻辑说明:Jsoup.parse把字符串转成可查询的 DOM 树,select用 CSS 选择器定位。参数上,table#kbtable里的#kbtable是页面里表格的 id,实际项目要打开教务系统页面按 F12 看真实 id 或 class。cells.get(1)的下标取决于表格列顺序,不同学校不一样,必须对着页面数。text()会自动去掉标签只留文本,trim()处理多余空格。
这一步最容易翻车的地方是编码。教务系统常用 GBK,如果请求时按 UTF-8 解码,中文会变乱码。解决办法是在请求头里声明Accept-Charset,或者拿到字节流后用new String(bytes, "GBK")转换。
3. 从登录到渲染:把查询链路在鸿蒙上跑通
3.1 工程结构与 gradle 依赖配置
课程设计通常是一个 HarmonyOS 工程加一个 Java 解析模块。如果解析逻辑放在 Java 侧,build.gradle里要加 JSoup 依赖:
dependencies { // JSoup 核心库,用于 HTML 解析 implementation 'org.jsoup:jsoup:1.17.2' }参数说明:版本号选一个稳定版即可,1.17.x 对 Java 8 兼容良好。热搜里「gradle 离线包」「gradle 打包打半天」的问题,多半是依赖下载卡住。解决办法是配置国内镜像仓库,或者提前把 jar 下好放进libs目录用implementation files('libs/jsoup-1.17.2.jar')引入。gradle的repositories块里把mavenCentral()换成镜像地址能明显提速。
注意:鸿蒙工程默认用 hvigor 构建,Java 模块和 ArkTS 模块的依赖管理是分开的。别把 JSoup 直接写进鸿蒙主模块的依赖里,放错位置会报找不到符号。
3.2 成绩查询的解析与数据建模
成绩页和课表页结构不同,通常是一个带表头的表格,每行包含课程名、学分、成绩、绩点。解析时先定位表头确认列顺序,再逐行取值。
public class ScoreParser { public static void parse(String html) { Document doc = Jsoup.parse(html); Elements rows = doc.select("table#scoreTable tbody tr"); for (Element row : rows) { Elements tds = row.select("td"); if (tds.size() < 4) continue; String name = tds.get(1).text(); String credit = tds.get(2).text(); String score = tds.get(3).text(); // 只保留数字成绩,过滤“优秀”“良好”这类等级制 if (score.matches("\\d+(\\.\\d+)?")) { System.out.println(name + " " + credit + " " + score); } } } }逻辑说明:tbody tr比直接tr更精确,能避开表头行。matches用正则过滤非数字成绩,因为很多学校体育、选修课是等级制,直接算 GPA 会抛异常。参数上,tds.get(1)到get(3)的下标同样要按实际页面核对,有的系统第一列是序号,有的没有。
数据建模建议定义一个Course类,字段包括名称、学分、成绩、绩点,解析完存进List<Course>,再通过鸿蒙的@State或AppStorage传给 UI 层渲染。这样解析和展示解耦,页面改版时只改解析器。
3.3 鸿蒙 UI 侧渲染课表与成绩
拿到结构化数据后,鸿蒙侧用List或Grid渲染。课表适合Grid,成绩适合List。
@Component struct ScoreList { @State scores: ScoreItem[] = []; build() { List({ space: 8 }) { ForEach(this.scores, (item: ScoreItem) => { ListItem() { Row() { Text(item.name).layoutWeight(1).fontSize(16) Text(item.score).fontSize(16).fontColor('#E64545') }.width('100%').padding(12) } }, (item: ScoreItem) => item.name) }.width('100%') } }逻辑说明:@State修饰的数组变化会触发 UI 刷新,ForEach的第三个参数是 key 生成函数,用课程名做 key 能避免重复渲染。参数上,layoutWeight(1)让课程名占满剩余空间,成绩靠右对齐。数据从解析层传过来时,注意线程切换——网络和解析不能在主线程做,用TaskPool或Worker放到后台,完成后回主线程更新@State。
4. 避坑与排查:教务查询最容易翻车的五个点
4.1 登录成功但后续请求跳回登录页
现象:登录接口返回 200,但请求课表时拿到的 HTML 是登录页。原因:Cookie 没带上,或者带上了但格式不对。解决:检查请求头里Cookie字段是否完整,有些系统要求JSESSIONID和另一个route字段都带。用抓包工具对比浏览器请求和你的请求,逐字段对齐。
4.2 中文全部变成乱码
现象:解析出来的课程名是「课ç¨」这类字符。原因:教务系统用 GBK 编码,而请求或解析时按 UTF-8 处理。解决:在拿到响应字节后显式用new String(bytes, Charset.forName("GBK"))解码,或者在 JSoup 解析前先转好字符串。鸿蒙侧http请求如果直接拿字符串,要在header里加Accept-Charset: gbk。
4.3 选择器今天能用明天就抓不到
现象:select("table#kbtable tr")突然返回空。原因:教务系统改版,id 或 class 变了,或者页面结构从 table 改成了 div。解决:把选择器写成多套备选,按优先级尝试;解析前先判断doc.select(...).isEmpty(),为空时记录原始 HTML 到日志,方便定位。别把选择器硬编码在一个地方,抽成常量集中管理。
4.4 gradle 同步失败导致 JSoup 引入不了
现象:build.gradle里加了依赖,同步时报Could not resolve org.jsoup:jsoup。原因:网络问题或仓库地址不可达。解决:换国内镜像仓库,或者下载 jar 手动放进libs目录用files方式引入。热搜里「gradle 离线包」说的就是这个场景,提前配好能省很多时间。
4.5 解析在主线程执行导致界面卡死
现象:点击查询后界面冻结几秒,然后才出结果。原因:网络请求和 HTML 解析都在主线程。解决:把整个查询链路包进TaskPool.execute,解析完把结果通过sendData回传主线程。鸿蒙对主线程耗时操作有严格限制,超过阈值会直接抛异常。
5. 进阶:让解析器扛住改版,用配置化选择器兜底
做到能查之后,真正决定这个课程设计能不能拿高分的是「容错」。教务系统改版是常态,把选择器写死在代码里,改一次就要重新编译。我的习惯是把选择器抽成一份配置,放在resources/rawfile下,解析时读取配置。
{ "schedule": { "table": "table#kbtable, table.kb, table[class*=schedule]", "row": "tr", "nameIndex": 1, "roomIndex": 2 }, "score": { "table": "table#scoreTable, table[class*=score]", "row": "tbody tr", "nameIndex": 1, "creditIndex": 2, "scoreIndex": 3 } }解析器按逗号分隔依次尝试每个选择器,命中第一个非空结果就用。nameIndex这类下标也配置化,改版时只改 JSON 不重新编译。这个思路在多个学校系统上验证过,能覆盖大部分结构微调。
验证方法上,我一般准备三份 HTML 样本:正常页面、改版后页面、登录失效页面,写单元测试分别断言解析结果。正常页面断言课程数大于 0,改版页面断言不抛异常且能降级,登录失效页面断言能识别出「请重新登录」关键字并提示用户。
| 场景 | 输入 | 期望行为 |
|---|---|---|
| 正常课表 | 标准 HTML | 解析出课程列表 |
| 结构改版 | id 变化 | 备选选择器命中 |
| 登录失效 | 登录页 HTML | 识别并提示重新登录 |
| 编码异常 | GBK 字节流 | 正确解码中文 |
最后说个血泪经验:别在解析器里写System.out.println调试完就留着,鸿蒙打包时这些输出会拖慢启动。调试用日志框架,上线前关掉。这个课程设计的技术含量不在 UI 多花哨,而在你能不能把「登录态 + 编码 + 选择器容错」这三件事做扎实。希望帮到你。
本文还有配套的精品资源,点击获取