大一上那次因为Iwrite的作文忘记写了,最近在掘金看到了一个Golang的定时任务库,所以打算利用爬虫定时获取一下未完成的作业,部署到云服务器上,并通过邮箱通知我。
先介绍一下爬虫的基本思路
现代web开发中,当用户登陆后,通常服务端会给用户签发一个令牌,这个令牌通常是Token,session或者其他的,可以被服务端验证的一段文本数据,之后的任何api请求都会去验证这个令牌,确保接口不会被滥用,保护用户隐私数据。
需要使用的库
- Http请求库 Resty:github.com/go-resty/resty/v2
- 爬虫库 Colly:github.com/gocolly/colly
- 定时任务库 Cron:github.com/robfig/cron/v3
- 邮件发送:gopkg.in/gomail.v2
Iwrite是如何实现认证服务的
登录接口
账号密码是明文传输的,并且有一个service字段,应该是使用的CAS认证,这里不过多介绍,因为我也不懂,这个过程并不难模拟,返回的数据中最有价值的就是ST。我们可以通过ST值获取iWSsiD值,相当于上文提到的令牌。
func Login() (St string,err error) {
client := resty.New()
loginData := map[string]string{
"service": "http://iwrite.unipus.cn/system/login",
"username": "xxxx",
"password": "xxxx",
}
resp, err := client.R().SetBody(loginData).Post(loginUrl)
if err != nil {
return "",err
}
resStruct := new(AutoGenerated)
json.Unmarshal([]byte(resp.String()), resStruct)
return resStruct.Rs.ServiceTicket,nil
}
type AutoGenerated struct {
Code string `json:"code"`
Msg string `json:"msg"`
Rs struct {
GrantingTicket string `json:"grantingTicket"`
ServiceTicket string `json:"serviceTicket"`
TgtExpiredTime int64 `json:"tgtExpiredTime"`
Role string `json:"role"`
Openid string `json:"openid"`
Nickname string `json:"nickname"`
Fullname any `json:"fullname"`
Username string `json:"username"`
Mobile string `json:"mobile"`
Email any `json:"email"`
Perms string `json:"perms"`
IsSsoLogin string `json:"isSsoLogin"`
IsCompleted any `json:"isCompleted"`
OpenidHash any `json:"openidHash"`
Jwt string `json:"jwt"`
Rt string `json:"rt"`
CreateTime any `json:"createTime"`
Status int `json:"status"`
Links []struct {
Rel string `json:"rel"`
Href string `json:"href"`
} `json:"links"`
} `json:"rs"`
}
获取到我们想要的令牌
我们可以通过ST值获取iWSsiD值,相当于上文提到的令牌,这里需要注意一下禁止302重定向,来拦截Cookie。
const Url = "http://iwrite.unipus.cn/system/login"
classInfos = make([]ClassInfo, 0)
//获取iWSsiD值
client := resty.New()
resp, err := client.SetRedirectPolicy(resty.NoRedirectPolicy()).R().SetQueryParam("ticket", ST).Get(Url)
if len(resp.Cookies()) < 3 {
return 0, nil, errors.New("获取iWSsiD值 失败")
}
iwssid := resp.Cookies()[2].Value
使用令牌进入用户主页并获取作业
这里我们采用Colly库,可以很方便的获取到指定Id/Class的内容
const writerUrl = "http://iwrite.unipus.cn/student"
c := colly.NewCollector()
c.OnRequest(func(r *colly.Request) {
r.Headers.Add("Cookie", "iWSsiD=\""+iwssid+"\"")
})
c.OnHTML(".table-top", func(e *colly.HTMLElement) {
var classInfo ClassInfo
classInfo.tableName = e.ChildText("h2.table-name")
classInfo.class = e.ChildText("div.data.date-con.class-name")
classInfo.time = e.ChildText("div.time_oneLine span")
classInfos = append(classInfos, classInfo)
})
c.OnHTML("#overNoId", func(e *colly.HTMLElement) {
text := e.Text
num, _ = strconv.Atoi(text)
})
c.Visit(writerUrl)
将获取的数据通过邮箱发送
其中的xxx为你的邮箱密码
func SendEmail(num int, classInfos []ClassInfo) error {
dialer := gomail.NewDialer(
"smtp.qq.com",
465,
"2945294768@qq.com",
"xxx",
)
m := gomail.NewMessage()
m.SetAddressHeader("From", "2945294768@qq.com", "hackerxiao")
m.SetHeader("To", "2945294768@qq.com")
m.SetHeader("Subject", "Iwrite作业提醒")
classInfoHTML := ""
for _, classInfo := range classInfos {
classInfoHTML += fmt.Sprintf("<p>%s</p><p>%s</p><p>%s</p><br><br>", classInfo.tableName, classInfo.class, classInfo.time)
}
body := fmt.Sprintf(`<div>
<div>
Iwrite作业提醒 还有%d个作业未提交
</div>
<div style="padding: 8px 40px 8px 50px;">
%s
</div>
</div>`,num, classInfoHTML)
m.SetBody("text/html", body)
if err := dialer.DialAndSend(m); err != nil {
return err
}
return nil
}
使用Cron定时执行
func main() {
c := cron.New()
c.AddFunc("0 18 * * *", func() {
st, err := iwrite.Login()
if err != nil {
fmt.Println("login error")
}
num, classINfos, err := iwrite.GetWriter(st)
if err != nil {
fmt.Println("GetWriter error", err)
}
iwrite.SendEmail(num, classINfos)
})
c.Run()
}