This commit is contained in:
184
parser/cet.go
Normal file
184
parser/cet.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CETScore 四六级成绩条目。
|
||||||
|
type CETScore struct {
|
||||||
|
Level string // 等级:英语四级/英语六级
|
||||||
|
ExamTime string // 考试时间:如 2025年06月
|
||||||
|
TicketNum string // 准考证号
|
||||||
|
Score string // 成绩
|
||||||
|
}
|
||||||
|
|
||||||
|
// CETStudentInfo 四六级查询中的学生信息。
|
||||||
|
type CETStudentInfo struct {
|
||||||
|
StudentID string // 学号
|
||||||
|
Name string // 姓名
|
||||||
|
Gender string // 性别
|
||||||
|
IDCard string // 身份证号
|
||||||
|
Duration string // 学制
|
||||||
|
Grade string // 年级
|
||||||
|
College string // 学院
|
||||||
|
Major string // 专业
|
||||||
|
}
|
||||||
|
|
||||||
|
// CETResult 四六级查询结果。
|
||||||
|
type CETResult struct {
|
||||||
|
StudentInfo CETStudentInfo
|
||||||
|
Scores []CETScore
|
||||||
|
}
|
||||||
|
|
||||||
|
// cetScoreRegex 匹配四六级成绩行。
|
||||||
|
// 示例:英语六级 时间 2025年06月 准考证号 372220251213105 成绩 445
|
||||||
|
var cetScoreRegex = regexp.MustCompile(`(英语[四六]级)\s+时间\s*([0-9]{4}年[0-9]{1,2}月)\s+准考证号\s*([0-9A-Za-z]+)\s+成绩\s*([0-9]+|缺考|合格|不合格|优秀|良好|通过|未通过)`)
|
||||||
|
|
||||||
|
// FetchCETHTML 解析四六级成绩 HTML 页面。
|
||||||
|
func ParseCETHTML(html string) (*CETResult, error) {
|
||||||
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &CETResult{}
|
||||||
|
table := findCETTable(doc)
|
||||||
|
if table.Length() == 0 {
|
||||||
|
return nil, fmt.Errorf("未找到四六级成绩表格")
|
||||||
|
}
|
||||||
|
|
||||||
|
parseCETStudentInfo(table, &result.StudentInfo)
|
||||||
|
result.Scores = parseCETScores(table)
|
||||||
|
if len(result.Scores) == 0 {
|
||||||
|
result.Scores = parseCETScoresFromText(doc.Find(".jdcx_area").Text())
|
||||||
|
}
|
||||||
|
if len(result.Scores) == 0 {
|
||||||
|
result.Scores = parseCETScoresFromText(doc.Find(".Contentbox").Text())
|
||||||
|
}
|
||||||
|
if len(result.Scores) == 0 {
|
||||||
|
result.Scores = parseCETScoresFromText(doc.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCETTable(doc *goquery.Document) *goquery.Selection {
|
||||||
|
selectors := []string{
|
||||||
|
"table.addlist",
|
||||||
|
".jdcx_area table",
|
||||||
|
"form#ff table",
|
||||||
|
"table:contains('已有成绩')",
|
||||||
|
}
|
||||||
|
for _, selector := range selectors {
|
||||||
|
table := doc.Find(selector).First()
|
||||||
|
if table.Length() > 0 {
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doc.Find("__not_found__")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCETStudentInfo(table *goquery.Selection, info *CETStudentInfo) {
|
||||||
|
table.Find("tr").Each(func(_ int, row *goquery.Selection) {
|
||||||
|
var label string
|
||||||
|
row.Find("th, td").Each(func(_ int, cell *goquery.Selection) {
|
||||||
|
text := normalizeCETText(cell.Text())
|
||||||
|
if text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cell.Is("th") {
|
||||||
|
label = normalizeCETLabel(text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assignCETStudentField(info, label, text)
|
||||||
|
label = ""
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func assignCETStudentField(info *CETStudentInfo, label, value string) {
|
||||||
|
switch label {
|
||||||
|
case "学号":
|
||||||
|
info.StudentID = value
|
||||||
|
case "姓名":
|
||||||
|
info.Name = value
|
||||||
|
case "性别":
|
||||||
|
info.Gender = value
|
||||||
|
case "身份证号":
|
||||||
|
info.IDCard = value
|
||||||
|
case "学制":
|
||||||
|
info.Duration = value
|
||||||
|
case "年级":
|
||||||
|
info.Grade = value
|
||||||
|
case "学院":
|
||||||
|
info.College = value
|
||||||
|
case "专业":
|
||||||
|
info.Major = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCETScores(table *goquery.Selection) []CETScore {
|
||||||
|
var scores []CETScore
|
||||||
|
table.Find("tr").Each(func(_ int, row *goquery.Selection) {
|
||||||
|
rowText := normalizeCETText(row.Text())
|
||||||
|
if !strings.Contains(rowText, "英语四级") && !strings.Contains(rowText, "英语六级") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scores = append(scores, parseCETScoresFromText(rowText)...)
|
||||||
|
})
|
||||||
|
return deduplicateCETScores(scores)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCETScoresFromText(text string) []CETScore {
|
||||||
|
text = normalizeCETText(text)
|
||||||
|
matches := cetScoreRegex.FindAllStringSubmatch(text, -1)
|
||||||
|
scores := make([]CETScore, 0, len(matches))
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) < 5 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
scores = append(scores, CETScore{
|
||||||
|
Level: strings.TrimSpace(match[1]),
|
||||||
|
ExamTime: strings.TrimSpace(match[2]),
|
||||||
|
TicketNum: strings.TrimSpace(match[3]),
|
||||||
|
Score: strings.TrimSpace(match[4]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return deduplicateCETScores(scores)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deduplicateCETScores(scores []CETScore) []CETScore {
|
||||||
|
if len(scores) <= 1 {
|
||||||
|
return scores
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(scores))
|
||||||
|
result := make([]CETScore, 0, len(scores))
|
||||||
|
for _, score := range scores {
|
||||||
|
key := strings.Join([]string{score.Level, score.ExamTime, score.TicketNum, score.Score}, "|")
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
result = append(result, score)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCETLabel(label string) string {
|
||||||
|
label = normalizeCETText(label)
|
||||||
|
label = strings.ReplaceAll(label, " ", "")
|
||||||
|
label = strings.ReplaceAll(label, ":", "")
|
||||||
|
label = strings.ReplaceAll(label, ":", "")
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCETText(text string) string {
|
||||||
|
text = strings.ReplaceAll(text, "\u00a0", " ")
|
||||||
|
return strings.Join(strings.Fields(strings.TrimSpace(text)), " ")
|
||||||
|
}
|
||||||
66
service/cet.go
Normal file
66
service/cet.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"schedule_converter/client"
|
||||||
|
"schedule_converter/parser"
|
||||||
|
apperr "schedule_converter/pkg/errors"
|
||||||
|
|
||||||
|
pb "schedule_converter/proto/runner"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cetQueryPath = "/sljcjcx/sljcjcxInit"
|
||||||
|
|
||||||
|
// GetCET 获取四六级成绩:登录 → 请求四六级查询结果页 → 解析 → 转换为 pb 结果。
|
||||||
|
func (s *Service) GetCET(ctx context.Context, req *pb.GetCetPayload) (*pb.CetResultData, error) {
|
||||||
|
log := slog.With("username", req.Username)
|
||||||
|
|
||||||
|
httpClient, err := s.newAuthenticatedClient(ctx, req.Username, req.Password, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
html, err := client.FetchHTML(ctx, httpClient, "GET", cetQueryPath, nil, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, apperr.New("fetch_cet", err, "获取四六级成绩页面失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
cetResult, err := parser.ParseCETHTML(html)
|
||||||
|
if err != nil {
|
||||||
|
return nil, apperr.New("parse_cet", err, "解析四六级成绩失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("四六级成绩获取成功", "score_count", len(cetResult.Scores))
|
||||||
|
return &pb.CetResultData{
|
||||||
|
StudentInfo: convertCETStudentInfo(cetResult.StudentInfo),
|
||||||
|
Scores: convertCETScores(cetResult.Scores),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertCETStudentInfo(info parser.CETStudentInfo) *pb.CetStudentInfo {
|
||||||
|
return &pb.CetStudentInfo{
|
||||||
|
StudentId: info.StudentID,
|
||||||
|
Name: info.Name,
|
||||||
|
Gender: info.Gender,
|
||||||
|
IdCard: info.IDCard,
|
||||||
|
Duration: info.Duration,
|
||||||
|
Grade: info.Grade,
|
||||||
|
College: info.College,
|
||||||
|
Major: info.Major,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertCETScores(scores []parser.CETScore) []*pb.CetScore {
|
||||||
|
result := make([]*pb.CetScore, 0, len(scores))
|
||||||
|
for _, score := range scores {
|
||||||
|
result = append(result, &pb.CetScore{
|
||||||
|
Level: score.Level,
|
||||||
|
ExamTime: score.ExamTime,
|
||||||
|
TicketNum: score.TicketNum,
|
||||||
|
Score: score.Score,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user