[技术博客] 课程中心爬虫教程
CHAP 1 基础知识
1.1 Session & Cookie
- 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session。
典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几件物品。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件、集群等。 - 服务端如何识别特定的客户?第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,就可以依据此来识别不同客户端了。
如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
总结:
- Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
- Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
1.2 Single Sign On
单点登录英文全称Single Sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。
如图所示,图中有4个系统,分别是Application1、Application2、Application3、和SSO。Application1、Application2、Application3没有登录模块,而SSO只有登录模块,没有其他的业务模块,当Application1、Application2、Application3需要登录时,将跳到SSO系统,SSO系统完成登录,其他的应用系统也就随之登录了。这完全符合我们对单点登录(SSO)的定义。
上图是CAS官网上的标准流程,具体流程如下:
- 用户访问app系统,app系统是需要登录的,但用户现在没有登录。
- 跳转到CAS server,即SSO登录系统,以后图中的CAS Server我们统一叫做SSO系统。 SSO系统也没有登录,弹出用户登录页。
- 用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSO的session,浏览器(Browser)中写入SSO域下的Cookie。
- SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app系统,同时将ST作为参数传递给app系统。
- app系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。
- 验证通过后,app系统将登录状态写入session并设置app域下的Cookie。
至此,跨域单点登录就完成了。以后我们再访问app系统时,app就是登录的。接下来,我们再看看访问app2系统时的流程。
- 用户访问app2系统,app2系统没有登录,跳转到SSO。
- 由于SSO已经登录了,不需要重新登录认证。
- SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
- app2拿到ST,后台访问SSO,验证ST是否有效。
- 验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。
这样,app2系统不需要走登录流程,就已经是登录了。SSO,app和app2在不同的域,它们之间的session不共享也是没问题的。
CHAP 2 实际应用
2.1 Session & Cookie 的Python实现
实现Python爬虫的方式有二:
-
selenium
通过利用 chromedriver 等自动化工具实现模拟浏览器的操作,官网上最大的标语:
优点:实现简单,调试简单,和用户正常使用浏览器的操作一样
缺点:内存资源消耗大,需要安装自动化驱动的支持
-
requests
模拟浏览器的请求,可以携带Cookie以保持Keep-alive的连接,官网的介绍:
Requests allows you to send HTTP/1.1 requests extremely easily. There’s no need to manually add query strings to your URLs, or to form-encode your POST data. Keep-alive and HTTP connection pooling are 100% automatic, thanks to urllib3.
优点:内存占用小,访问速度快
缺点:调试困难,不能对无url的按钮(如: javascript(0))进行模拟点击,需要手动模拟请求
考虑到我们的用户量规模在150人以上,而且每个人在登陆的时候都会进行课程同步,服务器是2C4G的刚好够用配置,所以我们使用的技术是Python的Requests模块。
2.2 SSO 破解登陆难题
在具体实现的时候,刚开始是想对 课程中心网站 直接进行登陆,但是直接访问登陆会跳转到 SSO 登陆界面,输入用户名密码之后再跳转回课程中心,中间跳转的过程太多,尝试实现未果。
在一次尝试使用财务系统的时候,从网页VPN进入,看到第三个应用就是课程中心,意识到通过网页VPN直接登陆可能是使用了单点登录的技术,所以尝试先成功登陆网页VPN,然后通过Session携带的信息再访问课程中心网站,果然,成功了。
具体实现
s = requests.session() # 创建一个Session,下面使用该Session进行网页请求
查看Session中保存的Cookie:
和网站上直接登陆保存的Cookie进行比对:
可以看到Session可以保持所有的浏览器中的信息,所以可以直接登陆到课程中心了。
CHAP 3 困难和坑
在上面的Selenium和Requests对比中,最明显的一点就是:
Selenium 可以直接模拟鼠标的点击
Requests 需要通过模拟 HTTP/1.1 的模拟实现
然而课程中心中的大部分都是没有直达Url的,大多用iframe框架加上POST查询实现页面的刷新和跳转,这给爬虫带来很大的难题。
下面讲一下遇到的困难和坑和解决方法:
3.1 当前站点url被隐藏
虽然在一般情况下点进课程的第一个是 主页 ,但是存在某些情况下,某些老师的设置是只有资源和作业,所以点进第一个就是资源,这时候直接获取href是会NoneType错误,这时候直接保存当前课程站点的Url即可。
3.2 课程站点内部显示使用iframe框架
内容存在于iframe框架内时,无法获取框架内的Url,而selenium可以通过切换如iframe内模拟点击,requests则需要进入iframe框架单独的页面。所以先获取src内的网址,重新GET请求。
3.3 资源界面内文件夹无Url链接
对于资源界面中的文件夹,可以发现点击并不会跳转网页,网页链接不变,通过JavaScript操作获取到文件夹内的资源并渲染显示,并且我们大概可以看到JavaScript执行的参数。这说明网页在用户看不见的地方进行了加载。
为了看到这种加载,我们开启Chrome的F12调试者模式,在Network下,开启Preserve Log,筛选All,保留网站内跳转产生的日志,再点击文件夹,可以看到保存了几个Log:
可以发现其中有一个status code为302的状态码,对应的是:
302 Found:临时重定向。
这个状态码是指客户端要请求的资源临时放到一个新地方了。同样,应答中也包含了资源的新地址。
查看该记录中的详细信息:
和上面审查元素中的onclick中的元素有强大的相关性,通过尝试可以发现点击文件夹深入的操作的collectionId是组成为: /group/该课程下的一串字符/文件夹名称
,sakai_action为 doNavigate
,sakai_csrf_token应该是csrf相关的Token,通常藏匿于网站的源码内,作为有效请求的标识符。
查看网页源代码,搜索课程有关的字符串,可以从几个地方发现:
- 网页头
- 课程站点头
直接通过第一个位置进行获取:
def getCollectionIdPrefix(rr):
# rr = beautifulSoup(session.get(assignment_iframe_url, cookies=cookie).text, 'html.parser')
for script in rr.findAll(attrs={'type':"text/javascript"}):
# print(str(script))
if 'collectionId' in str(script):
content = str(script)
break
# print(content)
pattern = re.compile('collectionId = (\S+);')
prefix = pattern.findall(content)[0][1:-1]
return prefix
然后搜索sakai_csrf_token:
只找到一条记录,存在于课程body的尾部,可以通过唯一的name进行获取:
def getCsrfToken(rr):
# rr = beautifulSoup(session.get(assignment_iframe_url, cookies=cookie).text, 'html.parser')
return rr.find('input',{'name':'sakai_csrf_token'}).attrs['value']
找到这几个就可以进行POST请求的伪造了:
def enterFolder(collectionId, ass_iframe_url, s, cookie, token):
# enter is bidirectional, both go deepin and return
## mock navigate data
data = {
'source': 0,
'collectionId': collectionId,
'navRoot': collectionId,
'criteria': 'title',
'sakai_action': 'doNavigate',
'sakai_csrf_token': token
}
## go to next folder
s.post(ass_iframe_url, cookies=cookie, data=data)
return
在进行POST请求后,当前Session中的Cookie已经改变,但是还没有获得资源,细看Preserve Log可以发现在POST后面会跟上一个非常相似的GET,里面没有携带任何内容,依旧请求资源站点的Url就可以了。
至于返回上一级文件夹,可以点击上面的面包屑导航,一样的方式查看Preserve Log伪造POST请求:
可以发现多了一个navRoot字段,与collectionId相同,其余一样,先POST再GET。
3.4 作业链接提交前后不一致
-
对于已经提交的作业,其Url构成为:
href="https://course.e2.buaa.edu.cn/portal/tool/a5695950-ebed-96d8-78e8b58ab8?submissionId=/assignment/s/0d40488a-ec2c-aef5-87b531b4/d600add7-1b92-a428-7057935e3fe7/243fafd9-7ae9-9836-279f9502&panel=Main&sakai_action=doView_grade" 作业进入的链接 + '?submissionId=/assigment/s/' + ... + '&panel=Main&sakai_action=doView_grade'.
-
对于未提交的作业,其Url构成为:
href="https://course.e2.buaa.edu.cn/portal/tool/a5695950-4b82-96d8-78e8b8ab8?assignmentReference=/assignment/a/0d404c2c-4650-aef5-87b431b4/2b33659dc-4769-99b7-5d0a8269&panel=Main&sakai_action=doView_submission" 作业进入的链接 + '?assignmentReference=/assignment/a/' + ... + '&panel=Main&sakai_action=doView_submission'.
由于后端的存储中,对作业的唯一性的判定采用的是Url判断,所以会出现提交作业前拉去课程中心和提交作业后拉取课程中心造成一份作业出现两次的Bug。
这时只能通过判断其作业进入链接是否相同来判断是否一致:
homework_url.split('?')[0]
3.5 通知中心通知显示不全
需要先选择显示200项然后再GET所有即可:
token = getCsrfToken(note_ss)
selectData = {
'eventSubmit_doChange_pagesize': 'changepagesize',
'selectPageSize': '200',
'sakai_csrf_token': token
}
s.post(note_url, cookies=cookie, data=selectData)
3.6 通知详情内容无结构
需要使用next_siblings进行遍历,需要注意的是next_siblings是属性而不是方法。
Reference
- Zhihu | COOKIE和SESSION有什么区别?
- JianShu | 单点登录 (SSO) 看这一篇就够了
- 免费的分布式的自动化测试工具
- [Requests: HTTP for Humans™](