package parser import ( "fmt" "strings" "schedule_converter/models" "github.com/PuerkitoBio/goquery" ) // ParseEmptyClassroomHTML 解析空教室查询结果 HTML,返回空教室条目列表。 func ParseEmptyClassroomHTML(html, semester, weekStart, weekEnd string) ([]models.EmptyClassroomEntry, error) { if html == "" { return nil, nil } // Clean nbsp entities cleaned := strings.ReplaceAll(html, " ", "") cleaned = strings.ReplaceAll(cleaned, "&", "&") doc, err := goquery.NewDocumentFromReader(strings.NewReader(cleaned)) if err != nil { return nil, err } dayNames := [7]string{"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"} periodLabels := [6]string{"1,2", "3,4", "5,6", "7,8", "9,10", "11,12"} var classrooms []models.EmptyClassroomEntry // Find the main data table with class="dataTable" doc.Find("table.dataTable").Each(func(_ int, table *goquery.Selection) { rows := table.Find("tr") if rows.Length() < 3 { return } // Verify this is the classroom table by checking for weekday headers hasWeekdayHeader := false rows.First().Find("th").Each(func(_ int, th *goquery.Selection) { text := strings.TrimSpace(th.Text()) for _, day := range dayNames { if strings.Contains(text, day) { hasWeekdayHeader = true break } } }) if !hasWeekdayHeader { return } // Build column mapping: column index -> (dayIndex, periodIndex) colMapping := make(map[int][2]int) // Start from column 1 (skip first column which is classroom name) colIdx := 1 for dayIdx := 0; dayIdx < 7; dayIdx++ { for periodIdx := 0; periodIdx < 6; periodIdx++ { colMapping[colIdx] = [2]int{dayIdx, periodIdx} colIdx++ } } // Process data rows (skip first 2 header rows) rows.Each(func(rowIdx int, row *goquery.Selection) { if rowIdx < 2 { return } cells := row.Find("td") if cells.Length() < 2 { return } // Get classroom name from first cell classroomName := strings.TrimSpace(cells.First().Text()) // Filter out invalid classroom names if classroomName == "" || len(classroomName) < 2 { return } // Skip non-classroom entries like "线上网络授课" if strings.Contains(classroomName, "线上") || strings.Contains(classroomName, "网络") { return } // Collect empty slots for this classroom var slots []string weekdaySet := make(map[int]bool) var weekdays []string // Check each time slot column for colIdx := 1; colIdx < cells.Length(); colIdx++ { cell := cells.Eq(colIdx) // Get mapping for this column mapping, exists := colMapping[colIdx] if !exists { continue } dayIdx := mapping[0] periodIdx := mapping[1] // Check if cell is empty (no kjs_icon div) isEmpty := true cell.Find("div.kjs_icon").Each(func(_ int, div *goquery.Selection) { isEmpty = false }) // Also check if cell has any content cellText := strings.TrimSpace(cell.Text()) if cellText != "" { isEmpty = false } if isEmpty { dayName := dayNames[dayIdx] periodText := periodLabels[periodIdx] slot := fmt.Sprintf("%s 第%s节", dayName, periodText) slots = append(slots, slot) if !weekdaySet[dayIdx] { weekdaySet[dayIdx] = true weekdays = append(weekdays, dayName) } } } // Create entry if there are empty slots if len(slots) > 0 { weekStr := "" if weekStart != "" && weekEnd != "" { weekStr = fmt.Sprintf("%s-%s周", weekStart, weekEnd) } entry := models.EmptyClassroomEntry{ Classroom: classroomName, Weekday: strings.Join(weekdays, ", "), Periods: strings.Join(slots, "\n"), Term: semester, Weeks: weekStr, } classrooms = append(classrooms, entry) } }) }) return classrooms, nil } // TestParseEmptyClassroomHTML is a test helper function that exports the parser for testing func TestParseEmptyClassroomHTML(html, semester, weekStart, weekEnd string) ([]models.EmptyClassroomEntry, error) { return ParseEmptyClassroomHTML(html, semester, weekStart, weekEnd) }