Python3 正方爬虫

2018-08-11 Yazzyk

本篇文章以我所用教务系统为例

由于学校把课表关闭了,所以我现在先爬取的成绩查询 本项目的完整代码地址:Github-zhengfang 首先观察学校官网的地址: 原本的地址是 https://220.167.53.63:95/ 然后变成 https://220.167.53.63:95/(4eggunep4b4jhzfwhk4pva21)/default2.aspx 首先端口使用的95端口,95端口是非安全端口,现在的主流浏览器应该是无法访问的,这里我只提供要用到的Chrome的Mac版的解决方案,具体可以参考我的文章 Chrome/Firefox非安全端口问题 打开终端复制以下命令,最后的--explicitly-allowed-ports=95中的95就是你想打开的非安全端口号

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome –explicitly-allowed-ports=95

然后是(4eggunep4b4jhzfwhk4pva21)中间的这窜数字,刷新几次后发现这个是会变的,也就是不确定的 default2.aspx是登录页面 再来看看NetWork的信息 在我们访问https://220.167.53.63:95/时进行了以个302跳转,那么在302跳转之前是个什么东西呢? 我用Burp Suite Professional抓包工具来看看: 得到上图中的信息然后我们可以发现(bfpyl355aa3owdf50bdgfg55)正好是我们需要的那段随机生成的值,也就是说,我们可以通过先爬取这个页面中的内容获得这个值,我们将这个值命名为token,然后代码如下: (由于这个博客的编辑器比较皮,所以格式可能有点不对,没法输制表符)

-*- coding: UTF-8 -*-
    import urllib.parse
    import requests
    import re
    import PIL
    from PIL import Image
    from bs4 import BeautifulSoup

    session = requests.Session() # 初始化session

    class ZF:
     初始化
         def __init__(self):
               self.url = 'https://220.167.53.63:95/' # 链接
               s = session.get('{}default2.aspx'.format(self.url),allow_redirects=False) # allow_redirects:重定向;会话,get方法获取页面,防止302重定向
               pattern = re.compile("href='/\((\w*)\)") # 正则表达式
               token = pattern.findall(s.text) # 查询页面中信息,匹配正则表达式并保存到列表token
               self.token = token[0] # 取出列表中的token,并赋值
               self.index_url = "https://220.167.53.63:95/({})/default2.aspx".format(self.token) # 定义主页地址 

接下来我们登录观察并获取信息: 通过观察发现url地址的defult2.aspx变成了xs_main.aspx?xh=[学号] 提交的表单信息中有6个值

__VIEWSTATE

不知道是什么东西,但是会变,猜测和csrf类似

内容
TextBox1 学号
TextBox2 密码
TextBox3 验证码
RadioButtonList1 登录页面学生的那个选框
Button1 永远都是空白

学号,密码,可以通过用户输入得到 Button1,和RadioButtonList1的值是固定的,而且RadioButtonList1如果在表单中不存在就会默认为学生选项 也就是说我们只需要解决验证码和__VIEWSTATE的值

回到登录页面观察form表单 很快便可以发现在form标签后面紧跟了一个input标签里面的value就是我们要的__VIEWSTATE的值 那么代码如下:

# -*- coding: UTF-8 -*-
import urllib.parse
import requests
import re
import PIL
from PIL import Image
from bs4 import BeautifulSoup

session = requests.Session()  # 初始化session


class ZF:
    # 初始化
    def __init__(self):
        self.url = 'http://220.167.53.63:95/'  # 链接
        s = session.get('{}default2.aspx'.format(self.url),
                        allow_redirects=False)  # allow_redirects:重定向;会话,get方法获取页面,防止302重定向
        pattern = re.compile("href='/\((\w*)\)")  # 正则表达式
        token = pattern.findall(s.text)  # 查询页面中信息,匹配正则表达式并保存到列表token
        self.token = token[0]  # 取出列表中的token,并赋值
        self.index_url = "http://220.167.53.63:95/({})/default2.aspx".format(self.token)  # 定义主页地址
        # requests过去页面信息
        r = requests.get(self.index_url)
        r.encoding = r.apparent_encoding
        # beautifulsoup格式化内容,用lxml解析
        soup = BeautifulSoup(r.text, 'lxml')
        # 从页面中获取__VIEWSTATE
        self.__VIEWSTATE = soup.select_one('input[name="__VIEWSTATE"]').get("value")
        # 学号和密码
        # self.user = ''
        # self.pwd = ''
        self.user = input("请输入学号:")
        self.pwd = input("请输入密码:")

验证码

验证码获取很简单: 直接鼠标右键复制图片链接,复制下来会发现就是default2.aspx变成了CheckCode.aspx 那么我们只需要把验证码以二进制的形式下载下来,需要的时候调用,能打开显示就行 代码如下:

# 获取验证码图片并保存至本地,再打开
    def captchas(self):
        try:
            url = "https://220.167.53.63:95/({})/CheckCode.aspx".format(self.token)  # 验证码图片地址
            img_r = requests.get(url)  # 获取验证码图片
            # 以二进制保存至本地
            with open('./img/captcha.jpg', 'wb') as f:
                f.write(img_r.content)
            # 利用PIL打开图片
            PIL.Image.open('./img/captcha.jpg').show()
            # 让用户输入验证码
            return input("请输入验证码:")
        except:
            print("error:获取验证码失败") 

获取了需要的信息,我们就可以进行模拟登陆了,这里我们登录后获取一下用户姓名并URL化,后面会用到:

