使用爬蟲爬取超星學習通的作業時間並且通過郵件提醒!

爬遍天下無敵手發表於2020-10-27

簡介

因為本人十分愛忘記做作業,因此,想通過爬蟲,爬取超星的學習通作業時間,並且進行定時提醒。

環境

  1. 阿里雲輕量級伺服器
  2. Centos7
  3. Anaconda3

先看效果展示

爬取過程


傳送郵件

過程

需要了解的知識

  1. http的訪問機制
  2. cookies,session是用來幹嘛的
  3. 驗證碼登入的流程
  4. 頁面的機制
  5. python幾個包的使用
  • requetst(網頁請求包)
  • lxml,etree(網頁介面處理包)
  • email,smtplib(郵件處理包)
  • muggle-ocr(驗證碼識別)
  • datetime和time(時間包)
  1. Centos下的anaconda的使用
  2. stmp郵件協議講解
  3. Centos如何進行定時任務
  4. Centos關於郵件傳送的埠

開始

(對於部分模組,有些部落格寫的非常好,我就不進行詳述,但是會提供連結)

1.http的訪問機制

一文搞懂HTTP協議(帶圖文)

2.cookies,session是用來幹嘛的

cookie和session的區別
session和cookies的區別

3. 驗證碼登入的流程

驗證碼的原理及作用
簡單來說(以超星學習為例子):

  • 每次進入登入頁面,它會先請求一個驗證碼的網址(不用管它code?***是什麼。,它只是一個通過js的datetime函式得到的一個時間戳(不明白的同學可以去搜一搜)。不要太在意這部分,一開始,我就走入歧途,想通過這個來獲取驗證碼,其實思路就錯了,引以為戒)

  • 進入該連結之後,得到一張圖片

    此時,表面上得到的是一張圖片,實際上,在伺服器端,它還生成了與之匹配的cookies資訊,並且返回給了登入頁面。而之後我們客戶端就必須攜帶這個cookies以及賬號密碼資訊進行訪問。

4. 頁面的機制

使用的chrome瀏覽器,開啟F12就可以瞭解整個頁面的轉換過程。主要分為:

  • html靜態網頁(容易獲取)
  • js後臺操作函式(通過相關函式來進行動態載入顯示頁)
    就比如這裡的驗證碼圖片,並不是靜態載入的,因此直接獲取到的html中並沒有該圖片的連結,反而是通過一個函式進行動態載入。
     

開啟F12後主要頁面如下:

 

 


在最後的圖中的formdata就是本次登入的資訊(賬號,密碼(base64加密),驗證碼等)

程式碼

#coding=UTF-8
#File name :爬蟲超星
#Author:龍文漢
#Data:2020.10.16
#Description:使用爬蟲爬取超星的作業詳情,獲取作業的截至時間
import time
import json
import requests
from lxml import etree
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import os
import base64
import datetime
import smtplib
from email.header import Header
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
import threading
import sys
import muggle_ocr

