All checks were successful
Build / build (push) Successful in 2m17s
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.
156 lines
4.1 KiB
Go
156 lines
4.1 KiB
Go
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)
|
||
}
|