Initial commit
This commit is contained in:
531
parser/schedule.go
Normal file
531
parser/schedule.go
Normal file
@@ -0,0 +1,531 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"jwts/client"
|
||||
"jwts/config"
|
||||
"jwts/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
|
||||
}
|
||||
|
||||
if err := os.WriteFile("grkb.html", body, 0644); err != nil {
|
||||
fmt.Printf(" 警告: 无法保存课表页面: %v\n", err)
|
||||
} else {
|
||||
fmt.Println(" 课表页面已保存到: grkb.html")
|
||||
}
|
||||
|
||||
// 解析课表
|
||||
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) {
|
||||
|
||||
// 检查是否有多个课程(用<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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user