Files

438 lines
10 KiB
Go
Raw Permalink Normal View History

2026-03-13 20:43:44 +08:00
package parser
import (
"regexp"
"sort"
"strconv"
"strings"
"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 == "&nbsp" {
continue
}
// 跳过&nbsp
if strings.TrimSpace(cellHTML) == "&nbsp" {
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-60=周一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
}