study_xxqg/lib/respond.go

754 lines
20 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 lib
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
rand2 "math/rand"
"net/http"
"regexp"
"strings"
"time"
"github.com/imroc/req/v3"
"github.com/mxschmitt/playwright-go"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/johlanse/study_xxqg/conf"
"github.com/johlanse/study_xxqg/model"
)
const (
MyPointsUri = "https://pc.xuexi.cn/points/my-points.html"
DailyBUTTON = `#app > div > div.layout-body > div >
div.my-points-section > div.my-points-content > div:nth-child(5) > div.my-points-card-footer > div.buttonbox > div`
WEEKEND = `#app > div > div.layout-body > div > div.my-points-section > div.my-points-content >
div:nth-child(7) > div.my-points-card-footer > div.buttonbox > div`
SPECIALBUTTON = `#app > div > div.layout-body >
div > div.my-points-section > div.my-points-content > div:nth-child(6) > div.my-points-card-footer > div.buttonbox > div`
)
// RespondDaily
/* @Description:
* @receiver c
* @param user
* @param model
*/
func (c *Core) RespondDaily(user *model.User, model string) {
// 捕获所有异常,防止程序崩溃
defer func() {
err := recover()
if err != nil {
log.Errorln("答题模块异常结束或答题已完成")
c.Push("text", "答题模块异常退出或答题已完成")
log.Errorln(err)
}
}()
// 判断浏览器是否被退出
if c.IsQuit() {
return
}
// 获取用户成绩
score, err := GetUserScore(user.ToCookies())
if err != nil {
log.Errorln("获取分数失败,停止每日答题", err.Error())
return
}
// 创建浏览器上下文对象
context, err := c.browser.NewContext()
// 添加一个script,防止被检测
_ = context.AddInitScript(playwright.BrowserContextAddInitScriptOptions{
Script: playwright.String("Object.defineProperties(navigator, {webdriver:{get:()=>undefined}});")})
if err != nil {
log.Errorln("创建实例对象错误" + err.Error())
return
}
// 在退出方法时关闭对象
defer func(context playwright.BrowserContext) {
err := context.Close()
if err != nil {
log.Errorln("错误的关闭了实例对象" + err.Error())
}
}(context)
// 创建一个页面
page, err := context.NewPage()
if err != nil {
log.Errorln("创建页面失败" + err.Error())
return
}
// 退出时关闭页面
defer func() {
page.Close()
}()
// 添加用户的cookie
err = context.AddCookies(user.ToBrowserCookies()...)
if err != nil {
log.Errorln("添加cookie失败" + err.Error())
return
}
// 跳转到积分页面
_, err = page.Goto(MyPointsUri, playwright.PageGotoOptions{
Referer: playwright.String(MyPointsUri),
Timeout: playwright.Float(10000),
WaitUntil: playwright.WaitUntilStateDomcontentloaded,
})
if err != nil {
log.Errorln("跳转页面失败" + err.Error())
return
}
log.Infoln("已加载答题模块")
// 判断答题类型,然后相应处理
switch model {
case "daily":
{
// 检测是否已经完成
if score.Content["daily"].CurrentScore >= score.Content["daily"].MaxScore {
log.Infoln("检测到每日答题已经完成,即将退出答题")
return
}
// 点击每日答题的按钮
err = page.Click(DailyBUTTON)
if err != nil {
log.Errorln("跳转到积分页面错误")
return
}
c.Push("text", "已加载每日答题模块")
}
case "weekly":
{
// 检测是否已经完成
if score.Content["weekly"].CurrentScore >= score.Content["weekly"].MaxScore {
log.Infoln("检测到每周答题已经完成,即将退出答题")
return
}
// err = page.Click(WEEKEND)
// if err != nil {
// log.Errorln("跳转到积分页面错误")
// return
//}
// 获取每周答题的ID
id, err := getweekID(user.ToCookies())
if err != nil {
return
}
// 跳转到每周答题界面
_, err = page.Goto(fmt.Sprintf("https://pc.xuexi.cn/points/exam-weekly-detail.html?id=%d", id), playwright.PageGotoOptions{
Referer: playwright.String(MyPointsUri),
Timeout: playwright.Float(10000),
WaitUntil: playwright.WaitUntilStateDomcontentloaded,
})
if err != nil {
log.Errorln("跳转到答题页面错误" + err.Error())
return
}
c.Push("text", "已加载每周答题模块")
}
case "special":
{
// 检测是否已经完成
if score.Content["special"].CurrentScore >= score.Content["special"].MaxScore {
log.Infoln("检测到特殊答题已经完成,即将退出答题")
return
}
// err = page.Click(SPECIALBUTTON)
// if err != nil {
// log.Errorln("跳转到积分页面错误")
//
// return
//}
// 获取专项答题ID
id, err := getSpecialID(user.ToCookies())
if err != nil {
return
}
// 跳转到专项答题界面
_, err = page.Goto(fmt.Sprintf("https://pc.xuexi.cn/points/exam-paper-detail.html?id=%d", id), playwright.PageGotoOptions{
Referer: playwright.String(MyPointsUri),
Timeout: playwright.Float(10000),
WaitUntil: playwright.WaitUntilStateDomcontentloaded,
})
if err != nil {
log.Errorln("跳转到答题页面错误" + err.Error())
return
}
c.Push("text", "已加载专项答题模块")
}
}
time.Sleep(5 * time.Second)
// 跳转到答题页面若返回true则说明已答完
// if getAnswerPage(page, model) {
// return
//}
tryCount := 0
for true {
label:
tryCount++
if tryCount >= 30 {
log.Panicln("多次循环尝试答题已超出30次自动退出")
}
if c.IsQuit() {
return
}
// 查看是否存在答题按钮,若按钮可用则重新提交答题
btn, err := page.QuerySelector(`#app > div > div.layout-body > div > div.detail-body > div.action-row > button`)
if err != nil {
log.Infoln("获取提交按钮失败,本次答题结束" + err.Error())
return
}
enabled, err := btn.IsEnabled()
if err != nil {
log.Errorln(err.Error())
continue
}
if enabled {
err := btn.Click()
if err != nil {
log.Errorln("提交答案失败")
}
}
// 该元素存在则说明出现了滑块
handle, _ := page.QuerySelector("#nc_mask > div")
if handle != nil {
log.Infoln(handle)
en, err := handle.IsVisible()
if err != nil {
return
}
if en {
page.Mouse().Move(496, 422)
time.Sleep(1 * time.Second)
page.Mouse().Down()
page.Mouse().Move(772, 416, playwright.MouseMoveOptions{})
page.Mouse().Up()
time.Sleep(10 * time.Second)
log.Infoln("可能存在滑块")
c.Push("text", "答题过程出现滑块,正在尝试滑动")
en, err = handle.IsVisible()
if err != nil {
return
}
if en {
page.Evaluate("__nc.reset()")
goto label
}
}
}
switch model {
case "daily":
{
// 检测是否已经完成
if score.Content["daily"].CurrentScore >= score.Content["daily"].MaxScore {
log.Infoln("检测到每日答题已经完成,即将退出答题")
return
}
}
case "weekly":
{
// 检测是否已经完成
if score.Content["weekly"].CurrentScore >= score.Content["weekly"].MaxScore {
log.Infoln("检测到每周答题已经完成,即将退出答题")
return
}
}
case "special":
{
// 检测是否已经完成
if score.Content["special"].CurrentScore >= score.Content["special"].MaxScore {
log.Infoln("检测到特殊答题已经完成,即将退出答题")
return
}
}
}
// 获取题目类型
category, err := page.QuerySelector(
`#app > div > div.layout-body > div > div.detail-body > div.question > div.q-header`)
if err != nil {
log.Errorln("没有找到题目元素" + err.Error())
return
}
_ = category.WaitForElementState(`visible`)
time.Sleep(1 * time.Second)
// 获取题目
question, err := page.QuerySelector(
`#app > div > div.layout-body > div > div.detail-body > div.question > div.q-body > div`)
if err != nil {
log.Errorln("未找到题目问题元素")
return
}
// 获取题目类型
categoryText, err := category.TextContent()
if err != nil {
log.Errorln("获取题目元素失败" + err.Error())
return
}
log.Infoln("## 题目类型:" + categoryText)
// 获取题目的问题
questionText, err := question.TextContent()
if err != nil {
log.Errorln("获取题目问题失败" + err.Error())
return
}
log.Infoln("## 题目:" + questionText)
// 获取答题帮助
openTips, err := page.QuerySelector(
`#app > div > div.layout-body > div > div.detail-body > div.question > div.q-footer > span`)
if err != nil || openTips == nil {
log.Errorln("未获取到题目提示信息")
goto label
}
log.Debugln("开始尝试获取打开提示信息按钮")
// 点击提示的按钮
err = openTips.Click()
if err != nil {
log.Errorln("点击打开提示信息按钮失败" + err.Error())
goto label
}
log.Debugln("已打开提示信息")
// 获取页面内容
content, err := page.Content()
if err != nil {
log.Errorln("获取网页全体内容失败" + err.Error())
goto label
}
time.Sleep(time.Second * time.Duration(rand2.Intn(3)))
log.Debugln("以获取网页内容")
// 关闭提示信息
err = openTips.Click()
if err != nil {
log.Errorln("点击打开提示信息按钮失败" + err.Error())
goto label
}
log.Debugln("已关闭提示信息")
// 从整个页面内容获取提示信息
tips := getTips(content)
log.Infoln("[提示信息]", tips)
// 填空题
switch {
case strings.Contains(categoryText, "填空题"):
if len(tips) < 1 {
tips = append(tips, "不知道")
}
// 填充填空题
err := FillBlank(page, tips)
if err != nil {
log.Errorln("填空题答题失败" + err.Error())
return
}
case strings.Contains(categoryText, "多选题"):
log.Infoln("读取到多选题")
options, err := getOptions(page)
if err != nil {
log.Errorln("获取选项失败" + err.Error())
return
}
log.Infoln("获取到选项答案:", options)
log.Infoln("[多选题选项]", options)
var answer []string
for _, option := range options {
for _, tip := range tips {
if strings.Contains(option, tip) {
answer = append(answer, option)
}
}
}
if len(answer) < 1 {
answer = append(answer, options...)
log.Infoln("无法判断答案自动选择ABCD")
}
log.Infoln("根据提示分别选择了", RemoveRepByLoop(answer))
// 多选题选择
err = radioCheck(page, answer)
if err != nil {
return
}
case strings.Contains(categoryText, "单选题"):
log.Infoln("读取到单选题")
options, err := getOptions(page)
if err != nil {
log.Errorln("获取选项失败" + err.Error())
return
}
log.Infoln("获取到选项答案:", options)
var answer []string
for _, option := range options {
for _, tip := range tips {
if strings.Contains(option, tip) {
answer = append(answer, option)
}
}
}
if len(answer) < 1 {
answer = append(answer, options[0])
log.Infoln("无法判断答案自动选择A")
}
log.Infoln("根据提示分别选择了", answer)
err = radioCheck(page, answer)
if err != nil {
return
}
}
score, _ = GetUserScore(user.ToCookies())
}
}
func getAnswerPage(page playwright.Page, model string) bool {
selectPages, err := page.QuerySelectorAll(`#app .ant-pagination .ant-pagination-item`)
if err != nil {
log.Errorln("获取到页码失败")
return false
}
log.Infoln("共获取到", len(selectPages), "页")
modelName := ""
modelSlector := ""
switch model {
case "daily":
return false
case "weekly":
modelName = "每周答题"
modelSlector = "button.ant-btn-primary"
case "special":
modelName = "专项答题"
modelSlector = "#app .items .item button"
}
for i := 1; i <= len(selectPages); i++ {
log.Infoln("获取到"+modelName, "第", i, "页")
err1 := selectPages[i-1].Click()
if err1 != nil {
log.Errorln("点击页码失败")
}
time.Sleep(2 * time.Second)
datas, err := page.QuerySelectorAll(modelSlector)
if err != nil {
log.Errorln("获取页面内容失败")
continue
}
for _, data := range datas {
content, err := data.TextContent()
if err != nil {
log.Errorln("获取按钮文本失败" + err.Error())
continue
}
if strings.Contains(content, "重新") || strings.Contains(content, "满分") {
continue
} else {
if strings.Contains(content, "电影试题") {
log.Infoln("发现有未答题的电影试题")
continue
}
enabled, err := data.IsEnabled()
if err != nil {
return false
}
if enabled {
log.Infoln("按钮可用")
}
data.WaitForElementState("stable", playwright.ElementHandleWaitForElementStateOptions{Timeout: playwright.Float(10000)})
time.Sleep(5 * time.Second)
err = data.Click(playwright.ElementHandleClickOptions{
Button: nil,
ClickCount: playwright.Int(2),
Delay: nil,
Force: nil,
Modifiers: nil,
NoWaitAfter: nil,
Position: nil,
Timeout: playwright.Float(100000),
})
if err != nil {
log.Errorln("点击按钮失败" + err.Error())
time.Sleep(2 * time.Second)
continue
}
time.Sleep(3 * time.Second)
return false
}
}
}
log.Infoln("检测到每周答题已经完成")
return true
}
func radioCheck(page playwright.Page, answer []string) error {
radios, err := page.QuerySelectorAll(`.q-answer.choosable`)
if err != nil {
log.Errorln("获取选项失败")
return err
}
log.Debugln("获取到", len(radios), "个按钮")
for _, radio := range radios {
textContent, err := radio.TextContent()
if err != nil {
log.Errorln("获取选项答案文本出现错误" + err.Error())
return err
}
for _, s := range answer {
if textContent == s {
err := radio.Click()
if err != nil {
log.Errorln("点击选项出现错误" + err.Error())
return err
}
r := rand2.Intn(2)
time.Sleep(time.Duration(r) * time.Second)
}
}
}
r := rand2.Intn(5)
time.Sleep(time.Duration(r) * time.Second)
checkNextBotton(page)
return nil
}
func getOptions(page playwright.Page) ([]string, error) {
handles, err := page.QuerySelectorAll(`.q-answer.choosable`)
if err != nil {
log.Errorln("获取选项信息失败")
return nil, err
}
var options []string
for _, handle := range handles {
content, err := handle.TextContent()
if err != nil {
return nil, err
}
options = append(options, content)
}
return options, err
}
func getTips(data string) []string {
data = strings.ReplaceAll(data, " ", "")
data = strings.ReplaceAll(data, "\n", "")
compile := regexp.MustCompile(`<fontcolor="red">(.*?)</font>`)
match := compile.FindAllStringSubmatch(data, -1)
var tips []string
for _, i := range match {
tips = append(tips, i[1])
}
return tips
}
func FillBlank(page playwright.Page, tips []string) error {
video := false
var answer []string
for _, tip := range tips {
if tip == "请观看视频" {
video = true
}
}
if video {
answer = append(answer, "不知道")
} else {
answer = tips
}
inouts, err := page.QuerySelectorAll(`div.q-body > div > input`)
if err != nil {
log.Errorln("获取输入框错误" + err.Error())
return err
}
log.Debugln("获取到", len(inouts), "个填空")
if len(inouts) == 1 && len(tips) > 1 {
temp := ""
for _, tip := range tips {
temp += tip
}
answer = strings.Split(temp, ",")
log.Infoln("答案已合并处理")
}
for i := 0; i < len(inouts); i++ {
err := inouts[i].Fill(answer[i])
if err != nil {
log.Errorln("填充答案失败" + err.Error())
continue
}
r := rand2.Intn(5)
time.Sleep(time.Duration(r) * time.Second)
}
r := rand2.Intn(2)
time.Sleep(time.Duration(r) * time.Second)
checkNextBotton(page)
return nil
}
func checkNextBotton(page playwright.Page) {
btns, err := page.QuerySelectorAll(`#app .action-row > button`)
if err != nil {
log.Errorln("未检测到按钮" + err.Error())
return
}
if len(btns) <= 1 {
err := btns[0].Check()
if err != nil {
log.Errorln("点击下一题按钮失败")
return
}
time.Sleep(2 * time.Second)
_, err = btns[0].GetAttribute("disabled")
if err != nil {
log.Infoln("未检测到禁言属性")
return
}
} else {
err := btns[1].Click()
if err != nil {
log.Errorln("提交试卷失败")
return
}
log.Infoln("已成功提交试卷")
}
}
// RemoveRepByLoop 通过两重循环过滤重复元素
func RemoveRepByLoop(slc []string) []string {
var result []string // 存放结果
for i := range slc {
flag := true
for j := range result {
if slc[i] == result[j] {
flag = false // 存在重复元素标识为false
break
}
}
if flag { // 标识为false不添加进结果
result = append(result, slc[i])
}
}
return result
}
func getSpecialID(cookies []*http.Cookie) (int, error) {
c := req.C()
c.SetCommonCookies(cookies...)
// 获取专项答题列表
repo, err := c.R().SetQueryParams(map[string]string{"pageSize": "1000", "pageNo": "1"}).Get(querySpecialList)
if err != nil {
log.Errorln("获取专项答题列表错误" + err.Error())
return 0, err
}
dataB64, err := repo.ToString()
if err != nil {
log.Errorln("获取专项答题列表获取string错误" + err.Error())
return 0, err
}
// 因为返回内容使用base64编码所以需要对内容进行转码
data, err := base64.StdEncoding.DecodeString(gjson.Get(dataB64, "data_str").String())
if err != nil {
log.Errorln("获取专项答题列表转换b64错误" + err.Error())
return 0, err
}
// 创建实例对象
list := new(SpecialList)
// json序列号
err = json.Unmarshal(data, list)
if err != nil {
log.Errorln("获取专项答题列表转换json错误" + err.Error())
return 0, err
}
log.Infoln(fmt.Sprintf("共获取到专项答题%d个", list.TotalCount))
// 判断是否配置选题顺序若ReverseOrder为true则从后面选题
if conf.GetConfig().ReverseOrder {
for i := len(list.List) - 1; i >= 0; i-- {
if list.List[i].TipScore == 0 {
log.Infoln(fmt.Sprintf("获取到未答专项答题: %vid: %v", list.List[i].Name, list.List[i].Id))
return list.List[i].Id, nil
}
}
} else {
for _, s := range list.List {
if s.TipScore == 0 {
log.Infoln(fmt.Sprintf("获取到未答专项答题: %vid: %v", s.Name, s.Id))
return s.Id, nil
}
}
}
log.Warningln("你已不存在未答的专项答题了")
return 0, errors.New("未找到专项答题")
}
func getweekID(cookies []*http.Cookie) (int, error) {
c := req.C()
c.SetCommonCookies(cookies...)
repo, err := c.R().SetQueryParams(map[string]string{"pageSize": "500", "pageNo": "1"}).Get(queryWeekList)
if err != nil {
log.Errorln("获取每周答题列表错误" + err.Error())
return 0, err
}
dataB64, err := repo.ToString()
if err != nil {
log.Errorln("获取每周答题列表获取string错误" + err.Error())
return 0, err
}
data, err := base64.StdEncoding.DecodeString(gjson.Get(dataB64, "data_str").String())
if err != nil {
log.Errorln("获取每周答题列表转换b64错误" + err.Error())
return 0, err
}
list := new(WeekList)
err = json.Unmarshal(data, list)
if err != nil {
log.Errorln("获取每周答题列表转换json错误" + err.Error())
return 0, err
}
log.Infoln(fmt.Sprintf("共获取到每周答题%d个", list.TotalCount))
if conf.GetConfig().ReverseOrder {
for i := len(list.List) - 1; i >= 0; i-- {
for _, practice := range list.List[i].Practices {
if practice.TipScore == 0 {
log.Infoln(fmt.Sprintf("获取到未答每周答题: %vid: %v", practice.Name, practice.Id))
return practice.Id, nil
}
}
}
} else {
for _, s := range list.List {
for _, practice := range s.Practices {
if practice.TipScore == 0 {
log.Infoln(fmt.Sprintf("获取到未答每周答题: %vid: %v", practice.Name, practice.Id))
return practice.Id, nil
}
}
}
}
log.Warningln("你已不存在未答的每周答题了")
return 0, errors.New("未找到每周答题")
}