package parser import ( "encoding/json" "fmt" "io" "net/http" "os" "regexp" "sort" "strconv" "strings" "schedule_converter/client" "schedule_converter/config" "schedule_converter/models" "github.com/PuerkitoBio/goquery" ) var weekTokenRegexp = regexp.MustCompile(`\[?\d+(?:[-,,]\d+)*\]?周`) type weekRoomEntry struct { weeksStr string room string } // 获取课表 func FetchSchedule(httpClient *http.Client) ([]models.Course, error) { return FetchScheduleWithClient(httpClient, config.Semester) } // FetchScheduleWithClient 使用指定的 HTTP 客户端和学期获取课表 func FetchScheduleWithClient(httpClient *http.Client, semester string) ([]models.Course, error) { fmt.Println("[5] 获取课表数据...") // POST请求获取课表 data := fmt.Sprintf("xnxq=%s", semester) req, err := http.NewRequest("POST", config.BaseURL+"/kbcx/queryGrkb", strings.NewReader(data)) if err != nil { return nil, err } req.Header = client.GetHeadersWithContentType("application/x-www-form-urlencoded") resp, err := httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } // 解析课表 courses, err := ParseScheduleHTML(string(body)) if err != nil { return nil, err } fmt.Printf(" 共提取 %d 门课程\n", len(courses)) // 输出到JSON jsonData, err := os.Create("schedule_result.json") if err != nil { return nil, err } defer jsonData.Close() encoder := json.NewEncoder(jsonData) encoder.SetIndent("", " ") err = encoder.Encode(courses) if err != nil { return nil, err } fmt.Println(" JSON结果已保存到: schedule_result.json") // 打印课程信息 for i, course := range courses { fmt.Printf(" 课程 %d: %s - %s\n", i+1, course.Name, course.Teacher) } return courses, nil } // 解析课表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) { // 检查是否有多个课程(用
分割) if strings.Contains(cellHTML, "
") || strings.Contains(cellHTML, "
") || strings.Contains(cellHTML, "
") { // 分割成多个课程 // 先统一替换 cellHTML = strings.ReplaceAll(cellHTML, "
", "|SPLIT|") cellHTML = strings.ReplaceAll(cellHTML, "
", "|SPLIT|") cellHTML = strings.ReplaceAll(cellHTML, "
", "|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) } // extractWeekInfo 提取一段文本中的完整周次片段 // 例如:于战华[15]周,[2-14]周 -> before=于战华, weeks=[15]周,[2-14]周, after= func extractWeekInfo(text string) (beforeWeek, weeksPart, afterWeek string, ok bool) { indexes := weekTokenRegexp.FindAllStringIndex(text, -1) if len(indexes) == 0 { return "", "", "", false } firstStart := indexes[0][0] beforeWeek = strings.TrimSpace(text[:firstStart]) parts := weekTokenRegexp.FindAllString(text, -1) weeksPart = strings.Join(parts, ",") // 周次后(含中间)非周次内容作为教室候选 tail := strings.TrimSpace(text[firstStart:]) afterWeek = strings.TrimSpace(weekTokenRegexp.ReplaceAllString(tail, "")) afterWeek = strings.Trim(afterWeek, ",, ") return beforeWeek, weeksPart, afterWeek, true } // 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 }