2026-03-13 20:43:44 +08:00
|
|
|
|
package parser
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"regexp"
|
|
|
|
|
|
"sort"
|
|
|
|
|
|
"strconv"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
2026-03-16 13:40:53 +08:00
|
|
|
|
"schedule_converter/models"
|
2026-03-13 20:43:44 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var weekTokenRegexp = regexp.MustCompile(`\[?\d+(?:[-,,]\d+)*\]?周`)
|
|
|
|
|
|
|
|
|
|
|
|
type weekRoomEntry struct {
|
|
|
|
|
|
weeksStr string
|
|
|
|
|
|
room string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析课表HTML
|
|
|
|
|
|
func ParseScheduleHTML(html string) ([]models.Course, error) {
|
|
|
|
|
|
// 创建一个带Reader的Document
|
|
|
|
|
|
reader := strings.NewReader(html)
|
|
|
|
|
|
doc, err := goquery.NewDocumentFromReader(reader)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
courseMap := make(map[string]*models.Course)
|
|
|
|
|
|
|
|
|
|
|
|
// 找到课表table
|
|
|
|
|
|
doc.Find("table").Each(func(tableIdx int, table *goquery.Selection) {
|
|
|
|
|
|
class, _ := table.Attr("class")
|
|
|
|
|
|
if !strings.Contains(class, "addlist") {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
table.Find("tr").Each(func(trIdx int, tr *goquery.Selection) {
|
|
|
|
|
|
tds := tr.Find("td")
|
|
|
|
|
|
if tds.Length() < 3 {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第一个td是上午/下午/晚上,第二个是节次
|
|
|
|
|
|
section := tds.Eq(1).Text()
|
|
|
|
|
|
if section == "" {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从第3个td开始是星期一到星期日 (索引2-8)
|
|
|
|
|
|
for dayIndex := 2; dayIndex <= 8; dayIndex++ {
|
|
|
|
|
|
if dayIndex >= tds.Length() {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
dayTd := tds.Eq(dayIndex)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取HTML内容
|
|
|
|
|
|
cellHTML, err := dayTd.Html()
|
|
|
|
|
|
if err != nil || cellHTML == "" || cellHTML == " " {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 跳过 
|
|
|
|
|
|
if strings.TrimSpace(cellHTML) == " " {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
day := getDayName(dayIndex - 1)
|
|
|
|
|
|
|
|
|
|
|
|
// 解析课程信息
|
|
|
|
|
|
parseCourseCellHTML(cellHTML, day, section, courseMap)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 将map转为slice
|
|
|
|
|
|
var courses []models.Course
|
|
|
|
|
|
for _, course := range courseMap {
|
|
|
|
|
|
courses = append(courses, *course)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return courses, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析课程单元格HTML
|
|
|
|
|
|
func parseCourseCellHTML(cellHTML, day, section string, courseMap map[string]*models.Course) {
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有多个课程(用<br/>分割)
|
|
|
|
|
|
if strings.Contains(cellHTML, "<br>") || strings.Contains(cellHTML, "<br/>") || strings.Contains(cellHTML, "<br />") {
|
|
|
|
|
|
// 分割成多个课程
|
|
|
|
|
|
// 先统一替换
|
|
|
|
|
|
cellHTML = strings.ReplaceAll(cellHTML, "<br>", "|SPLIT|")
|
|
|
|
|
|
cellHTML = strings.ReplaceAll(cellHTML, "<br/>", "|SPLIT|")
|
|
|
|
|
|
cellHTML = strings.ReplaceAll(cellHTML, "<br />", "|SPLIT|")
|
|
|
|
|
|
|
|
|
|
|
|
courses := strings.Split(cellHTML, "|SPLIT|")
|
|
|
|
|
|
|
|
|
|
|
|
var courseName string
|
|
|
|
|
|
for i, part := range courses {
|
|
|
|
|
|
part = strings.TrimSpace(part)
|
|
|
|
|
|
if part == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
beforeWeek, weekEntries := extractWeekEntries(part)
|
|
|
|
|
|
if len(weekEntries) == 0 {
|
|
|
|
|
|
// 没有周数信息,这是课程名(教室会在周数后面或下一个部分)
|
|
|
|
|
|
courseName = part
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 有周数信息,说明这行包含老师+周数+地点
|
|
|
|
|
|
// 需要结合之前的课程名
|
|
|
|
|
|
if courseName == "" {
|
|
|
|
|
|
courseName = part
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 先取片段中的第一个有效教室作为回退值
|
|
|
|
|
|
fallbackRoom := ""
|
|
|
|
|
|
for _, entry := range weekEntries {
|
|
|
|
|
|
if entry.room != "" {
|
|
|
|
|
|
fallbackRoom = entry.room
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果片段里没有教室,查看下一个部分是否是教室
|
|
|
|
|
|
if fallbackRoom == "" {
|
|
|
|
|
|
// 找下一个部分
|
|
|
|
|
|
for j := i + 1; j < len(courses); j++ {
|
|
|
|
|
|
nextPart := strings.TrimSpace(courses[j])
|
|
|
|
|
|
if nextPart == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
// 检查下一个部分是否是教室(包含楼/室/厅/院)
|
|
|
|
|
|
if strings.Contains(nextPart, "楼") || strings.Contains(nextPart, "室") || strings.Contains(nextPart, "厅") || strings.Contains(nextPart, "院") {
|
|
|
|
|
|
fallbackRoom = nextPart
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果下一个部分不包含周数信息,说明是课程名,停止查找
|
|
|
|
|
|
if !regexp.MustCompile(`\d+.*周`).MatchString(nextPart) {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析老师(在beforeWeek中)
|
|
|
|
|
|
teacher := ""
|
|
|
|
|
|
// 去掉课程名部分
|
|
|
|
|
|
if strings.HasPrefix(beforeWeek, courseName) {
|
|
|
|
|
|
teacher = strings.TrimSpace(beforeWeek[len(courseName):])
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 老师可能直接在整个字符串中
|
|
|
|
|
|
teacherPattern := regexp.MustCompile(`([一-龥]{2,4})$`)
|
|
|
|
|
|
teacherMatch := teacherPattern.FindStringSubmatch(beforeWeek)
|
|
|
|
|
|
if teacherMatch != nil {
|
|
|
|
|
|
teacher = teacherMatch[1]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析节次
|
|
|
|
|
|
sectionNums := parseSection(section)
|
|
|
|
|
|
|
|
|
|
|
|
// 每个“周次-地点”片段分别入库,避免不同周次地点混淆
|
|
|
|
|
|
for _, entry := range weekEntries {
|
|
|
|
|
|
room := strings.TrimSpace(entry.room)
|
|
|
|
|
|
if room == "" {
|
|
|
|
|
|
room = fallbackRoom
|
|
|
|
|
|
}
|
|
|
|
|
|
weeks := parseWeeks(entry.weeksStr)
|
|
|
|
|
|
if len(weeks) == 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用课程名+老师+教室作为 key,避免不同地点被错误合并
|
|
|
|
|
|
key := buildCourseKey(courseName, teacher, room)
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已存在
|
|
|
|
|
|
if existing, exists := courseMap[key]; exists {
|
|
|
|
|
|
// 添加新的上课时间
|
|
|
|
|
|
existing.Schedule = append(existing.Schedule, models.Schedule{
|
|
|
|
|
|
Day: day,
|
|
|
|
|
|
Section: sectionNums,
|
|
|
|
|
|
Weeks: weeks,
|
|
|
|
|
|
})
|
|
|
|
|
|
if existing.Room == "" && room != "" {
|
|
|
|
|
|
existing.Room = room
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 创建新课程
|
|
|
|
|
|
courseMap[key] = &models.Course{
|
|
|
|
|
|
Name: courseName,
|
|
|
|
|
|
Teacher: teacher,
|
|
|
|
|
|
Room: room,
|
|
|
|
|
|
Schedule: []models.Schedule{
|
|
|
|
|
|
{
|
|
|
|
|
|
Day: day,
|
|
|
|
|
|
Section: sectionNums,
|
|
|
|
|
|
Weeks: weeks,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 这个课程处理完后,重置courseName
|
|
|
|
|
|
courseName = ""
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 只有一个课程,直接解析
|
|
|
|
|
|
line := strings.TrimSpace(cellHTML)
|
|
|
|
|
|
|
|
|
|
|
|
beforeWeek, weekEntries := extractWeekEntries(line)
|
|
|
|
|
|
if len(weekEntries) == 0 {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fallbackRoom := ""
|
|
|
|
|
|
for _, entry := range weekEntries {
|
|
|
|
|
|
if entry.room != "" {
|
|
|
|
|
|
fallbackRoom = entry.room
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从beforeWeek中提取老师和课程名
|
|
|
|
|
|
teacherPattern := regexp.MustCompile(`([一-龥]{2,4})$`)
|
|
|
|
|
|
teacherMatch := teacherPattern.FindStringSubmatch(beforeWeek)
|
|
|
|
|
|
|
|
|
|
|
|
var courseName, teacher string
|
|
|
|
|
|
if teacherMatch != nil {
|
|
|
|
|
|
teacher = teacherMatch[1]
|
|
|
|
|
|
courseName = strings.TrimSpace(beforeWeek[:len(beforeWeek)-len(teacher)])
|
|
|
|
|
|
} else {
|
|
|
|
|
|
courseName = beforeWeek
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if courseName == "" {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析节次
|
|
|
|
|
|
sectionNums := parseSection(section)
|
|
|
|
|
|
|
|
|
|
|
|
for _, entry := range weekEntries {
|
|
|
|
|
|
room := strings.TrimSpace(entry.room)
|
|
|
|
|
|
if room == "" {
|
|
|
|
|
|
room = fallbackRoom
|
|
|
|
|
|
}
|
|
|
|
|
|
weeks := parseWeeks(entry.weeksStr)
|
|
|
|
|
|
if len(weeks) == 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用课程名+老师+教室作为 key,避免不同地点被错误合并
|
|
|
|
|
|
key := buildCourseKey(courseName, teacher, room)
|
|
|
|
|
|
|
|
|
|
|
|
if existing, exists := courseMap[key]; exists {
|
|
|
|
|
|
existing.Schedule = append(existing.Schedule, models.Schedule{
|
|
|
|
|
|
Day: day,
|
|
|
|
|
|
Section: sectionNums,
|
|
|
|
|
|
Weeks: weeks,
|
|
|
|
|
|
})
|
|
|
|
|
|
if existing.Room == "" && room != "" {
|
|
|
|
|
|
existing.Room = room
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
courseMap[key] = &models.Course{
|
|
|
|
|
|
Name: courseName,
|
|
|
|
|
|
Teacher: teacher,
|
|
|
|
|
|
Room: room,
|
|
|
|
|
|
Schedule: []models.Schedule{
|
|
|
|
|
|
{
|
|
|
|
|
|
Day: day,
|
|
|
|
|
|
Section: sectionNums,
|
|
|
|
|
|
Weeks: weeks,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildCourseKey(name, teacher, room string) string {
|
|
|
|
|
|
return strings.TrimSpace(name) + "|" + strings.TrimSpace(teacher) + "|" + strings.TrimSpace(room)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// extractWeekEntries 将一段文本拆成多个“周次-地点”片段
|
|
|
|
|
|
// 例如:李鹏程[2-8]周N楼-335,[9]周N楼-331
|
|
|
|
|
|
// 会拆成:
|
|
|
|
|
|
// 1) weeks=[2-8]周 room=N楼-335
|
|
|
|
|
|
// 2) weeks=[9]周 room=N楼-331
|
|
|
|
|
|
func extractWeekEntries(text string) (beforeWeek string, entries []weekRoomEntry) {
|
|
|
|
|
|
indexes := weekTokenRegexp.FindAllStringIndex(text, -1)
|
|
|
|
|
|
if len(indexes) == 0 {
|
|
|
|
|
|
return "", nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
firstStart := indexes[0][0]
|
|
|
|
|
|
beforeWeek = strings.TrimSpace(text[:firstStart])
|
|
|
|
|
|
|
|
|
|
|
|
for i, idx := range indexes {
|
|
|
|
|
|
start := idx[0]
|
|
|
|
|
|
end := idx[1]
|
|
|
|
|
|
nextStart := len(text)
|
|
|
|
|
|
if i+1 < len(indexes) {
|
|
|
|
|
|
nextStart = indexes[i+1][0]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
weeksStr := strings.TrimSpace(text[start:end])
|
|
|
|
|
|
room := strings.TrimSpace(text[end:nextStart])
|
|
|
|
|
|
room = weekTokenRegexp.ReplaceAllString(room, "")
|
|
|
|
|
|
room = strings.Trim(room, ",, ")
|
|
|
|
|
|
room = strings.TrimSpace(room)
|
|
|
|
|
|
|
|
|
|
|
|
entries = append(entries, weekRoomEntry{
|
|
|
|
|
|
weeksStr: weeksStr,
|
|
|
|
|
|
room: room,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return beforeWeek, entries
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func getDayName(index int) string {
|
|
|
|
|
|
// 返回0-6,0=周一,6=周日
|
|
|
|
|
|
days := []string{"0", "1", "2", "3", "4", "5", "6"}
|
|
|
|
|
|
if index >= 1 && index <= 7 {
|
|
|
|
|
|
return days[index-1]
|
|
|
|
|
|
}
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func parseSection(section string) []int {
|
|
|
|
|
|
// 格式: 1-2 或 3-4
|
|
|
|
|
|
re := regexp.MustCompile(`(\d+)-(\d+)`)
|
|
|
|
|
|
match := re.FindStringSubmatch(section)
|
|
|
|
|
|
if match != nil {
|
|
|
|
|
|
start, _ := strconv.Atoi(match[1])
|
|
|
|
|
|
end, _ := strconv.Atoi(match[2])
|
|
|
|
|
|
var nums []int
|
|
|
|
|
|
for i := start; i <= end; i++ {
|
|
|
|
|
|
nums = append(nums, i)
|
|
|
|
|
|
}
|
|
|
|
|
|
return nums
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 单独数字
|
|
|
|
|
|
num, err := strconv.Atoi(section)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
return []int{num}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return []int{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func parseWeeks(weeksStr string) []int {
|
|
|
|
|
|
if weeksStr == "" {
|
|
|
|
|
|
return []int{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 统一格式:去掉括号/周/空白,统一中文分隔符
|
|
|
|
|
|
raw := strings.TrimSpace(weeksStr)
|
|
|
|
|
|
oddOnly := strings.Contains(raw, "单")
|
|
|
|
|
|
evenOnly := strings.Contains(raw, "双")
|
|
|
|
|
|
|
|
|
|
|
|
replacer := strings.NewReplacer(
|
|
|
|
|
|
"[", "", "]", "",
|
|
|
|
|
|
"(", "", ")", "",
|
|
|
|
|
|
"(", "", ")", "",
|
|
|
|
|
|
"周", "",
|
|
|
|
|
|
",", ",",
|
|
|
|
|
|
"、", ",",
|
|
|
|
|
|
" ", "",
|
|
|
|
|
|
)
|
|
|
|
|
|
normalized := replacer.Replace(raw)
|
|
|
|
|
|
if normalized == "" {
|
|
|
|
|
|
return []int{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
weekSet := make(map[int]struct{})
|
|
|
|
|
|
for _, part := range strings.Split(normalized, ",") {
|
|
|
|
|
|
part = strings.TrimSpace(part)
|
|
|
|
|
|
if part == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 区间格式:1-8
|
|
|
|
|
|
if strings.Contains(part, "-") {
|
|
|
|
|
|
rangeParts := strings.SplitN(part, "-", 2)
|
|
|
|
|
|
if len(rangeParts) != 2 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
start, err1 := strconv.Atoi(strings.TrimSpace(rangeParts[0]))
|
|
|
|
|
|
end, err2 := strconv.Atoi(strings.TrimSpace(rangeParts[1]))
|
|
|
|
|
|
if err1 != nil || err2 != nil || start > end {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
for i := start; i <= end; i++ {
|
|
|
|
|
|
if oddOnly && i%2 == 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if evenOnly && i%2 != 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
weekSet[i] = struct{}{}
|
|
|
|
|
|
}
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 单周次:例如 9
|
|
|
|
|
|
num, err := strconv.Atoi(part)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if oddOnly && num%2 == 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if evenOnly && num%2 != 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
weekSet[num] = struct{}{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(weekSet) == 0 {
|
|
|
|
|
|
return []int{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
weeks := make([]int, 0, len(weekSet))
|
|
|
|
|
|
for w := range weekSet {
|
|
|
|
|
|
weeks = append(weeks, w)
|
|
|
|
|
|
}
|
|
|
|
|
|
sort.Ints(weeks)
|
|
|
|
|
|
return weeks
|
|
|
|
|
|
}
|