python爬虫+django 搭建学分积查询网站
环境:Python 2.7 + Django 1.8
名词介绍:
Python是一种面向对象,解释型计算机程序设计语言,具有丰富和强大的库。
网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动的抓取万维网信息的程序或者脚本。在这里就是通过爬虫把成绩网页抓取下来,然后再用正则表达式提取出来需要的信息。
Django是一个常用的python web框架,采用了MVC的框架模式,即模型M,视图V和控制器C,自带的功能比较多和全面,适合快速开发。
设计过程:
其实刚开始我只是学习在学习Python爬虫时跟随着教程写了一个可以查询自己学校成绩的代码,只是在命令行中显示出来成绩显得很不友好,后来想算学分积的话就要做一个图形化界面,然后就想到了做一个网页。
1.网络爬虫的实现:
相关教程:
http://cuiqingcai.com/1052.html
https://www.zhihu.com/question/20899988
这个爬虫也就是查询学分积的核心,一共包含四个部分:
1.模拟登陆到教学管理系统
2.抓取成绩界面
3.从里边提取出成绩信息
4.将成绩信息以每学科,每学期的形式进行整理
1.模拟登陆到教学管理系统
首先打开浏览器(我用的是Chrome浏览器),打开登陆界面,按F12进入开发者模式。
再点登陆,在Network窗口下点最上边的login.action,然后就可以看到Headers的详细信息
在这里可以看到请求的url,请求方式为POST,还有Request Headers,以及最重要的是下边的表单提交内容,学校的毕竟做的比较简陋,用的是明文的。然后在下边模拟登陆的过程中就要用到这两个数据username和password,关于encodePassword和session_locale:zh_CN,我发现加不加都没有影响,注意参数的名字也不能有误,要和上边的保持一致。
接下来就是要实现 模拟登陆了,就像登陆一样,目的都是为了让服务器能够识别用户的身份,进行session跟踪而储存在用户本地终端上的数据(通常经过加密),这里为了实现模拟登陆就要用到Python的urllib,urllib2,以及cookielib模块。urllib和urllib2主要用来执行各种HTTP请求,cookielib模块的主要作用是提供可存储cookie的对象,以便于与urllib2模块配合使用来访问Internet资源。Cookielib模块非常强大,我们可以利用本模块的CookieJar类的对象来捕获cookie并在后续连接请求时重新发送,以此来实现模拟登录功能。这里相关模块的具体用法都不再具体说明,可以自己去搜相关的文档。学校的网站不用添加headers就可以直接登录了。
模拟登陆代码如下所示:
- import urllib
- import urllib2
- import cookielib
- import re
- import string
- import types
- class NPU:
- def __init__(self,name,passwd):
- #登录URL
- self.loginUrl = 'http://us.nwpu.edu.cn/eams/login.action'
- #声明一个MozillaCookieJar对象实例来保存cookie,之后写入文件
- self.cookies = cookielib.MozillaCookieJar('cookie.txt')
- self.postdata = urllib.urlencode({
- 'username':name,
- 'password':passwd,
- 'encodedPassword':'',
- 'session_locale':'zh_CN',
- })
- #利用urllib2库的HTTPCookieProcessor对象来创建cookie处理器
- #handler = urllib2.HTTPCookieProcessor(cookie)
- #通过handler来构建opener
- #opener = urllib2.build_opener(handler)
- #下边一句把上边两句合一块了
- self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookies))
- #获取本学期成绩页面
- def getPage(self):
- try:
- #建立一个请求
- request = urllib2.Request(url = self.loginUrl,data = self.postdata)
- #建立连接,模拟登陆
- result = self.opener.open(request)
- #保存cookie到文件
- self.cookies.save(ignore_discard = True, ignore_expires = True)
- except urllib2.URLError,e:
- print '连接失败'
- if hasattr(e,"reason"):
- print "error",e.reason
- return None
2.第二步就是获得成绩界面的html源码,通过这两行代码就可以实现。
- result = self.opener.open(self.gradeUrl)
- return result.read().decode('utf-8')
3.第三步:使用正则表达式将所需要的内容提取出来。
Python正则表达式教程如下:
http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html
由于返回界面的字符串非常长,从里面提取所需要的数据的话具有强大匹配字符串能力的正则表达式。
关于成绩信息,要匹配的信息有每个课程的时间,名字,学分,成绩。下边是网页中一门成绩的显示形式:
- <tr> <td>2015-2016 春</td>
- <td>U33L11018</td>
- <td>U33L11018.01</td>
- <td><a href="/eams/teach/grade/course/person!info.action?courseGrade.id=36309376" target="_blank" title="查看成绩详情">唐诗选讲</a>
- </td>
- <td>综合素养</td> <td>2</td>
- <td style=""></td><td style=""></td><td style=""> 80
- </td><td style=""> 80
- </td><td style=""> 80
- </td><td> 0
- </td>
- </tr>
关于这个正则表达式我前后改了好多次,后来经过测试,要考虑到实验成绩,补考成绩,所以每个人成绩表格那一栏有可能是不一样的。还有一个要注意的问题就是正则表达式的写法,正则表达式写的鲁棒性越强有可能匹配到的信息中有误,越弱即越精确则正则表达式匹配的效率可能会大大降低。
这里关于正则表达式的写法见下边
4.最后一步就是将匹配到的数据进行整理,为后续动态的显示在网页中做准备,通过定义课程对象Course和学期对象Term最终返回学期对象列表term_list
整个爬虫代码如下所示:
- # -*- coding:utf-8 -*-
- import urllib
- import urllib2
- import cookielib
- import re
- import string
- import types
- class Term:
- time = ""
- id = ""
- courses_list = []
- def __init__(self, id, time, courses_list):
- self.id = id
- self.time = time
- self.courses_list = courses_list
- def __str__(self):
- return self.id+' '+self.time+' '+self.name+' '+self.courses_list
- class Course:
- id = ""
- time = ""
- name = ""
- weight = 0
- grade = 0
- def __init__(self, id, time, name, weight, grade):
- self.id = id
- self.time = time
- self.name = name
- self.weight = weight
- self.grade = grade
- def __str__(self):
- return self.id+' '+self.time+' '+self.name+' '+str(self.weight)+' '+str(self.grade)
- class NPU:
- def __init__(self,name,passwd):
- #登录URL
- self.loginUrl = 'http://us.nwpu.edu.cn/eams/login.action'
- #成绩URL
- self.gradeUrl = 'http://us.nwpu.edu.cn/eams/teach/grade/course/person!historyCourseGrade.action?projectType=MAJOR'
- self.cookies = cookielib.MozillaCookieJar('cookie.txt')
- self.postdata = urllib.urlencode({
- 'username':name,
- 'password':passwd,
- 'encodedPassword':'',
- 'session_locale':'zh_CN',
- })
- #成绩对象数组
- #构建opener
- self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookies))
- #获取本学期成绩页面
- def getPage(self):
- try:
- request = urllib2.Request(url = self.loginUrl,data = self.postdata)
- #建立连接,模拟登陆
- result = self.opener.open(request)
- self.cookies.save(ignore_discard = True, ignore_expires = True)
- #打印登录内容
- #print 'asdf'
- #print result.read()
- #获得成绩界面的html
- result = self.opener.open(self.gradeUrl)
- return result.read().decode('utf-8')
- except urllib2.URLError,e:
- print '连接失败'
- if hasattr(e,"reason"):
- print "error",e.reason
- return None
- def getGrades(self, page):
- #print page
- reg = 'not find#$$'
- tablelen11= '<tr>\s*?<th w.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?</tr>'
- tablelen12= '<tr>\s*?<th w.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?</tr>'
- tablelen13= '<tr>\s*?<th w.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?</tr>'
- tablelen14= '<tr>\s*?<th w.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?<th.*?</th>\s*?</tr>'
- if re.search(u'补考成绩', page) and re.search(u'实验成绩', page) and re.findall(tablelen14, page,re.S):
- print '14'
- reg = '<tr>\s*?<td>(.*?)</td>.*?<td.*?<td.*?<td><a href=".*?>(.*?)</a>.*?<t.*?'+'<td>(.*?)</td.*?<td.*?<td.*?<td.*?<td.*?<td.*?<td.*?<t.*?>\s*(\w*).*?<.*?<t.*?</tr>'
- elif (re.search(u'补考成绩', page) or re.search(u'实验成绩', page)) and re.search(tablelen13, page,re.S):
- print '13'
- reg = '<tr>\s*?<td>(.*?)</td>.*?<td.*?<td.*?<td><a href=".*?>(.*?)</a>.*?<t.*?'+'<td>(.*?)</td.*?<td.*?<td.*?<td.*?<td.*?<td.*?<t.*?>\s*(\w*).*?<.*?<t.*?</tr>'
- elif re.search(tablelen12, page,re.S):
- print '12'
- reg = '<tr>\s*?<td>(.*?)</td>.*?<td.*?<td.*?<td><a href=".*?>(.*?)</a>.*?<t.*?'+'<td>(.*?)</td.*?<td.*?<td.*?<td.*?<td.*?<t.*?>\s*(\w*).*?<.*?<t.*?</tr>'
- elif re.search(tablelen11, page,re.S):
- print '11'
- reg = '<tr>\s*?<td>(.*?)</td>.*?<td.*?<td.*?<td><a href=".*?>(.*?)</a>.*?<t.*?'+'<td>(.*?)</td.*?<td.*?<td.*?<td.*?<t.*?>\s*(\w*).*?<.*?<t.*?</tr>'
- # if re.findall(u'补考成绩', page):
- # print '含补考成绩'
- # reg = '<tr>\s*?<td>(.*?)</td>.*?<td.*?<td.*?<td><a href=".*?>(.*?)</a>.*?<t.*?'+'<td>(.*?)</td.*?<td.*?<td.*?<td.*?<td.*?<td.*?<td.*?<t.*?>\s*(\w*).*?<.*?<t.*?</tr>'
- # else:
- # reg = '<tr>\s*?<td>(.*?)</td>.*?<td.*?<td.*?<td><a href=".*?>(.*?)</a>.*?<t.*?'+'<td>(.*?)</td.*?<td.*?<td.*?<td.*?<td.*?<td.*?<t.*?>\s*(\w*).*?<.*?<t.*?</tr>'
- myItems = re.findall(reg, page, re.S)
- if myItems:
- print '查询成功'
- else:
- print '查询失败'
- grade_dict = {}
- terms = []
- term_list = []
- cnt = 1
- for item in myItems:
- print item[0],item[1],item[2],item[3]
- #print item[0].encode('utf-8'),item[1].encode('utf-8'),item[2].encode('utf-8'),item[3].encode('utf-8')
- #print type(item[0]), type(item[1]), type(item[2]), type(item[3])
- if re.match('^\d+\.?\d*$', item[2]) and re.match('^\d+\.?\d*$', item[3]):
- courseid = 'course_'+str(cnt)
- cnt = cnt+1
- if not grade_dict.has_key(item[0].encode('utf-8')):
- grade_dict[item[0].encode('utf-8')] = []
- terms.append(item[0].encode('utf-8'))
- grade_dict[item[0].encode('utf-8')].append(Course(courseid, item[0].encode('utf-8'),item[1].encode('utf-8'),string.atof(item[2]),string.atof(item[3])))
- termcnt = 1
- for k in terms:
- termid = 'term_'+str(termcnt)
- termcnt = termcnt + 1
- list = grade_dict[k]
- term_list.append(Term(termid, list[0].time, list))
- for i in list:
- print i
- return term_list
用django框架来搭建网站
官方网站:
https://www.djangoproject.com/
django中文文档:
https://github.com/7sDream/django-intro-zh
bootstrap前端框架:
其实界面一共两个,即登录界面和成绩界面,其中主界面实现的功能是实现对每个学期所有课程的全选和全不选,以及每门课程的全选和全部选,这里关于js的代码还是请教了一下Cherry和斌巨才完成的,自己写js的代码满是Bug,然后因为网页是用了bootstrap的前端框架,所以实现了适应屏幕的功能,在电脑和手机上都可以适应屏幕。
其中主要的难点就是如何向网站中动态添加数据,以及js代码中为那些Checkbox建立事件的实现,因为每个人的课程数目都是不易样的,这里采用了django里的模板标签,然后前边写爬虫费劲心思写的两个类就派上用场了。
模板代码如下:
- {% for term in term_list %}
- <div class="table-responsive">
- <table class="table table-striped table-bordered">
- <thead>
- <th>
- <input type="checkbox" name="{{term.id}}" id="{{term.id}}" checked="true" value="checkbox" onclick="termChange(this)"> {{term.time}}
- </th>
- <tr>
- <th>课程名称</th>
- <th>学分</th>
- <th>成绩</th>
- </tr>
- </thead>
- {% for course in term.courses_list %}
- <tr>
- <td class="atLeft"><input type="checkbox" name="{{term.id}}" checked="true" id="{{course.id}}" onclick="courseChange(this)"> {{course.name}}</td>
- <td>{{course.weight}}</td>
- <td>{{course.grade}}</td>
- </tr>
- {% endfor %}
- </table>
- </div>
- {% endfor %}
然后就可以把这整段代码部署在服务器上了,
测试网址:http://qmlangma.cn:8000/credit/login/
展示图片:
总结:
1、通过这个项目,我学到了从前端到后台的整个流程是如何运作的,自己之前所学的爬虫的知识也派上了用场,Python的强大功能也真是令人折服。
2、 关于字符串编码的问题,python2中是一个永恒的话题。。爬虫爬下来的html是utf-8编码,还是有点迷惑,用正则表达式进行匹配时要加一个’u’ re.search(u'补考成绩', page),调了好久才调出来,自己用正则表达式时还是要写很久,不断的尝试才能得到想要的结果,自己对正则表达式的理解还是不够深入。
3、 关于html的问题,js写的还是很烂,bug找不出来,还是请的别人找的。
4、 关于debug模式关闭的问题,可以在自己本地启用debug模式,但是别的机器调用的话还是没有解决。
5、 关于项目迁移的问题,我把本地写好的代码直接复制到云主机中,发现运行不了,也不 知道出了什么问题,目前在云主机上跑的还是debug模式,原来一直想部署到apache上,一直嫌麻烦没有去做。