尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

鸿蒙教务查询软件:JSoup解析HTML与ArkTS实战

鸿蒙教务查询软件:JSoup解析HTML与ArkTS实战 简介这份资源是面向高校学生与鸿蒙开发初学者的课程设计完整项目包主题为基于JSoup的鸿蒙教务查询软件帮助开发者在HarmonyOS平台上实现课程表、成绩等教务信息的抓取与展示。项目覆盖鸿蒙开发环境搭建、JSoup库集成、网络请求与安全、HTML数据解析与UI展示、用户体验优化及分布式能力探索等环节适合已具备Java与安卓基础、希望迁移到鸿蒙生态的开发者练手。压缩包共183个文件约3.1MB以38个java源码、64个xml布局与配置、50个png图片资源为主另含json、gradle构建脚本及har、jar依赖文件结构完整可直接导入DevEco Studio运行。目前已有448人学习下载读者可借此掌握JSoup解析教务网页的实战思路、鸿蒙网络请求与数据绑定方法并理解跨平台开发中技术复用的具体路径。1. 鸿蒙教务查询软件为什么用 JSoup 而不是官方 API教务系统没有开放接口这是做校园类应用时最先撞上的墙。鸿蒙开发课程设计里基于 JSoup 的鸿蒙教务查询软件这个选题之所以反复出现是因为它绕开了「等学校给 API」这条死路用 JSoup 直接解析教务系统返回的 HTML把课表、成绩、考试安排从页面里抠出来再在鸿蒙端渲染成原生界面。它解决的是学生查成绩要反复登录、页面在手机上排版错乱、课表没法离线看这几个具体痛点适合正在做鸿蒙应用开发课程设计、手里只有一台真机或模拟器、又不想把时间全花在后端接口上的同学。JSoup 本身是 Java 生态里成熟的 HTML 解析库鸿蒙的 ArkTS 虽然不能直接跑 Java 字节码但通过 HTTP 请求拿到 HTML 字符串后用正则或轻量解析逻辑处理思路和 JSoup 完全一致——这也是这个选题在鸿蒙语境下仍然成立的原因。2. 从登录到解析教务查询的完整链路怎么搭2.1 先搞清楚教务系统的三种典型形态动手之前得先摸清目标教务系统长什么样这决定了后面 JSoup 解析策略怎么写。常见的有三类第一类是纯表单登录POST 账号密码后返回一个带 session 的页面课表和成绩都在后续 GET 请求的 HTML 表格里第二类是带验证码的登录请求要多带一个验证码字段验证码图片通常是独立 URL第三类是前后端分离的页面数据通过 JSON 接口返回这种其实用不上 JSoup直接解析 JSON 更省事。课程设计里最常遇到的是第一类和第二类因为老牌教务系统比如各类正方、URP 的定制版基本都是服务端渲染。判断方法很简单用浏览器打开教务系统F12 切到 Network 面板勾选 Preserve log完整走一遍登录加查成绩的流程看每个请求的 Response 是 HTML 还是 JSON。如果是 HTML且表格里有table、tr、td这类标签那 JSoup 就是对的工具。这一步别偷懒我见过有人上来就写解析代码结果发现目标系统返回的是加密后的 JSON白写两天。2.2 鸿蒙端的 HTTP 请求与 Cookie 管理鸿蒙的 ArkTS 里发 HTTP 请求用ohos.net.http模块核心是http.createHttp()然后调request。教务系统的登录态靠 Cookie 维持所以第一次登录成功后必须把响应头里的Set-Cookie存下来后续请求手动带上。下面是一个最小可跑的登录加取页面的代码骨架import http from ohos.net.http; // 全局保存 session cookie实际项目建议用 PersistentStorage 持久化 let sessionCookie: string ; async function loginAndFetch(url: string, username: string, password: string): Promisestring { const httpRequest http.createHttp(); // 第一步POST 登录表单格式和浏览器里看到的一致 const loginResp await httpRequest.request(url /login, { method: http.RequestMethod.POST, header: { Content-Type: application/x-www-form-urlencoded }, extraData: username${username}password${password}, expectDataType: http.HttpDataType.STRING }); // 从响应头提取 Set-Cookie注意 header 的 key 大小写不固定 const setCookie loginResp.header[Set-Cookie] || loginResp.header[set-cookie]; if (setCookie) { // 只取分号前的键值对后面的 Path、HttpOnly 等属性不需要 sessionCookie (setCookie as string).split(;)[0]; } // 第二步带着 cookie 请求成绩页面 const gradeResp await httpRequest.request(url /grade, { method: http.RequestMethod.GET, header: { Cookie: sessionCookie }, expectDataType: http.HttpDataType.STRING }); httpRequest.destroy(); // 用完销毁避免连接泄漏 return gradeResp.result as string; }逻辑说明extraData传的是表单编码后的字符串字段名必须和浏览器里实际提交的一致这个从 Network 面板的 Payload 里抄。expectDataType设成 STRING 是为了直接拿到 HTML 文本如果设成 ARRAY_BUFFER 还得自己解码。参数方面header里的Content-Type在 POST 时必须是application/x-www-form-urlencoded否则服务端可能不认Cookie 提取时要注意鸿蒙返回的 header key 可能是Set-Cookie也可能是set-cookie两个都判一下更稳。2.3 用 JSoup 思路解析 HTML 表格鸿蒙端没有 JSoup 的 ArkTS 移植版但解析逻辑可以照搬 JSoup 的选择器思维。教务系统的成绩和课表基本都是table结构行是tr单元格是td。下面这段代码用正则加字符串切分的方式把成绩表格里的「课程名、学分、成绩」三列抽出来interface GradeItem { courseName: string; credit: string; score: string; } function parseGradeTable(html: string): GradeItem[] { const result: GradeItem[] []; // 先定位到成绩表格教务系统通常给 table 一个 id 或 class // 这里用「包含成绩关键词的 table」做粗略匹配实际按页面结构调整 const tableMatch html.match(/table[^]*idgradeTable[\s\S]*?\/table/i); if (!tableMatch) return result; const tableHtml tableMatch[0]; // 按 tr 切分行跳过表头行 const rows tableHtml.split(/tr[^]*/i).slice(1); for (const row of rows) { // 提取每行里的 td 内容 const cells row.match(/td[^]*([\s\S]*?)\/td/gi); if (!cells || cells.length 3) continue; // 去掉 td 标签本身和内部可能存在的 span、a 标签只留文本 const clean (s: string) s.replace(/[^]/g, ).trim(); result.push({ courseName: clean(cells[0]), credit: clean(cells[1]), score: clean(cells[2]) }); } return result; }逻辑说明先用正则把整个table块抠出来避免解析到页面其他表格。然后按tr切行slice(1)跳过表头。每行再用正则匹配tdclean函数负责剥掉嵌套的span、a等标签只保留纯文本。参数上idgradeTable这个选择器要换成你目标教务系统里实际的 id 或 class这个从浏览器 Elements 面板里直接看。如果表格没有 id可以用class或者表格前面的标题文字做定位。提示教务系统的 HTML 经常有编码问题如果解析出来是乱码检查响应头里的Content-Type是否带charsetgbk鸿蒙端需要手动做 GBK 到 UTF-8 的转换或者用TextDecoder指定编码。3. 鸿蒙端 UI 渲染与数据绑定把解析结果变成课表3.1 用 List 和 Grid 搭出可用的课表界面解析出来的数据最终要落到界面上。课表适合用Grid组件成绩列表适合用List。鸿蒙的 ArkUI 声明式写法里数据驱动 UI 的核心是State装饰器数组一变界面自动刷新。下面是一个课表网格的简化实现Entry Component struct SchedulePage { // 7 列对应周一到周日每列是一个课程数组 State schedule: string[][] [[], [], [], [], [], [], []]; build() { Column() { Grid() { ForEach(this.schedule, (dayCourses: string[], dayIndex: number) { GridItem() { Column() { Text([一,二,三,四,五,六,日][dayIndex]) .fontSize(14).fontWeight(FontWeight.Bold) ForEach(dayCourses, (course: string) { Text(course) .fontSize(12) .padding(4) .backgroundColor(#E8F0FE) .borderRadius(4) .margin({ top: 4 }) }) } } }) } .columnsTemplate(1fr 1fr 1fr 1fr 1fr 1fr 1fr) .rowsGap(8) .columnsGap(4) .padding(12) } .width(100%) .height(100%) } }逻辑说明State修饰的schedule是二维数组外层 7 个元素对应一周七天内层是当天的课程名列表。Grid的columnsTemplate设成 7 个1fr表示等分七列。ForEach嵌套使用外层遍历天内层遍历课程。参数上rowsGap和columnsGap控制间距borderRadius给每个课程块加圆角这些按视觉需要调。实际项目里课程块可能还要显示节次和教室把string换成对象数组即可。3.2 数据持久化让课表在没网时也能看教务查询软件的一个刚需是离线看课表因为教室信号经常不好。鸿蒙端持久化用ohos.data.preferences适合存 JSON 字符串这种小数据。下面是把解析结果存下来和读出来的代码import preferences from ohos.data.preferences; const STORE_NAME schedule_store; const KEY_SCHEDULE schedule_json; async function saveSchedule(context: Context, schedule: string[][]): Promisevoid { const store await preferences.getPreferences(context, STORE_NAME); await store.put(KEY_SCHEDULE, JSON.stringify(schedule)); await store.flush(); // 必须 flush否则可能不落盘 } async function loadSchedule(context: Context): Promisestring[][] | null { const store await preferences.getPreferences(context, STORE_NAME); const raw await store.get(KEY_SCHEDULE, ) as string; if (!raw) return null; return JSON.parse(raw) as string[][]; }逻辑说明getPreferences拿到的是整个存储实例put写入键值对flush强制同步到磁盘。读取时给一个空字符串默认值判断为空就返回 null调用方据此决定是走网络请求还是直接用缓存。参数上STORE_NAME是文件名同一应用内唯一即可KEY_SCHEDULE是键名存多个数据时用不同键区分。注意flush是异步的不 await 的话可能在应用被杀时丢数据这个坑我踩过。4. 避坑与排查教务查询软件最容易翻车的五个地方4.1 登录成功但后续请求返回登录页现象POST 登录接口返回 200但紧接着请求成绩页面拿到的 HTML 里还是登录表单。原因通常是 Cookie 没带对或者服务端用的是JSESSIONID之外的 token 机制。解决先在浏览器里对比登录前后的 Cookie 变化确认哪个字段是会话标识鸿蒙端提取Set-Cookie时注意可能有多个 Cookie 字段需要全部拼接而不是只取第一个。另外有些教务系统会在登录成功后重定向鸿蒙的 http 模块默认不自动跟随重定向需要手动处理 302 响应。4.2 解析出来的中文全是乱码现象result里的中文显示成æˆç»©这种。原因是教务系统返回的 HTML 编码是 GBK而鸿蒙默认按 UTF-8 解码。解决在请求时把expectDataType设成ARRAY_BUFFER拿到字节数组后用util.TextDecoder指定gbk解码。如果不想改请求方式也可以在拿到字符串后用iconv-lite的鸿蒙移植版做二次转换但多一层依赖。4.3 表格结构一变解析就崩现象上学期能跑这学期教务系统改版解析结果全空。原因是正则写得太死绑定了具体的id或列顺序。解决解析逻辑里加容错比如先判断表格是否存在不存在就返回空数组而不是抛异常列索引不要写死用表头文字匹配来确定「课程名」在第几列。更稳的做法是把选择器配置化改版时只改配置不改代码。4.4 频繁请求被教务系统封 IP现象调试阶段反复登录突然所有请求都返回 403 或超时。原因是教务系统有频率限制或防火墙。解决调试时把响应缓存到本地文件解析逻辑用缓存数据跑不要每次都打网络请求正式使用时加请求间隔至少 2 秒一次登录失败不要无限重试加个三次上限。4.5 鸿蒙模拟器网络请求超时现象代码在真机上正常模拟器里请求一直超时。原因是模拟器的网络配置和宿主机不同某些教务系统的内网地址模拟器访问不到。解决确认教务系统是公网可访问还是仅校园网如果是内网模拟器需要配置代理或者直接用真机调试。另外鸿蒙模拟器的 DNS 解析偶尔有问题把 URL 里的域名换成 IP 试一下能快速定位。5. 进阶技巧用缓存加增量更新把查询速度压到一秒内课程设计做到能跑不难难的是让查询体验接近原生应用。我自己的习惯是加一层「缓存加增量更新」首次登录后把课表和成绩全量拉下来存本地之后每次打开应用先渲染缓存同时在后台发一个轻量请求检查数据是否有更新有更新再刷新界面。这样用户感知到的打开速度就是读本地存储的速度通常在一秒以内。判断数据是否更新的方法因教务系统而异。常见做法是请求一个「通知公告」或「学期信息」页面对比页面里的学期起止日期或公告数量是否变化。如果变了再触发全量拉取。下面是一个简单的版本号比对逻辑async function checkUpdate(context: Context, url: string): Promiseboolean { const store await preferences.getPreferences(context, schedule_store); const lastVersion await store.get(data_version, ) as string; const httpRequest http.createHttp(); const resp await httpRequest.request(url /notice, { method: http.RequestMethod.GET, header: { Cookie: sessionCookie } }); httpRequest.destroy(); // 从公告页面里提取一个能代表数据版本的字符串比如最新公告的日期 const html resp.result as string; const versionMatch html.match(/(\d{4}-\d{2}-\d{2})/); const currentVersion versionMatch ? versionMatch[1] : ; if (currentVersion currentVersion ! lastVersion) { await store.put(data_version, currentVersion); await store.flush(); return true; // 需要全量更新 } return false; }逻辑说明lastVersion存的是上次拉取时记录的版本标识这里用公告页面里第一个日期做代表。每次启动时请求公告页提取当前版本和本地比对不一致就返回 true 触发全量更新。参数上data_version这个键名随意只要和保存时一致即可版本标识的提取正则要根据实际公告页面的格式调整有的系统日期格式是2024年03月15日正则要跟着改。注意增量更新不要在应用启动时同步阻塞放到aboutToAppear之后异步执行否则首屏渲染会被网络请求拖慢。另一个值得做的优化是解析结果的缓存格式。不要存原始 HTML存解析后的结构化 JSON这样即使教务系统页面结构变了只要缓存还在用户至少能看到上次的数据不会白屏。我一般会在preferences里同时存schedule_json和grade_json两个键各自独立更新课表更新频率低、成绩更新频率高分开存避免互相影响。最后说一个调试习惯教务系统的 HTML 结构千奇百怪别指望一次写对解析逻辑。我的做法是先把目标页面的 HTML 保存到本地文件写一个 Node.js 脚本用 cheerioJSoup 的 JS 等价物快速验证选择器验证通过后再把逻辑翻译成 ArkTS 的正则版本。这样调试循环从「改代码、编译、装应用、登录」缩短到「改脚本、跑一下」效率差好几倍。这个方案值不值得做如果你只是交课程设计把登录加解析加列表渲染跑通就够了但如果你想让它真正可用缓存和增量更新这两层是分水岭加上之后体验完全不一样。希望帮到你。本文还有配套的精品资源点击获取
返回列表