Skip to content
This repository was archived by the owner on Sep 12, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 53 additions & 5 deletions src/main/java/bean/BUAACourseInfo.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,59 @@
package main.java.bean

/**
* GSON 数据类,用于映射北航教务系统课表 API 的 JSON 响应。
* 顶层结构。
*
* @property code 状态码,通常成功时为 0。
* @property msg 响应信息。
* @property datas 核心数据负载。
*/
data class BUAACourseInfo(
val code: Int,
val msg: String,
val datas: Datas
) {
/**
* 包含所有课表列表的核心数据模型。
*
* @property arrangedList 已安排(有具体时间地点)的课程列表。
* @property notArrangeList 未安排的课程列表。
* @property practiceList 实践类课程列表。
* @property code 未知用途的编码。
* @property name 未知用途的名称。
*/
data class Datas(
val arrangedList: List<CourseItem>,
val notArrangeList: List<CourseItem>,
val practiceList: List<CourseItem>,
val code: String,
val name: String
) {
/**
* 代表一个具体的课程项目信息。
*
* @property week 星期(例如:"星期一"),但通常使用 dayOfWeek。
* @property courseCode 课程代码。
* @property credit 学分。
* @property courseName 课程名称。
* @property byCode 未知用途编码。
* @property beginSection 开始节次。
* @property endSection 结束节次。
* @property titleDetail 标题详情列表,包含课程的各种详细信息。
* @property multiCourse 是否为多课程。
* @property teachClassName 教学班名称。
* @property placeName 上课地点。
* @property teachingTarget 教学对象。
* @property weeksAndTeachers 包含周次和教师的聚合字符串。
* @property teachClassId 教学班ID。
* @property cellDetail UI单元格的详细信息,通常包含教师和周次。
* @property tags 课程标签。
* @property courseSerialNo 课程序号。
* @property startTime 课程开始时间 (格式 "HH:mm")。
* @property endTime 课程结束时间 (格式 "HH:mm")。
* @property color 用于UI显示的颜色代码。
* @property dayOfWeek 星期几,一个整数 (例如,1代表周一)。
*/
data class CourseItem(
val week: String = "",
val courseCode: String = "",
Expand All @@ -20,25 +62,31 @@ data class BUAACourseInfo(
val byCode: String = "",
val beginSection: Int = 0,
val endSection: Int = 0,
val titleDetail: List<String> = listOf(),
val titleDetail: List<String> = emptyList(),
val multiCourse: String = "",
val teachClassName: String = "",
val placeName: String = "",
val teachingTarget: String = "",
val weeksAndTeachers: String = "",
val teachClassId: String = "",
val cellDetail: List<CellDetail> = listOf(),
val tags: List<String> = listOf(),
val cellDetail: List<CellDetail> = emptyList(),
val tags: List<String> = emptyList(),
val courseSerialNo: String = "",
val beginTime: String = "",
val startTime: String = "",
val endTime: String = "",
val color: String = "",
val dayOfWeek: Int = 0
) {
/**
* 课表UI单元格的显示细节。
*
* @property color 文本颜色。
* @property text 显示的文本内容。
*/
data class CellDetail(
val color: String = "",
val text: String = ""
)
}
}
}
}
223 changes: 153 additions & 70 deletions src/main/java/parser/BUAAParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,65 @@ import main.java.bean.TimeTable
import parser.Parser

/**
* Date: 2024/03/02
* 课表地址: https://byxt.buaa.edu.cn/ -> 查询 -> 课表查询 -> 我的课表
* 项目地址: https://github.com/PandZz/CourseAdapter
* 作者: PandZz
* 北京航空航天大学本研教务系统课表解析器。
*
* 北京航空航天大学-新本研教务
* 解析了POST(https://byxt.buaa.edu.cn/jwapp/sys/homeapp/api/home/student/getMyScheduleDetail.do)的返回结果(json)
* 该解析器用于处理从北航新版教务系统 API 获取的课表 JSON 数据。
* 使用方法:访问 https://byxt.buaa.edu.cn/ ,登录后依次点击
* “查询” -> “课表查询” -> “我的课表” -> “学期课表”
* 然后点击界面右下角下载按钮导入课表。
* 注意:一定要记得切换到学期课表,否则可能漏课。
* API 端点: `https://byxt.buaa.edu.cn/jwapp/sys/homeapp/api/home/student/getMyScheduleDetail.do` (POST)
*
* ---
* ### 变更历史
* - **v2.1.0 (2025-09-07 by Gemini):**
* - **代码风格优化**:
* - 将魔法字符串(如 "上课教师:")提取为伴生对象的常量,增强可维护性。
* - 将 `getNodes()` 方法转换为 `override val` 属性,更符合 Kotlin 惯例。
* - 优化了教师信息和周次信息的解析逻辑,使用解构声明和函数式链式调用,使代码更简洁、意图更清晰。
* - **健壮性增强**:
* - 在解析教师信息时,使用 `split(limit = 3)` 并进行解构,能更优雅地处理格式不完整的数据,避免数组越界。
* - 简化了正则表达式,使其更高效且易于理解。
* - **v2.0.0 (2025-09-07 by oNya):**
* - 重构解析核心,将数据源从 `cellDetail` 迁移至 `titleDetail` 中的 "上课教师" 字段,以适应新版数据格式。
* - 实现了对复杂、非连续周次字符串(如 "[1-3周,5周(单)]")的解析。
* - 增强了对多教师共同授课情况的处理能力。
*
* @param source 从 API 获取的原始 JSON 字符串。
* @author PandZz (初版)
* @author oNya (v2.0.0)
* @author Gemini (v2.1.0 Refactoring)
* @date 2024/03/02
* @version 2.1.0
*/

