目标
用Python爬虫爬取学校教务网站上的个人课表, 并解析出课表内容进行初步显示
思路
- 爬取
- 登录教务网站
- 进入课表页获得改页源码
- 解析
需要用到的库有:
requests
: 处理网络请求, 下载, 获得源码等BeautifulSoup
网页解析Image
: 打开显示验证码 (处理验证码的方法很多, 我这里用的是人工识别, 手工输入)re
正则匹配
实现
爬取部分
登录到个人首页
- 查看学校网站源码获得几个必要的网址, 以及提交请求时需要哪些数据
- 登录页, 右键选择检查, 选择 network项, 勾选 preserve log 项.
这里包括后面都是具体情况具体分析, 毕竟没有一劳永逸的爬虫
- 记下接受请求的网址
Request URL
——–这是网址 - 在request headers 中, 记下
Connection
这个键值对, 记下Uset-Agent
, 注意Referer
的值
——–这是提交请求的heades
部分 - 继续往下, 在
Form Data
中, 有__VIEWSTATE
这个长字符串能在登录页的源码中找到,txtUserName
是用户名, 自己设定,TextBox2
是密码, 自己设定,txtSecretCode
是验证码, 后面手工输入,Button1
对应的是登录按钮, 此处置空但不能少.
———这是提交请求的data
部分
- 回到登录页, 邮右键 查看网页源代码:
- 可以找到
__VIEWSTATE
的值, 解析源码即可获得 - 可以看到验证码的相对网址,
requests
请求下载保存到本地即可, 然后用Image
库打开图片
- 可以找到
- 用
BeautifulSoup
解析登录页源码, 从用户处获得验证码, 将要提交的数据headers
和data
包装好, 发出 post 请求就正常登录了. 登录后拿到的源码就是个人教务首页的源码了
进入课表页
- 观察首页, 可以看到课表页需要另外跳转
- 类似上面步骤, 打开检查, 勾选
preserve log
跳转到 点击跳转到课表页, 寻找拿到了课表信息的那一条请求 - 点击
doc
进行筛选, 选中那一条请求, 点击response
查看请求得到的代码, 发现是课程信息, 可以确定这条请求就是要找的请求
- 点击
headers
, 查看请求详细信息- 可以发现用的方法是 get , 记下
Request URL
, 记下 Request Headers 中的Referer
网址
- 可以发现用的方法是 get , 记下
- 用上面的信息, 发一次 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账号,无妨