class PaChaongxin():
    #main_function
    def __init__(self,username,password,emai):
        self.username = username
        self.password = password
        self.to = emai
        self.code = 0
        self.code_status = None #驗證碼正確還是失敗
        self.user_pas_status = None #賬戶名和密碼
        self.sender_mail = 'xxxx@xxx'#傳送者郵件
        self.sender_pass = 'xxxxxxx'  # 郵箱的stm密碼
        self.session = requests.session()
        self.header = {
            'User-Agent': 'xxxxxxxx'#自己的user_agent
        }

    def User_Pas(self):
        #輸入賬號,密碼
        # self.username = input("請輸入學號:")
        # self.password = input("請輸入密碼:")
        # self.to = input("請輸入郵箱:")
        #self.username = xxxxxxx
        #self.password = 'xxxxx'
        #self.to = 'xxxxxx'
        return

    def Get_code(self):
        #獲取驗證碼,以及攜帶的cookies
        code_url = 'https://passport2.chaoxing.com/num/code'#超星驗證碼網址
        path_path = 'vari_code.png'
        code_response = self.session.get(code_url)
        #儲存驗證碼
        img = open(path_path,'wb')
        img.write(code_response.content)
        img.close()
        #顯示驗證碼,並且初始化,人為輸入,不適用識別程式
        # img_open = Image.open('vari_code.png')
        # img = mpimg.imread('vari_code.png',0)
        # plt.imshow(img)  # 顯示圖片
        # plt.axis('off')  # 不顯示座標軸
        # plt.show()
        # self.code = input("請輸入驗證碼:")
        print("befor:",self.code)
        self.code = self.Code_Verifed()
        print("after:", self.code)
        os.remove(path_path)

    def Load_Page(self):
        #使用session進入登入介面驗證
        self.password = base64.b64encode(self.password.encode("utf-8"))  # 被編碼的引數必須是二進位制資料
        param = {
            'fid': 'xxxx',
            'uname': self.username,
            'numcode': self.code,
            'password': self.password,
            'refer': 'http%3A%2F%2Fi.chaoxing.com',
            't': 'true'
        }
        header = {
            'Accept': 'application/json, text/javascript, */*; q=0.01',
            'Accept-Encoding': 'gzip, deflate, br',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,und;q=0.7',
            'Connection': 'keep-alive',
            'Content-Length': '109',
            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
            'Host': 'passport2.chaoxing.com',
            'Origin': 'https://passport2.chaoxing.com',
            'Referer': 'https://passport2.chaoxing.com/login?loginType=3&newversion=true&fid=-1&refer=http%3A%2F%2Fi.chaoxing.com',
            'Sec-Fetch-Dest': 'empty',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Site': 'same-origin',
            'User-Agent': 'xxxxxxxx',#注意自己更改
            'X-Requested-With': 'XMLHttpRequest',
        }
        load_url = 'https://passport2.chaoxing.com/unitlogin?'
        load_response = self.session.post(load_url,headers=header,data=param)
        load_response_msg = json.loads(load_response.text)
        print(load_response_msg,load_response_msg.keys())
        if 'mes' in load_response_msg.keys():
            if load_response_msg['mes'] == '驗證碼錯誤':
                print('驗證碼錯誤')
                self.code_status = True
                while(self.code_status):
                    self.Get_code()
                    param['numcode'] = self.code
                    load_response = self.session.post(load_url, headers=header, data=param)
                    load_response_msg = json.loads(load_response.text)
                    if 'mes' not in load_response_msg.keys() or load_response_msg['mes'] != '驗證碼錯誤':
                        self.code_status = False

            if 'mes' in load_response_msg.keys() and load_response_msg['mes'] == '使用者名稱或密碼錯誤':
                print('使用者名稱或密碼錯誤')
                self.user_pas_status = True
                while(self.user_pas_status):
                    self.User_Pas()
                    self.Get_code()
                    param['numcode'] = self.code
                    param['uname'] = self.username
                    param['password'] = self.password
                    load_response = self.session.post(load_url, headers=header, data=param)
                    load_response_msg = json.loads(load_response.text)
                    if 'mes' not in load_response_msg.keys() and load_response_msg['mes'] != '使用者名稱或密碼錯誤':
                        self.code_status = False

        #上面返回的應該是登入成功,接下來,帶著新的cookies訪問主頁
        self_page_url = 'http://i.mooc.chaoxing.com'
        self_page_response = self.session.get(url=self_page_url,headers = self.header)
        #個人空間的主頁面,這裡直接提取課程的部分,要在左側的按鈕裡找到相對應的連線,
        #該課程的頁面直接鑲嵌在本頁面,所以帶著session直接訪問也可以
        # self_page_html = etree.HTML(self_page.text)
        # return self_page_html

    def Get_Class_View(self):
        #進入所有課程的介面
        class_view_url = 'http://mooc1-2.chaoxing.com/visit/courses'
        class_view = self.session.get(class_view_url,headers = self.header)
        class_view_html = etree.HTML(class_view.text)
        #直接返回整個頁面的編碼,方便後續的查詢
        return class_view_html

    def Go_to_work(self,Singel_Class_Url_after):
        #通過外面直接傳來的課程網址,直接跳轉
        #進入單個課程的介面的作業模組
        Singel_Class_Url = 'https://mooc1-2.chaoxing.com'+Singel_Class_Url_after
        single_class_response = self.session.get(Singel_Class_Url,headers=self.header)
        single_class_page = etree.HTML(single_class_response.text)
        url_after = single_class_page.xpath("/html/body/div[4]/div/div/div[2]/ul/li[6]/a/@data")
        if len(url_after) == 0:
            open_zuoye_url = 'https://mooc1-2.chaoxing.com' + \
                             single_class_page.xpath("/html/body/div[2]/div/div/div[2]/ul/li[6]/a/@data")[0]
        else:
            open_zuoye_url = 'https://mooc1-2.chaoxing.com' + \
                     single_class_page.xpath("/html/body/div[4]/div/div/div[2]/ul/li[6]/a/@data")[0]
        work_xml_response = self.session.get(open_zuoye_url,headers=self.header)
        work_xml = etree.HTML(work_xml_response.text)
        #這裡的open_zuoye直接轉到了作業的介面
        #呼叫每個作業的函式,方便多執行緒
        single_class_text = self.Get_work_time(work_xml)
        return single_class_text

    def Get_work_time(self,work_xml):
        # 以上就是關於頁面跳轉的函式,接下來就是作業的擷取
        #作業的資訊提取
        work_num = len(work_xml.xpath('//*[@id="RightCon"]/div/div/div[2]/ul/li'))
        class_name = work_xml.xpath('/html/body/div[2]/div/h1/span[1]/@title')[0]
        #郵件的內容:[[課程],[作業名稱,截至時間,剩餘時間]*n]的列表
        #無作業的課程直接返回
        if work_num == 0:
            return None
        work_text_info = []#有作業的課程
        work_text_info.append(class_name)#新增課程名
        #對每個專案進行整理
        for i in range(1,work_num+1):
            work_name = work_xml.xpath('//*[@id="RightCon"]/div/div/div[2]/ul/li['+str(i)+']/div[1]/p/a/text()')[0].strip()
            work_status = work_xml.xpath('/html/body/div[3]/div[1]/div/div/div/div[2]/ul/li['+str(i)+']/div[1]/span[3]/strong/text()')[0].strip()
            work_end_time = work_xml.xpath('//*[@id="RightCon"]/div/div/div[2]/ul/li['+str(i)+']/div[1]/span[2]/text()')
            # print(work_end_time)
            if len(work_end_time) == 0:#無截至日期的課程
                break
            else:
                work_end_time = work_end_time[0]
            print(class_name,work_name,work_end_time)
            if work_status != '已完成' and work_status != "待批閱":#存在未完成的課程
                work_text_info.append([])
                #增加作業名
                work_text_info[-1].append(work_name)
                #使用datatime,計算時間差
                current_time = time.strftime("%Y-%m-%d %H:%M", time.localtime())
                #轉化位datatime的格式
                current_time_tran = datetime.datetime.strptime(current_time ,"%Y-%m-%d %H:%M")
                work_end_time_tran = datetime.datetime.strptime(work_end_time ,"%Y-%m-%d %H:%M")
                time_mul = work_end_time_tran - current_time_tran
                time_mul_day = time_mul.days
                if time_mul_day <= 1:
                    time_m, time_s = divmod(time_mul.seconds, 60)
                    time_h, time_m = divmod(time_m, 60)
                    lea_time = '0天:'+str(time_h)+'小時:'+str(time_m)+'分鐘'
                    #增加剩餘時間
                    work_text_info[-1].append(lea_time)
                else:
                    lea_time = str(time_mul_day)+'天'
                    work_text_info[-1].append(lea_time)
        print(class_name,"已經結束")
        #返回單課程的作業資訊
        return work_text_info

    def Code_Verifed(self):
        sdk = muggle_ocr.SDK(model_type=muggle_ocr.ModelType.Captcha)
        with open(r'vari_code.png', 'rb') as f:
            captcha_bytes = f.read()
        code = sdk.predict(image_bytes=captcha_bytes)
        return code

    def send_email_by_qq(self,text):
        # 利用郵箱發郵件提醒
        # 設定總的郵件體物件,物件型別為mixed
        msg_root = MIMEMultipart('mixed')
        # 郵件新增的頭尾資訊等
        msg_root['From'] = 'xxxx@xxxxx<xxxx@xxxx>'
        msg_root['To'] = self.to
        # 郵件的主題,顯示在接收郵件的預覽頁面
        subject = '快到作業截止時間了!'
        msg_root['subject'] = Header(subject, 'utf-8')

        # 構造文字內容
        text_inf = "未完成作業總覽:\n"
        for i in range(0,len(text)):
            text_inf += text[i][0]+"\n"#新增標題
            for x in range(1,len(text[i])):
                text_inf += '\t\t'
                text_inf += str(text[i][x])
            text_inf += "\n\n"
        text_sub = MIMEText(text_inf, 'plain', 'utf-8')
        print(text_inf)
        msg_root.attach(text_sub)

        # # 構造超文字
        # url = "https://blog.csdn.net/chinesepython"
        # html_info = """
        # <p>點選以下連結,你會去向一個更大的世界</p>
        # <p><a href="%s">click me</a></p>
        # <p>i am very galsses for you</p>
        # """% url
        # html_sub = MIMEText(html_info, 'html', 'utf-8')
        # # 如果不加下邊這行程式碼的話,上邊的文字是不會正常顯示的,會把超文字的內容當做文字顯示
        # html_sub["Content-Disposition"] = 'attachment; filename="csdn.html"'
        # # 把構造的內容寫到郵件體中
        # msg_root.attach(html_sub)

        # # 構造圖片
        # image_file = open(r'D:\python_files\images\test.png', 'rb').read()
        # image = MIMEImage(image_file)
        # image.add_header('Content-ID', '<image1>')
        # # 如果不加下邊這行程式碼的話,會在收件方方面顯示亂碼的bin檔案,下載之後也不能正常開啟
        # image["Content-Disposition"] = 'attachment; filename="red_people.png"'
        # msg_root.attach(image)

        # # 構造附件
        # txt_file = open(r'D:\python_files\files\hello_world.txt', 'rb').read()
        # txt = MIMEText(txt_file, 'base64', 'utf-8')
        # txt["Content-Type"] = 'application/octet-stream'
        # #以下程式碼可以重新命名附件為hello_world.txt
        # txt.add_header('Content-Disposition', 'attachment', filename='hello_world.txt')
        # msg_root.attach(txt)

        try:
            
            sftp_obj = smtplib.SMTP_SSL('smtp.qq.com', 465)
            sftp_obj.login(self.sender_mail, self.sender_pass)
            sftp_obj.sendmail(self.sender_mail, self.to, msg_root.as_string())
            sftp_obj.quit()
            print('sendemail successful!')

        except Exception as e:
            print('sendemail failed next is the reason')
            print(e)

    def Begin(self):
        self.User_Pas()#獲取資訊
        self.Get_code()#獲取驗證碼以及cookies資訊,用session進行儲存
        self.Load_Page()#進入個人中心,更新cookies
        class_view_html = self.Get_Class_View()#進入所有作業的單頁面,並且返該介面
        len_class = len(class_view_html.xpath('/html/body/div/div[2]/div[3]/ul/li'))#計算一共有多少個課程
        all_text = []#總的郵件資訊
        for i in range(1,len_class):#開始遍歷每門課
            print('開始第',i)
            Singel_Class_Url_after = class_view_html.xpath('/html/body/div/div[2]/div[3]/ul/li['+str(i)+']/div[2]/h3/a/@href')[0]
            # print(Singel_Class_Url_after)
            # all_text.append([threading.Thread(target=self.Go_to_work,args=(Singel_Class_Url_after)).start()])
            work_text = self.Go_to_work(Singel_Class_Url_after)
            if work_text == None or len(work_text) <= 1:
                continue
            else:
                all_text.append(work_text)
            #將沒有作業的課程刪除

        return all_text


