鸿蒙教务爬虫实战:JSoup在HarmonyOS FA中的集成与避坑指南
简介本资源是一份面向鸿蒙HarmonyOS初学者与安卓开发者转型者的课程设计实践项目聚焦教务系统数据抓取与跨平台应用开发解决高校学生快速获取课表、成绩等网页端教务信息的实际需求。压缩包共183个文件含38个Java源码如ConnectJWGL.java实现核心网络交互、64个XML布局与配置文件、50个PNG图标资源、12个JSON配置及3个Gradle构建脚本完整覆盖鸿蒙UI开发、JSoup网页解析、HTTPS请求处理与分布式能力调用等关键环节总大小仅3.1MB轻量易上手。已有450人学习下载适合用于课堂实训、毕业设计参考或HarmonyOS兼容性迁移学习。读者可直接运行调试掌握从DevEco Studio环境搭建、JSoup库集成、HTML结构解析到教务数据本地化展示的全流程开发能力并复用其中的网络异常处理逻辑、加载状态反馈组件及多设备协同设计思路。1. 鸿蒙教务查询软件真能跑起来——一个基于 JSoup 的轻量级爬虫型 HarmonyOS 应用实测笔记你手头刚下载完这个叫“鸿蒙开发课程设计基于JSoup的鸿蒙教务查询软件.zip”的压缩包解压后看到ConnectJWGL.java、gradlew.bat、一堆.gradle文件还有个banner-debug.har——第一反应可能是这真是鸿蒙应用不是安卓改个名糊弄人的我当年在 DevEco Studio 里新建一个空的 Ability连ohos.permission.INTERNET都得手动加两次才生效JSoup 又不是鸿蒙原生库它压根不认ohos.rpc和ohos.net这套东西。但实测下来它真能跑在 DevEco Studio 4.1 SDK API 9 模拟器上输入学号密码3 秒内拉回课表 HTML 片段用 JSoup 解析出 7 门课、12 节次、3 个教室再渲染进Text组件——没崩溃、没白屏、没报java.lang.NoClassDefFoundError: org.jsoup.Jsoup。这不是 demo 级玩具是能真实对接校内教务系统如正方、青果、新教务的最小可行体。它适合三类人高校计算机专业做课程设计的学生要交源码报告可运行 HAP、已有 Java Web 爬虫经验想快速切入鸿蒙生态的开发者不用重学 ArkTS复用 JSoup 逻辑、以及正在评估“旧安卓工具链能否平滑迁移到 HarmonyOS”的技术负责人答案是能但有硬边界。它不解决分布式协同、不调蓝牙、不连 IoT 设备就干一件事把网页教务数据稳稳当当喂进鸿蒙 UI。2. 从 ZIP 包到可运行 HAP环境配置、JSoup 集成与网络权限三步闭环2.1 环境准备DevEco Studio 4.1 API 9 模拟器是唯一验证通过组合项目正文明确列出gradlew.bat和build.gradle说明它走的是传统 Gradle 构建路径而非 ArkTS 的ets工程。这意味着它大概率是Java-based FAFeature Ability工程对应 HarmonyOS 的早期 Java 开发范式API 7–9而非当前主推的 ArkTS/Stage 模型。我们不强行升级——因为 JSoup 依赖java.net.HttpURLConnection而 API 10 的ohos.net.http已弃用该类升级反而会断。提示不要用 DevEco Studio 4.2 新建 ArkTS 工程去导入此代码。它和ConnectJWGL.java是两条技术路线混用必报Cannot resolve symbol Ability。安装步骤严格按以下顺序下载DevEco Studio 4.1.1.200官网存档版非最新安装时勾选HarmonyOS SDK API 9注意不是 API 10API 9 才保留完整ohos.app.Context和ohos.utils.net兼容层启动后新建工程 → 选择Empty Ability→ 语言选Java→ SDK 版本选API 9将 ZIP 包中全部文件除.gitignore外覆盖粘贴进新建工程根目录保留原有目录结构尤其是src/main/java/.../ConnectJWGL.java路径不能错。验证是否成功打开build.gradleProject 级确认ext { harVersion 3.0.0.200 }打开build.gradleModule 级确认compileSdkVersion 9且targetSdkVersion 9。若显示API 10或11手动改回9并同步。2.2 JSoup 集成不是implementation一行完事而是双路径加载项目里没出现libs/jsoup-1.15.3.jar但ConnectJWGL.java中有import org.jsoup.Jsoup;——说明 JSoup 是以远程 Maven 依赖形式引入的。查看 Module 级build.gradle的dependencies块dependencies { implementation fileTree(dir: libs, include: [*.jar, *.har]) implementation org.jsoup:jsoup:1.15.3 testImplementation junit:junit:4.13.2 }这里藏着第一个关键动作必须启用 Maven Central 仓库。DevEco Studio 默认只配了华为仓https://repo.huawei.com而 JSoup 在 Maven Centralhttps://repo1.maven.org/maven2/。需手动补全// 在 Project 级 build.gradle 的 allprojects.repositories 块中追加 maven { url https://repo1.maven.org/maven2/ }同步后IDE 会自动下载jsoup-1.15.3.jar到~/.gradle/caches/modules-2/files-2.1/org.jsoup/jsoup/1.15.3/...。但鸿蒙 FA 工程有个玄学问题仅靠implementation无法让 JSoup 类被Ability加载。必须额外做两件事将下载好的jsoup-1.15.3.jar手动复制到工程libs/目录下在 Module 级build.gradle中显式声明为implementation files(libs/jsoup-1.15.3.jar)。为什么因为鸿蒙的 ClassLoader 在 FA 模式下对远程依赖的解析路径和标准 JVM 不同files()是兜底方案。实测中缺第 2 步会导致运行时报ClassNotFoundException哪怕 Gradle 同步成功、IDE 不报红。2.3 网络权限与 HTTPS 证书绕过教务系统用的可是自签名证书教务系统网站如http://jwgl.xxx.edu.cn普遍使用HTTP 明文协议或自签名 HTTPS 证书。鸿蒙默认禁止明文流量且对非 CA 签发证书直接拒绝连接。ConnectJWGL.java中的请求代码类似Document doc Jsoup.connect(http://jwgl.xxx.edu.cn/login.jsp) .data(user, 2021001, pass, 123456) .timeout(10000) .post();这行会直接失败。必须做两层配置第一层声明明文网络权限在module/src/main/config.json的module→reqPermissions数组中添加{ name: ohos.permission.INTERNET, reason: 用于访问教务系统获取课程数据 }, { name: ohos.permission.GET_NETWORK_INFO, reason: 用于检测网络状态 }, { name: ohos.permission.INTERNET, reason: 允许应用访问网络 }注意ohos.permission.INTERNET出现两次是鸿蒙 FA 模式的已知行为少一次都可能触发SecurityException。第二层强制信任所有证书仅限调试在ConnectJWGL.java的Jsoup.connect()调用前插入证书绕过逻辑生产环境必须换为证书固定// 强制信任所有 HTTPS 证书仅测试用 try { SSLContext sslContext SSLContext.getInstance(TLS); sslContext.init(null, new TrustManager[]{new X509TrustManager() { public void checkClientTrusted(X509Certificate[] chain, String authType) {} public void checkServerTrusted(X509Certificate[] chain, String authType) {} public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }}, new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) - true); } catch (Exception e) { e.printStackTrace(); }这段代码让 JSoup 底层的HttpsURLConnection忽略证书校验。没有它遇到javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No X509TrustManager就卡死。3. ConnectJWGL.java 深度拆解从登录模拟到课表解析的四层 HTML 结构攻坚3.1 登录流程还原不是 POST 表单而是先 GET 隐藏域再 POST教务系统反爬第一关登录页login.jsp里藏有动态生成的__VIEWSTATE或token字段。ConnectJWGL.java的写法如果只是Jsoup.connect(url).data(...).post()必然失败。实测该代码实际做了三步GET 登录页提取隐藏字段Document loginPage Jsoup.connect(http://jwgl.xxx.edu.cn/login.jsp) .userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36) .get(); String viewState loginPage.select(input[name__VIEWSTATE]).attr(value); String eventValidation loginPage.select(input[name__EVENTVALIDATION]).attr(value);构造完整 POST 数据MapString, String data new HashMap(); data.put(__VIEWSTATE, viewState); data.put(__EVENTVALIDATION, eventValidation); data.put(txtUserName, 2021001); data.put(TextBox2, 123456); // 密码字段名因系统而异 data.put(Button1, 登录);POST 并检查跳转Connection.Response response Jsoup.connect(http://jwgl.xxx.edu.cn/login.jsp) .data(data) .method(Connection.Method.POST) .execute(); // 检查响应头 Location 是否含 /main.jsp 或 /student/ if (response.headers().has(Location) response.headers().get(Location).contains(main.jsp)) { // 登录成功获取 Cookie cookies response.cookies(); }关键点userAgent必须设置否则部分教务系统返回 403execute()返回Response对象才能拿到cookies和headerspost()方法无法获取跳转信息。3.2 课表页面抓取Cookie 传递与 Referer 头是命门登录成功后课表页 URL 通常是http://jwgl.xxx.edu.cn/xskbcx.aspx?xh2021001。但直接Jsoup.connect(url).cookies(cookies).get()会返回登录页 HTML——因为服务器校验Referer头和 Session Cookie 是否匹配。ConnectJWGL.java中正确写法是Connection connection Jsoup.connect(http://jwgl.xxx.edu.cn/xskbcx.aspx?xh2021001) .cookies(cookies) .header(Referer, http://jwgl.xxx.edu.cn/main.jsp) // 必须和上一页一致 .header(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36); Document courseDoc connection.get();漏掉Referer头90% 的教务系统会重定向回登录页。cookies必须是上一步response.cookies()返回的MapString, String不能自己拼字符串。3.3 HTML 结构解析用 select() 定位比用 getElementById() 更可靠教务系统 HTML 通常无规范 ID但表格结构稳定。ConnectJWGL.java解析课表的核心逻辑是Elements tables courseDoc.select(table[width98%]); // 锁定主课表容器 if (!tables.isEmpty()) { Element table tables.get(0); Elements rows table.select(tr:gt(0)); // 跳过表头行 for (Element row : rows) { Elements cells row.select(td); if (cells.size() 7) { // 保证有7列节次、周一至周五 String section cells.get(0).text().trim(); // 第1列节次如1-2节 String mon cells.get(1).text().trim(); // 周一 String tue cells.get(2).text().trim(); // 周二 // ... 同理取周三至周五 // 解析单格内容可能含br、多行文本、空格 String courseInfo parseCellContent(mon); // 自定义清洗方法 } } }parseCellContent()是关键辅助函数处理常见脏数据private String parseCellContent(String raw) { if (raw null || raw.trim().isEmpty()) return ; // 移除所有br标签并替换为换行 raw raw.replaceAll(br[^]*, \n); // 移除多余空格和换行 raw raw.replaceAll(\\s, ).trim(); // 提取课程名、教师、教室正则匹配常见模式 Pattern p Pattern.compile(([\\u4e00-\\u9fa5a-zA-Z0-9\\(\\)\\-])\\s([\\u4e00-\\u9fa5]{2,})\\s([A-Z][0-9]{3})); Matcher m p.matcher(raw); if (m.find()) { return String.format(%s|%s|%s, m.group(1), m.group(2), m.group(3)); } return raw; }这个正则适配了 80% 的教务系统课表单元格格式高等数学(一) 张老师 A203→高等数学(一)|张老师|A203。若你的学校格式不同如用/分隔改正则即可。3.4 数据绑定到鸿蒙 UI用 ListContainer CustomComponent 实现滚动课表解析出的课表数据是ListCourseItem其中CourseItem包含section节次、mon周一课、tue周二课等字段。鸿蒙 Java FA 中不能直接用RecyclerView要用ListContainer// 在 AbilitySlice 的 onActive() 中 ListContainer listContainer (ListContainer) findComponentById(ResourceTable.Id_list_container); CourseItemProvider provider new CourseItemProvider(courseList, this); listContainer.setItemProvider(provider);CourseItemProvider继承RecycleItemProvider核心是getComponent()方法Override public Component getComponent(int position, Component convertComponent, ComponentContainer container) { DirectionalLayout layout; if (convertComponent null) { layout (DirectionalLayout) LayoutScatter.getInstance(context) .parse(ResourceTable.Layout_course_item, null, false); } else { layout (DirectionalLayout) convertComponent; } CourseItem item list.get(position); Text sectionText (Text) layout.findComponentById(ResourceTable.Id_text_section); Text monText (Text) layout.findComponentById(ResourceTable.Id_text_mon); // ... 绑定其他列 sectionText.setText(item.getSection()); monText.setText(item.getMon()); return layout; }course_item.xml布局是一个横向DirectionalLayout内含 7 个Text组件节次 周一至周五。重点ListContainer的setReboundEffect(true)可开启弹性滚动提升体验。4. 避坑指南六个真实翻车现场与血泪修复方案4.1 现象Jsoup.connect().get()报java.net.SocketTimeoutException: timeout原因教务系统服务器设置了极短的连接超时如 3 秒而 JSoup 默认timeout(3000)不够用或模拟器 DNS 解析失败尤其用 HTTP 时。解决将超时设为 15000 毫秒.timeout(15000)强制指定 DNS在connect()前加System.setProperty(sun.net.spi.nameservice.provider.1, dns,sun);若仍失败改用HttpURLConnection手动构建请求绕过 JSoup 网络层。4.2 现象登录成功后访问课表页返回空 HTML 或乱码原因cookies未正确传递或Referer头缺失/错误更隐蔽的是部分教务系统要求Cookie中必须包含ASP.NET_SessionId且值不能过期。解决打印cookies.toString()确认含ASP.NET_SessionId用banner-debug.har项目自带 HAR 包在 Chrome DevTools 的 Network 标签中导入对比真实浏览器请求头逐项补全Cookie、Referer、User-Agent若ASP.NET_SessionId值为空说明登录未真正成功检查response.statusCode()是否为 200 且response.body().length() 10000。4.3 现象课表解析出空数据cells.get(i).text()返回空字符串原因HTML 中课程信息被包裹在span或div内text()只取直接文本节点或内容被 JavaScript 动态注入JSoup 不执行 JS。解决改用ownText()获取元素自身文本不含子元素用html()获取原始 HTML 字符串再正则提取若确为 JS 渲染放弃 JSoup改用鸿蒙WebEngine加载页面并注入 JS 提取数据但失去离线解析能力。4.4 现象HAP 安装后点击按钮无响应Log 显示java.lang.SecurityException: Permission denied原因config.json中reqPermissions缺少ohos.permission.INTERNET或module.json5若误用 Stage 模型中权限声明位置错误。解决确认config.json在module/src/main/目录下且reqPermissions数组在module对象内删除module.json5FA 工程不用它在config.json中添加deviceCapability: [phone, tablet]避免模拟器因设备类型不匹配拒绝权限。4.5 现象JSoup 解析中文乱码如é«˜ç‰æ•°å¦原因教务系统返回的 HTML 声明编码为GBK但 JSoup 默认用UTF-8解析。解决在connect().get()前指定编码Document doc Jsoup.connect(url) .userAgent(ua) .timeout(10000) .charset(GBK) // 关键 .get();若不确定编码先用Connection.Response获取原始字节流byte[] bytes response.bodyAsBytes(); String html new String(bytes, GBK); // 或 GB2312 Document doc Jsoup.parse(html, url);5. 进阶技巧用 banner-debug.har 反向工程教务接口以及 HAP 包体积压缩实战5.1 HAR 包深度利用从banner-debug.har提取真实请求链路项目自带banner-debug.har这是 Chrome 浏览器导出的 HTTP 归档文件记录了真实用户操作教务系统的完整网络请求。它比看ConnectJWGL.java源码更可靠——因为代码可能过时而 HAR 是实时抓包。用 Python 解析 HAR无需安装额外工具Python 3.6 自带jsonimport json with open(banner-debug.har, r, encodingutf-8) as f: har json.load(f) # 提取所有 POST 请求登录、查课表 entries har[log][entries] for entry in entries: if entry[request][method] POST: url entry[request][url] # 提取 POST 数据 post_data if postData in entry[request]: if text in entry[request][postData]: post_data entry[request][postData][text] elif params in entry[request][postData]: post_data .join([f{p[name]}{p[value]} for p in entry[request][postData][params]]) print(fURL: {url}) print(fData: {post_data[:100]}...) print(- * 50)输出示例URL: http://jwgl.xxx.edu.cn/login.aspx Data: __VIEWSTATE/wEPDwULLTE2MzUyNjQ2MjQPZBYCZg9kFgICAw9kFgICAQ8PFgIeBFRleHQFHuiAmOacieaYrWPsOWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacieaYrWPsOacie......关键收获确认__VIEWSTATE字段名有些系统叫__EVENTTARGET发现密码字段实际名为TextBox2而非password获取到真实的Referer值如http://jwgl.xxx.edu.cn/default2.aspx。提示用 VS Code 安装 HAR Viewer 插件可图形化查看 HAR比命令行更直观。5.2 HAP 包瘦身从 12MB 到 3.2MB 的四步压缩法原始构建的 HAP 包build/outputs/default/xxx.hap体积常超 12MB主要因jsoup-1.15.3.jar360KB和gradle-wrapper.jar60KB被误打包。鸿蒙 HAP 不需要 Gradle WrapperJSoup 也只需核心类。步骤一排除无用依赖在 Module 级build.gradle中dependencies块改为dependencies { // 移除 implementation org.jsoup:jsoup:1.15.3 implementation files(libs/jsoup-1.15.3.jar) // 只保留本地 jar }步骤二精简 JSoup JAR可选下载jsoup-1.15.3-sources.jar用jar -tf查看内容发现org/jsoup/nodes/Document$QuirksMode.class等类极少用。用jar -xf解压后删除org/jsoup/parser/HTML 解析器、org/jsoup/safety/XSS 过滤等非必需包再jar -cf重打包为jsoup-core.jar仅剩connect/,nodes/,select/体积降至 180KB。步骤三禁用调试符号在build.gradle的buildTypes中添加release { minifyEnabled true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt) // 添加 JSoup 保留规则 proguardFiles file(proguard-rules.pro) }proguard-rules.pro内容-keep class org.jsoup.** { *; } -keep class org.jsoup.nodes.** { *; } -keep class org.jsoup.select.** { *; }步骤四启用资源压缩在config.json的module下添加deliveryWithInstall: true, installationFree: false, compressed: true执行gradlew buildRelease后HAP 体积从 12.4MB 降至3.2MB安装速度提升 3 倍且无功能损失。6. 教务数据落地的最后一公里把课表存进鸿蒙 Data Ability实现跨应用调用光在本应用里展示课表不够——学生可能想把课表同步到日历、设上课提醒、或分享给同学。鸿蒙的 Data Ability 是标准数据共享接口比 SharedPreferences 更规范、比文件共享更安全。6.1 定义 Data Ability 的 URI 与权限在config.json的module→abilities数组中新增{ name: CourseDataAbility, type: data, uri: dataability:///com.example.jwgl.CourseDataAbility, exported: true, permissions: [ com.example.jwgl.permission.PROVIDER ] }并在reqPermissions中声明{ name: com.example.jwgl.permission.PROVIDER, reason: 允许其他应用读取课表数据 }6.2 实现 CourseDataAbility支持 query() 与 insert()新建CourseDataAbility.java继承DataAbilitypublic class CourseDataAbility extends DataAbility { private static final String TABLE_COURSE course; private DatabaseHelper dbHelper; Override public void onStart(Intent intent) { super.onStart(intent); dbHelper new DatabaseHelper(this); } Override public ResultSet query(Uri uri, String[] columns, DeviceId deviceId) { if (uri.getPath().equals(/course)) { // 返回所有课表 return dbHelper.queryAllCourse(); } return null; } Override public int insert(Uri uri, ValuesBucket value) { if (uri.getPath().equals(/course)) { // 插入单条课表记录 return dbHelper.insertCourse(value); } return -1; } }DatabaseHelper使用鸿蒙RelationalStoreAPI 9 支持public class DatabaseHelper { private RelationalStore store; public DatabaseHelper(Context context) { try { StoreConfig config StoreConfig.newSingleVersionConfig(context.getFilesDir() /course.db); store new RelationalStoreDelegate(config); } catch (Exception e) { e.printStackTrace(); } } public ResultSet queryAllCourse() { String sql SELECT * FROM course ORDER BY section ASC; return store.executeQuery(sql, null); } public int insertCourse(ValuesBucket values) { String sql INSERT INTO course (section, mon, tue, wed, thu, fri) VALUES (?, ?, ?, ?, ?, ?); Object[] args { values.getString(section), values.getString(mon), values.getString(tue), values.getString(wed), values.getString(thu), values.getString(fri) }; return store.executeSql(sql, args); } }6.3 其他应用调用示例用 DataAbilityHelper 查询课表在另一个鸿蒙应用中只需三行代码获取课表DataAbilityHelper helper DataAbilityHelper.creator(this); Uri uri Uri.parse(dataability:///com.example.jwgl.CourseDataAbility/course); ResultSet rs helper.query(uri, null, null); while (rs.goToNextRow()) { String section rs.getString(rs.getColumnIndexForName(section)); String mon rs.getString(rs.getColumnIndexForName(mon)); // ... 绑定到 UI }这实现了真正的“教务数据即服务”JWGL as a Service。从爬虫解析到跨应用共享整个链路闭环。我后来给学院教务处演示时他们当场提出能否把成绩查询也做成 Data Ability答案是肯定的——只要ConnectJWGL.java里加一个queryScore()方法解析cjcx.aspx页面再插入score表即可。从那以后我每次做教务类鸿蒙项目都强制走一遍banner-debug.har分析 Data Ability 封装流程。它不保证 100% 适配所有学校系统但能让你在 2 小时内判断出这个系统能不能爬、要不要换方案、数据怎么安全共享。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →