学校教务系统爬取计划

本文以一名高校学子的身份,向你展示爬取一个垃圾教务系统是何种体验。

自从学校换了教务系统以后,旧的教务系统不再维护,不得以只能自己做一个推送系统。题外话,这新教务系统是真的烂啊。

需求分析

因为是自己用的,所以只需要简单爬取后再通过 STMP 或者 wxpy 进行推送,一个简单的课程推送系统就完成了。 当然,后来我发现“简单爬取”可一点都不简单。 鉴于在图书馆进行开发,我提前将敏感信息存进了一个叫 config 字典里,这样使用敏感信息的时候就不会被看到了。

requests试水

说到用 Python 写爬虫,我第一时间想到了 requests。requests 凭借它的易用性,虏获了万千“脚本男孩”的心。通过开发者工具简单分析了请求,得到了请求地址、请求信息格式、请求头。万事俱备,只差一 POST。然而现实是,当我 POST 过去的时候,返回了 405 错误。明明直接用浏览器 POST 请求没有问题,但用脚本却不行,也许是请求头出现了什么差错。

在接近两个小时的尝试,我放弃了这个方法。

selenium大法好

此计不成,我只好祭出 selenium。利用 selenium,我轻易地登陆了教务系统,但时间已经很晚,我打算把爬取信息的工作留到明天。

第二天,你猜怎么着,由于 selenium 的特征被识别,我无法进入课表界面。

我尝试在控制台将 selenium 的特征改掉:

  • window.navigator.webdriver = false
  • window.navigator.language = ‘en-US
  • …………

尝试后无果,最终发现可以通过修改 chrome 设置解决。然而教务系统的加载速度实在令人头疼,各种等待才把命中率控制在 80%

最后上代码:

from config import config
from datetime import datetime
from selenium.webdriver import ActionChains, Chrome, ChromeOptions
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


class Crawler:
    option = None
    driver = None
    today_classes = None
    username = ''
    password = ''

    def __init__(self, username=config['login_account'], password=config['login_password']):
        if self.option is None:
            self.option = ChromeOptions()
            self.option.add_experimental_option('excludeSwitches', ['enable-automation'])
            self.option.add_argument('--headless')
            self.username = username
            self.password = password
            self.driver = Chrome(options=self.option)

            def login(self):
                if self.driver:
                    self.driver.get(config['login_url'])
                    WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.CLASS_NAME, 'el-input__inner')))
                    username_text = self.driver.find_elements_by_class_name('el-input__inner')[0]
                    password_text = self.driver.find_elements_by_class_name('el-input__inner')[1]
                    submit_btn = self.driver.find_element_by_class_name('btn-login')

                    username_text.send_keys(self.username)
                    password_text.send_keys(self.password)
                    ActionChains(self.driver).move_to_element(submit_btn).click(submit_btn).perform()

                    def get_info(self):
                        """
                                                                                                                                                                                                                                                                                                                                                        store the information into object
                                                                                                                                                                                                                                                                                                                                                        :return:
                                                                                                                                                                                                                                                                                                                                                        """
                        WebDriverWait(self.driver, 30).until(EC.presence_of_element_located((By.CLASS_NAME, 'el-submenu')))
                        elective_system_tab = self.driver.find_elements_by_class_name('el-submenu')[1]
                        ActionChains(self.driver).move_to_element(elective_system_tab).click(elective_system_tab).perform()

                        WebDriverWait(self.driver, 30).until(EC.presence_of_element_located((By.CLASS_NAME, 'el-menu-item')))
                        self_classes_tab = self.driver.find_elements_by_class_name('el-menu-item')[3]
                        ActionChains(self.driver).move_to_element(self_classes_tab).click(self_classes_tab).perform()
                        self.driver.implicitly_wait(10)
                        classes = self.driver.find_elements_by_css_selector(f'td.{config["CLASS_NAME"][datetime.today().weekday()]}'
                                                                            f'>div>div>div>div')
                        class_list = []
                        for i, x in enumerate(classes):
                            text = x.get_attribute('innerHTML')
                            if text:
                                class_list.append(text)
                                self.today_classes = class_list
                                self.driver.close()

                                def show_info(self):
                                    if self.today_classes and len(self.today_classes) != 0:
                                        for class_ in self.today_classes:
                                            print(class_)

                                            def __call__(self):
                                                self.login()
                                                self.get_info()

                                                def __repr__(self):
                                                    if self.today_classes:
                                                        res = '\n'.join(self.today_classes)
                                                        return res
                                                    raise AttributeError('Classes information hasn\'t been got')

测试模块和邮件模块就不放出来了,部署到服务器后,就可以每天等待明天的上课通知了,想想都开心

续:大乌龙

作文后的那天晚上和朋友聊起教务系统,得知他居然也在写该系统的爬虫。他没有用 selenium 实现。反复交谈后,发现原来是分析请求的时候复制错了登陆链接。

Postman 一顿乱撸以后,用 requests 实现了一个版本(吐槽一下 fstring,在某些场景下真的不如 C 风格的格式化来得痛快)

class Crawler:
    session_ = None

    is_login = False

    token = ''
    session_id = ''

    account = ''
    password = ''

    info = None

    def __init__(self, account=config['login_account'],
                 password=config['login_password']):
        if self.session_ is None:
            self.session_ = session()
            self.account = account
            self.password = password

            def login(self):
                payload = f'{{"userCode":"{self.account}","password":"{self.password}","userCodeType":"account"}}'
                response = requests.request('POST', config['login_url'],
                                            data=payload,
                                            headers=config['login_headers'])
                j_response = json.loads(response.text)

                if j_response['errorCode'] != "success":
                    return
                self.token = j_response['data']['token']
                self.session_id = response.cookies.get_dict()['SESSION']
                self.is_login = True

                def get_info(self):
                    if not self.is_login:
                        return
                    headers = config['api_headers']
                    headers['TOKEN'] = self.token
                    headers['Cookie'] = f'SESSION={self.session_id}; token='
                    payload = f'{{"jczy013id":"2019-2020-1","pkgl002id":"W13414710000WH","zt":"2","pkzc":"{get_tomorrow_week_number()}"}}'
                    response= self.session_.request('POST', config['api_url'],
                                                    data=payload,
                                                    headers=headers)
                    weekday = get_tomorrow_weekday()
                    data = json.loads(response.text)['data']
                    data.sort(key=lambda x: int(x['pksjmx'][:3]))
                    self.info = [x for x in data if x['pksjmx'].startswith(f'{weekday}')]

                    def __call__(self):
                        self.login()
                        self.get_info()

                        def __repr__(self):
                            return '\n'.join([x['pksjshow']+'\n' +
                                              x['kc_name']+'\n' +
                                              x['teachernames_1']+'\n' +
                                              x['js_name'] + '\n' for x in self.info])