if __name__ == '__main__':
    #while 1:
        #time_list = time.strftime("%H:%M:%S", time.localtime()).split(":")
      # if time_list[0] == "08" and time_list[1] == "00" and time_list[2] == "00":
       #    pachong = PaChaongxin(201821094053,"Mmgh774109","1766446371@qq.com")
       #     work_email = pachong.Begin()
       #     pachong.send_email_by_qq(work_email)
 
      #     pachong = PaChaongxin(201821094049,"wcsba8102","2651447403@qq.com")
        #    work_email = pachong.Begin()
        #    pachong.send_email_by_qq(work_email)
    pachong = PaChaongxin(學號,密碼,郵箱)
    work_email = pachong.Begin()
    pachong.send_email_by_qq(work_email)
 
    pachong = PaChaongxin(xxxxxxx,"xxxxxxx","xxxxx@xxxxx")
    work_email = pachong.Begin()
    pachong.send_email_by_qq(work_email)




 

總結

果然任務驅動加上興趣的學習,能更加擴充套件知識面,本次例項,都瞭解了:爬蟲知識,網路知識,密碼學,雲伺服器的使用,郵件知識,python的使用。總體來說,受益匪淺。希望大家能共同進步

PS:如有需要Python學習資料的小夥伴可以加點選下方連結自行獲取

python免費學習資料以及群交流解答點選即可加入

 

相關文章