Files
schedule_converter/parser/empty_classroom.go
lafay ab37aeb2fc
All checks were successful
Build / build (push) Successful in 2m17s
refactor: introduce service layer and migrate gRPC handlers to use it
Extract business logic from gRPC handlers into a dedicated service package.
Add context support throughout for cancellation and timeouts. Move models to
their own package, remove hardcoded credentials from config, and simplify
parsers to only handle HTML parsing.
2026-06-16 18:33:13 +08:00

156 lines
4.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, "&nbsp", "")
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)
}