爬取个人课表及成绩

目标

用Python爬虫爬取学校教务网站上的个人课表, 并解析出课表内容进行初步显示

思路

  • 爬取
    • 登录教务网站
    • 进入课表页获得改页源码
  • 解析

需要用到的库有:

  • requests : 处理网络请求, 下载, 获得源码等
  • BeautifulSoup 网页解析
  • Image : 打开显示验证码 (处理验证码的方法很多, 我这里用的是人工识别, 手工输入)
  • re 正则匹配

实现

爬取部分
登录到个人首页
  • 查看学校网站源码获得几个必要的网址, 以及提交请求时需要哪些数据
    • 登录页, 右键选择检查, 选择 network项, 勾选 preserve log 项.

  • 在浏览器尝试登录, 找到其中发出 post 请求的项. 查看并且分析其中的信息:
    这里包括后面都是具体情况具体分析, 毕竟没有一劳永逸的爬虫

    • 记下接受请求的网址 Request URL
      ——–这是网址
    • 在request headers 中, 记下 Connection 这个键值对, 记下 Uset-Agent , 注意 Referer 的值
      ——–这是提交请求的heades部分
    • 继续往下, 在 Form Data 中, 有 __VIEWSTATE 这个长字符串能在登录页的源码中找到, txtUserName 是用户名, 自己设定, TextBox2 是密码, 自己设定, txtSecretCode 是验证码, 后面手工输入, Button1 对应的是登录按钮, 此处置空但不能少.
      ———这是提交请求的data部分





    • 回到登录页, 邮右键 查看网页源代码:
      • 可以找到 __VIEWSTATE 的值, 解析源码即可获得
      • 可以看到验证码的相对网址, requests 请求下载保存到本地即可, 然后用 Image 库打开图片

    • BeautifulSoup 解析登录页源码, 从用户处获得验证码, 将要提交的数据 headersdata 包装好, 发出 post 请求就正常登录了. 登录后拿到的源码就是个人教务首页的源码了
    进入课表页
    • 观察首页, 可以看到课表页需要另外跳转
    • 类似上面步骤, 打开检查, 勾选 preserve log 跳转到 点击跳转到课表页, 寻找拿到了课表信息的那一条请求
    • 点击 doc 进行筛选, 选中那一条请求, 点击 response 查看请求得到的代码, 发现是课程信息, 可以确定这条请求就是要找的请求
    • 点击 headers , 查看请求详细信息
      • 可以发现用的方法是 get , 记下 Request URL , 记下 Request Headers 中的 Referer 网址
    • 用上面的信息, 发一次 get 请求就能拿到课表页的源码了
    代码
     1class Crawling:
    2    #public data
    3    originURL = 'http://...'  #作省略处理,实际即上文分析的 Request URL 的值
    4    originHeaders = {'User-Agent''Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36',
    5            'Connection''keep-alive'}  #记录headers, 伪装成浏览器访问. 'referer'的值要依据网页添加
    6    checkcodePath = './code.png'  #验证码保存路径
    7    checkcodeURL = originURL+'CheckCode.aspx'  #验证码网址
    8    session = None #一个会话, 让cookie得以保存传递. 后面都使用这个session进行post和get.
    9
    10    #personal data
    11    __originData = {'Button1':''#登录时要提交的数据: 用户名, 密码, '__VIEWSTATE'的值要从登录页源码中提取, 'txtSecretCode'(验证码)的值要手工输入
    12
    13
    14    def __init__(self, userName, passward):
    15        self.__originData['txtUserName'] = userName
    16        self.__originData['TextBox2'] = passward
    17        self.session = requests.session() 
    18
    19    def setData(self, pageSoup, headers):
    20        data = self.__originData
    21        data['__VIEWSTATE'] = pageSoup.findAll('input')[0].get('value')  #解析得到'__VIEWSTATE'的值, 将'__VIEWDATE'的值加入字典
    22        checkcode = self.session.get(self.checkcodeURL, headers=headers)  #获得验证码网页
    23        with open(self.checkcodePath, 'wb'as fp:  #保存验证码图片
    24            fp.write(checkcode.content)
    25        checkcodeImg = Image.open(self.checkcodePath)
    26        checkcodeImg.show()  #展示验证码
    27        data['txtSecretCode'] = input("请输入图片中的验证码: ")  #获得手工输入的验证码, 将验证码加入数据字典中.  数据准备完毕
    28        return data
    29
    30    def setHeaders(self, refererURL):
    31        headers = self.originHeaders
    32        headers['Referer'] = refererURL
    33        return headers
    34
    35    def login(self):
    36        loginPageURL = self.originURL+'default2.aspx'  #登录的网址(post请求网址, 同时也是referer网址)
    37
    38        loginPageCode = self.session.get(loginPageURL, headers=self.originHeaders).text  #进入登录页, 保存登录页源码
    39        loginPageSoup = BeautifulSoup(loginPageCode, 'lxml')  #登录页源码传入解析器解析
    40
    41        loginData = self.setData(loginPageSoup, self.originHeaders)  #得到post需要的data
    42        loginHeaders = self.setHeaders(loginPageURL)  #得到post需要的headers
    43        homePage = self.session.post(loginPageURL, data=loginData, headers=loginHeaders)  #发出post请求(登录), 进入个人教务系统主页
    44
    45        return homePage
    46
    47    def switchToSchedule(self, homePage):
    48        homePageURL = self.originURL+'xs_main.aspx?xh='+self.__originData['txtUserName']  #主页网址(referer网址)
    49
    50        homePageSoup = BeautifulSoup(homePage.text, 'lxml')  #解析主页源码
    51        targetURL = homePageSoup.findAll('a')[18].get('href')  #得到课表页网址, 其中有中文待处理
    52        name = re.search(r'[\u4e00-\u9fa5]{2,}', targetURL).group()  #正则匹配到中文
    53        nameInURL = str(name.encode('gb2312')).replace('\\x''%').upper()[2:-1]  #将中文转换为地址
    54        classPageURL = self.originURL+'xskbcx.aspx?xh='+self.__originData['txtUserName']+'&xm='+nameInURL+'&gnmkdm=N121603'  #得到课表页数据来源网址(get请求网址)
    55
    56        classPageHeaders = self.setHeaders(homePageURL)  #得到get需要的headers
    57        classPage = self.session.get(classPageURL, headers=classPageHeaders)  #发出get请求,得到课表页数据
    58
    59        return classPage
    60
    解析部分
    • BeautifulSoup 和 正则表达式 解析网页, 拿到课程的信息
    代码:
     1class ResolvePage:
    2    soup = None
    3    schedule = []  #存放课表
    4    scheduleTime = []  #存放当前学年学期, 索引0为学年, 索引1为学期
    5
    6    def __init__(self, pageCode):  #pageCode可以是爬取的源码也可以是源码的文件句柄
    7        self.soup = BeautifulSoup(pageCode, 'lxml')
    8
    9    def getSchedule(self):
    10        return self.schedule
    11    def getScheduleYear(self):
    12        return self.scheduleTime[0]
    13    def getScheduleSemester(self):
    14        return self.scheduleTime[1]
    15
    16    def resolveScheduleTime(self):  #获得课表所在学年与学期
    17        for option in self.soup.findAll('option'):
    18            if option.get('selected') == 'selected':
    19                self.scheduleTime.append(option.get('value'))
    20
    21    def resolveScheduleContent(self):
    22        classes = []
    23
    24        #取下包含了课表内容的源码
    25        rows = self.soup.findAll('tr')[4:17]
    26        for row in rows:
    27            columns = row.findAll('td')
    28            for column in columns:
    29                if column.get('align') == 'Center' and column.text != '\xa0':
    30                    classes.append(str(column))
    31
    32        #去除无用部分,留下<\br>用来分隔各项
    33        for i in range(len(classes)):
    34            index = classes[i].find('>')+1
    35            classes[i] = classes[i][index:-5]
    36        #合为一个字符串
    37        classes = '<br/>'.join(classes)
    38        #按分隔符拆开为列表
    39        classes =re.split(r'<br/><br/>|<br/>', classes)  #按正则表达式分割, | 为或, 遵循短路原则, 所以 | 两边顺序不可变换
    40
    41        #按科目拆开
    42        subject = []
    43        for i in range(len(classes)):
    44            subject.append(classes[i])
    45            if (i+1)%5 == 0:
    46                self.schedule.append(subject)
    47                subject = []
    48

    效果

    此为最新代码的效果,最新代码见代码仓库

    更新补充

    第二天增加了对个人成绩的爬取, 调整了代码结构, 使用了第三方库 prettytable 让爬取的课表和成绩打印的很漂亮

    如图:

    [成绩截图省略]

    参考我的代码仓库

    此为旧文,图片水印为我的CSDN账号,无妨