class BUAAParser(source: String) : Parser(source) {
private val teacherAndWeekRegex = Regex("""^(.+)\[(\d+)-(\d+)周(?:\(([单双])\))?]$""")
override fun getNodes(): Int {
return 14

/**
* 定义了课程表中一天最大的课程节数。
* 北航课表通常为14节。
* @return 课程节数。
*/
override fun getNodes(): Int = 14

/**
* 从源 JSON 数据中解析并生成课程列表。
* @return [Course] 对象的列表,每个对象代表一节具体的课程安排。
*/
override fun generateCourseList(): List<Course> {
return Gson().fromJson(source, BUAACourseInfo::class.java)
?.datas?.arrangedList
?.flatMap(::parseCourseItem)
?: emptyList()
}

/**
* 生成北京航空航天大学的时间表。
* @return [TimeTable] 对象,包含所有课程节次的起止时间。
*/
override fun generateTimeTable(): TimeTable {
return TimeTable(
name = "北京航空航天大学", timeList = listOf(
name = "北京航空航天大学",
timeList = listOf(
TimeDetail(1, "08:00", "08:45"),
TimeDetail(2, "08:50", "09:35"),
TimeDetail(3, "09:50", "10:35"),
Expand All @@ -46,73 +87,115 @@ class BUAAParser(source: String) : Parser(source) {
)
}

override fun generateCourseList(): List<Course> {
val result = arrayListOf<Course>()
val response = Gson().fromJson(source, BUAACourseInfo::class.java)
response.datas.arrangedList.forEach { courseItem ->
parseCourseItem(courseItem).forEach {
result.add(it)
}
}
return result
}
/**
* 内部数据类,用于临时存储从字符串中解析出的周次信息。
*/
private data class WeekInfo(val startWeek: Int, val endWeek: Int, val type: Int)

data class TeacherAndWeek(
val teacher: String,
val beginWeek: Int,
val endWeek: Int,
val type: Int // 0: 每周, 1: 单周, 2: 双周
)

// 解析教师和周数, 例如: "张三[1-16周(单)]" -> TeacherAndWeek("张三", 1, 16, 1)
private fun parseTeacherAndWeek(teachersAndWeeks: String): TeacherAndWeek {
val matchResult = teacherAndWeekRegex.find(teachersAndWeeks)
if (matchResult != null) {
val (teacher, beginWeekStr, endWeekStr, typeStr) = matchResult.destructured
val beginWeek = beginWeekStr.toInt()
val endWeek = endWeekStr.toInt()
val type = when (typeStr) {
"单" -> 1
"双" -> 2
else -> 0
}
/**
* 解析单个课程项目,将其转换为一个或多个 [Course] 对象。
*
* @param courseItem 从 JSON 解析出的原始课程项。
* @return 解析后的 [Course] 对象列表。
*/
private fun parseCourseItem(courseItem: BUAACourseInfo.Datas.CourseItem): List<Course> {
val teacherInfoSource = courseItem.titleDetail
.firstNotNullOfOrNull {
it.takeIf { it.startsWith(TEACHER_INFO_PREFIX) }
?.substringAfter(TEACHER_INFO_PREFIX)
} ?: return emptyList()

val teacherAndWeek = TeacherAndWeek(teacher, beginWeek, endWeek, type)
// println(teacherAndWeek)
return teacherAndWeek
}
return TeacherAndWeek("", 0, 0, 0)
return teacherInfoSource.split(TEACHER_BLOCK_DELIMITER)
.flatMap { teacherBlock -> createCoursesFromTeacherBlock(teacherBlock, courseItem) }
}

private fun parseCourseItem(courseItem: BUAACourseInfo.Datas.CourseItem): List<Course> {
val result = arrayListOf<Course>()
val cellDetail = courseItem.cellDetail
val name = courseItem.courseName
val day = courseItem.dayOfWeek
val room = courseItem.placeName
cellDetail[1].text.split(" ").forEach { teacherAndWeeks ->
val teacherAndWeek = parseTeacherAndWeek(teacherAndWeeks)
val teacher = teacherAndWeek.teacher
val beginWeek = teacherAndWeek.beginWeek
val endWeek = teacherAndWeek.endWeek
val type = teacherAndWeek.type
val course = Course(
name = name,
day = day,
room = room,
teacher = teacher,
/**
* 从单个教师信息块创建课程列表。
*
* @param teacherBlock 教师信息块, 例如 "张三/[6-17周]/6-7节"。
* @param courseItem 原始课程项。
* @return 解析后的 [Course] 对象列表。
*/
private fun createCoursesFromTeacherBlock(
teacherBlock: String,
courseItem: BUAACourseInfo.Datas.CourseItem
): List<Course> {
val parts = teacherBlock.split(TEACHER_INFO_DELIMITER)
if (parts.size < 2) return emptyList()

val teacherName = parts[0]
val weeksString = parts[1]

return parseWeeksString(weeksString).map { weekInfo ->
Course(
name = courseItem.courseName,
day = courseItem.dayOfWeek,
room = courseItem.placeName,
teacher = teacherName,
startNode = courseItem.beginSection,
endNode = courseItem.endSection,
startWeek = beginWeek,
endWeek = endWeek,
type = type,
credit = courseItem.credit.toFloat(),
note = courseItem.titleDetail[8],
startTime = courseItem.beginTime,
startWeek = weekInfo.startWeek,
endWeek = weekInfo.endWeek,
type = weekInfo.type,
credit = courseItem.credit.toFloatOrNull() ?: 0f,
note = courseItem.titleDetail.getOrElse(8) { "" },
startTime = courseItem.startTime,
endTime = courseItem.endTime
)
result.add(course)
}
return result
}

/**
* 解析包含复杂周次信息的字符串。
*
* @param weeksString 格式如 "[1-3周(单),7-13周(单)]" 或 "[1-3周,5周]" 的字符串。
* @return 一个包含所有解析出的周次规则的 [WeekInfo] 列表。
*/
private fun parseWeeksString(weeksString: String): List<WeekInfo> {
return weeksString.removeSurrounding("[", "]")
.split(WEEKS_DELIMITER)
.mapNotNull { parseWeekPattern(it.trim()) }
}

/**
* 解析单个周次模式字符串。
*
* @param pattern 格式如 "1-3周(单)", "7-13周", 或 "5周" 的字符串。
* @return 解析成功则返回 [WeekInfo] 对象,否则返回 `null`。
*/
private fun parseWeekPattern(pattern: String): WeekInfo? {
return weekPatternRegex.matchEntire(pattern)?.let { matchResult ->
val (startWeekStr, endWeekStr, typeStr) = matchResult.destructured

val startWeek = startWeekStr.toInt()
val endWeek = endWeekStr.ifEmpty { startWeekStr }.toInt()
val type = when (typeStr) {
"单" -> TYPE_ODD
"双" -> TYPE_EVEN
else -> TYPE_ALL
}
WeekInfo(startWeek, endWeek, type)
}
}

companion object {
private const val TYPE_ALL = 0 // 每周
private const val TYPE_ODD = 1 // 单周
private const val TYPE_EVEN = 2 // 双周

private const val TEACHER_INFO_PREFIX = "上课教师:"
private const val TEACHER_BLOCK_DELIMITER = " "
private const val TEACHER_INFO_DELIMITER = "/"
private const val WEEKS_DELIMITER = ","

/**
* 用于解析单个周次模式的正则表达式。
* - Group 1: (\d+) -> 开始周 (例如 "1" 或 "5")
* - Group 2: (?:-(\d+))? -> 结束周 (可选, 例如 "3")
* - Group 3: (?:\(([单双])\))? -> 周类型 (可选, 例如 "单")
*
* 示例匹配: "1-3周(单)", "7-13周", "5周"
*/
private val weekPatternRegex = Regex("""(\d+)(?:-(\d+))?周(?:\(([单双])\))?""")
}
}
2 changes: 1 addition & 1 deletion src/main/java/test/BUAATest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import main.java.parser.BUAAParser
import java.io.File

fun main() {
val source = File("C:/Users/R7000P/OneDrive/桌面/getMyScheduleDetail.do.json")
val source = File("getMyScheduleDetail.do.json")
.readText()
BUAAParser(source).apply {
generateCourseList()
Expand Down