模拟登陆

    def login(self):
        # http://220.167.53.63:95/(2gfd5m3bax5l2rigc0yfiofq)/xs_main.aspx?xh=201713013059
        try:

            url = "http://220.167.53.63:95/({})/default2.aspx".format(self.token)  # 生成登陆地址
            # 用字典保存data
            data = {
                '__VIEWSTATE': self.__VIEWSTATE,
                'TextBox1': self.user,
                'TextBox2': self.pwd,
                'TextBox3': self.captchas(),
                'Button1': ''
            }
            r = session.post(url, data=data)
            r.encoding = r.apparent_encoding  # 修改编码为网页编码
            # 检查提交后URL中是否含有'xs_main.aspx?xh=',如果有就表示登陆成功
            if 'xs_main.aspx?xh=' in r.url:
                soup = BeautifulSoup(r.text, 'lxml')
                print('登陆成功\n欢迎您:', soup.select_one('.info span#xhxm').text)
                # 获取姓名
                name = re.sub('同学', '', soup.select_one('.info span#xhxm').text)
                # 将姓名URL化
                xm = urllib.parse.quote(name)
                return xm
            else:
                print('登陆失败', r.url)
        except:
            print("error:登录出错")

接下来进行一次查询操作 可以看到这个东西
https://220.167.53.63:95/(xg1yk155oifue555urpnb0ng)/xscjcx.aspx?xh=\[学号\]&xm=\[URL化的姓名\]&gnmkdm=N121605
这个地址是声明我们从哪里来的,我们可以由拼接生成

 # 获取URL化后的姓名
            name = self.login()#这里是刚刚获取的URL话的名字
            grade_url = "{}({})/xscjcx.aspx?xh={}&xm={}&gnmkdm=N121605".format(self.url, self.token, self.user, name)  # 生成URL 

我们要把这个写入headers这里要注意的是这个headers里Referer是一定要有的

    header = {
                    "Referer": grade_url,  # Referer必须有
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36"
                }  # 生成header 

再来看下我们提交的东西 这一看,妈哟!这么长的什么东西!? 不要慌,回到之前页面看看 从表单那可以看到这么些东西
  可以发现这些值就是我们需要的,那么爬取下来即可

    r = requests.get(grade_url, headers=header)
                r.encoding = r.apparent_encoding
                """
                r = requests.get("{}/{}/xscjcx.aspx?{}".format(self.url, self.token, urllib.parse.urlencode({
                    'xh': self.user,
                    'xm': info[2],
                    'gnmkdm': 'N121603'
                })))
                """
                soup = BeautifulSoup(r.text, 'lxml')
                # 获取表单信息
                __EVENTTARGET = soup.select_one('input[name="__EVENTTARGET"]').get("value")
                __EVENTARGUMENT = soup.select_one('input[name="__EVENTARGUMENT"]').get("value")
                new__VIEWSTATE = soup.select_one('input[name="__VIEWSTATE"]').get("value") 
对应的value
那么还有4个值 ddlXN
表单中的学年 ddlXQ
表单中的学期 ddl_kcxz
表单中的课程性质 btn_xq
点击的哪个按钮,这里我默认用学期成绩按钮

这里的ddlXNddlXQ我们让用户来输入 ddl_kcxz设为空和btn_xq为URL化的“学期成绩” 把表单信息存入data

 grade_data = {
                "__EVENTTARGET": __EVENTTARGET,
                "__EVENTARGUMENT": __EVENTARGUMENT,
                "__VIEWSTATE": new__VIEWSTATE,
                "ddlXN": input("请输入学年(如:2017-2018):"),
                "ddlXQ": input("请输入学期(如:1):"),
                "ddl_kcxz": "",
                "btn_xq": urllib.parse.quote("学期成绩")
            } 

有了信息后我们进行访问,会获得一些信息,然后我们将这些信息提取出来,然后写入HTML文档里

            grade_html = session.post(grade_url, headers=header, data=grade_data)
            soup_grade = BeautifulSoup(grade_html.text, 'lxml')
            title = soup_grade.select('font[size="4"]')[0].text  # 标题
            xh = soup_grade.select_one('span#lbl_xh').text  # 学号
            xm = soup_grade.select_one('span#lbl_xm').text  # 姓名
            xy = soup_grade.select_one('span#lbl_xy').text  # 学院
            zy = '专业:{}'.format(soup_grade.select_one('span#lbl_zymc').text)  # 专业
            xzb = soup_grade.select_one('span#lbl_xzb').text  # 行政班
            table = soup_grade.select_one('table#Datagrid1').prettify()  # 成绩表
            grade_table = re.sub('"datelist"', '"mdui-table"', table)
            grade = title + "<br/>" + xh + "<br/>" + xm + "<br/>" + xy + "<br/>" + zy + "<br/>" + xzb + "<br/>" + grade_table
            html = '''<!doctype html>
    <html lang="zh-cn">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>{}同学的成绩单</title>
        <link rel="stylesheet" href="http://cdnjs.loli.net/ajax/libs/mdui/0.4.1/css/mdui.min.css">
    </head>
    <body>
            '''.format(urllib.parse.unquote(name))
            # 将内容写入html文件
            with open('./index.html', 'a+') as f:
                f.truncate(0)
                f.write(html + grade)
            print("it's done!")
        except:
            print("error:获取成绩单出错")

完整代码: GitHub 接下来,我想把这个用Django写成web端,但是验证码哪里给卡着了