Python爬取Coursera课程资源的详细过程


Posted in Python onNovember 04, 2014

有时候我们需要把一些经典的东西收藏起来,时时回味,而Coursera上的一些课程无疑就是经典之作。Coursera中的大部分完结课程都提供了完整的配套教学资源,包括ppt,视频以及字幕等,离线下来后会非常便于学习。很明显,我们不会去一个文件一个文件的下载,只有傻子才那么干,程序员都是聪明人!

那我们聪明人准备怎么办呢?当然是写一个脚本来批量下载了。首先我们需要分析一下手工下载的流程:登录自己的Coursera账户(有的课程需要我们登录并选课后才能看到相应的资源),在课程资源页面里,找到相应的文件链接,然后用喜欢的工具下载。

很简单是吧?我们可以用程序来模仿以上的步骤,这样就可以解放双手了。整个程序分为三个部分就可以了:

登录Coursera;在课程资源页面里面找到资源链接;根据资源链接选择合适的工具下载资源。

下面就来具体的实现以下吧!

登录

刚开始时自己并没有添加登录模块,以为访客就可以下载相应的课程资源,后来在测试comnetworks-002这门课程时发现访客访问资源页面时会自动跳转到登录界面,下图是chrome在隐身模式访问该课程资源页面时的情况。

Python爬取Coursera课程资源的详细过程

要想模拟登录,我们先找到登录的页面,然后利用google的Developer Tools分析账号密码是如何上传到服务器的。

我们在登录页面的表单中填入账号密码,然后点击登录。与此同时,我们需要双眼紧盯Developer Tools——Network,找到提交账号信息的url。一般情况下,如果要向服务器提交信息,一般都用post方法,这里我们只需要先找到Method为post的url。悲剧的是,每次登录账号时,Network里面都找不到提交账户信息的地址。猜测登录成功后,直接跳转到登录成功后的页面,想要找的内容一闪而过了。

于是就随便输入了一组账号密码,故意登录失败,果真找到了post的页面地址,如下图:

Python爬取Coursera课程资源的详细过程

地址为:https://accounts.coursera.org/api/v1/login。为了知道向服务器提交了哪些内容,进一步观察post页面中表单中内容,如下图:

Python爬取Coursera课程资源的详细过程

我们看到一共有三个字段:

email:账号的注册邮箱password:账号密码webrequest:附加的字段,值为true。

接下来就动手写吧,我选择用python的Requests库来模拟登录,关于Requests官网是这样介绍的。

Requests is an elegant and simple HTTP library for Python, built for human beings.

事实上requests用起来确实简单方便,不亏是专门为人类设计的http库。requests提供了Session对象,可以用来在不同的请求中传递一些相同的数据,比如在每次请求中都携带cookie。

初步的代码如下:

signin_url = "https://accounts.coursera.org/api/v1/login"
logininfo = {"email": "...",
             "password": "...",
             "webrequest": "true"
             }
user_agent = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) "
              "AppleWebKit/537.36 (KHTML, like Gecko) "
              "Chrome/36.0.1985.143 Safari/537.36")
post_headers = {"User-Agent": user_agent,
                "Referer": "https://accounts.coursera.org/signin"
                }
coursera_session = requests.Session()
login_res = coursera_session.post(signin_url,
                                  data=logininfo,
                                  headers=post_headers,
                                  )
if login_res.status_code == 200:
    print "Login Successfully!"
else:
    print login_res.text

将表单中提交的内容存放在字典中,然后作为data参数传递给Session.post函数。一般情况下,最好是加上请求User-AgentReferer等请求头部,User-Agent用来模拟浏览器请求,Referer用来告诉服务器我是从referer页面跳转到请求页面的,有时候服务器会检查请求的Referer字段来保证是从固定地址跳到当前请求页的。

上面片段的运行结果很奇怪,显示如下信息:Invalid CSRF Token。后来在github上面搜索到一个Coursera的批量下载脚本,发现人家发送页面请求时headers多了XCSRF2Cookie, XCSRF2Token, XCSRFToken, cookie4个字段。于是又重新看了一下post页面的请求头部,发现确实有这几个字段,估计是服务器端用来做一些限制的。

用浏览器登录了几次,发现XCSRF2Token, XCSRFToken是长度为24的随机字符串,XCSRF2Cookie为"csrf2_token_"加上长度为8的随机字符串。不过一直没搞明白Cookie是怎么求出来的,不过看github上面代码,Cookie似乎只是"csrftoken"和其他三个的组合,试了一下竟然可以。

在原来的代码上添加以下部分就足够了。

def randomString(length):
    return ''.join(random.choice(string.letters + string.digits) for i in xrange(length))
XCSRF2Cookie = 'csrf2_token_%s' % ''.join(randomString(8))
XCSRF2Token = ''.join(randomString(24))
XCSRFToken = ''.join(randomString(24))
cookie = "csrftoken=%s; %s=%s" % (XCSRFToken, XCSRF2Cookie, XCSRF2Token)
post_headers = {"User-Agent": user_agent,
                "Referer": "https://accounts.coursera.org/signin",
                "X-Requested-With": "XMLHttpRequest",
                "X-CSRF2-Cookie": XCSRF2Cookie,
                "X-CSRF2-Token": XCSRF2Token,
                "X-CSRFToken": XCSRFToken,
                "Cookie": cookie
                }

至此登录功能初步实现。

分析资源链接

登录成功后,我们只需要get到资源页面的内容,然后过滤出自己需要的资源链接就行了。资源页面的地址很简单,为https://class.coursera.org/name/lecture,其中name为课程名称。比如对于课程comnetworks-002,资源页面地址为https://class.coursera.org/comnetworks-002/lecture。

抓取到页面资源后,我们需要分析html文件,这里选择使用BeautifulSoup。BeautifulSoup是一个可以从HTML或XML文件中提取数据的Python库,相当强大。具体使用官网上有很详细的文档,这里不再赘述。在使用BeautifulSoup前,我们还得找出资源链接的规律,方便我们过滤。

其中课程每周的总题目为class=course-item-list-header的div标签下,每周的课程均在class=course-item-list-section-list的ul标签下,每节课程在一个li标签中,课程资源则在li标签中的div标签中。

查看了几门课程之后,发现过滤资源链接的方法很简单,如下:

ppt和ppt资源:用正则表达式匹配链接;字幕资源:找到title="Subtitles (srt)"的标签,取其href属性;视频资源:找到title="Video (MP4)"的标签,取其href属性即可。

字幕和视频也可以用正则表达式过滤,不过用BeautifulSoup根据title属性来匹配,有更好的易读性。而ppt和pdf资源,没有固定的title属性,只好利用正则表达式来匹配。

具体代码如下:

soup = BeautifulSoup(content)
chapter_list = soup.find_all("div", class_="course-item-list-header")
lecture_resource_list = soup.find_all("ul", class_="course-item-list-section-list")
ppt_pattern = re.compile(r'https://[^"]*\.ppt[x]?')
pdf_pattern = re.compile(r'https://[^"]*\.pdf')
for lecture_item, chapter_item in zip(lecture_resource_list, chapter_list):
    # weekly title
    chapter = chapter_item.h3.text.lstrip()
    for lecture in lecture_item:
        lecture_name = lecture.a.string.lstrip()
        # get resource link
        ppt_tag = lecture.find(href=ppt_pattern)
        pdf_tag = lecture.find(href=pdf_pattern)
        srt_tag = lecture.find(title="Subtitles (srt)")
        mp4_tag = lecture.find(title="Video (MP4)")
        print ppt_tag["href"], pdf_tag["href"]
        print srt_tag["href"], mp4_tag["href"]

下载资源

既然已经得到了资源链接,下载部分就很容易了,这里我选择使用curl来下载。具体思路很简单,就是输出curl resource_link -o file_name到一个种子文件中去,比如到feed.sh中。这样只需要给种子文件执行权限,然后运行种子文件即可。

为了便于归类课程资源,可以为课程每周的标题建立一个文件夹,之后该周的所有课程均下载在该目录下。为了方便我们快速定位到每节课的所有资源,可以把一节课的所有资源文件均命名为课名.文件类型。具体的实现比较简单,这里不再给出具体程序了。可以看一下一个测试例子中的feed.sh文件,部分内容如下:

mkdir 'Week 1: Introduction, Protocols, and Layering'
cd 'Week 1: Introduction, Protocols, and Layering'
curl https://d396qusza40orc.cloudfront.net/comnetworks/lect/1-readings.pdf -o '1-1 Goals and Motivation (15:46).pdf'
curl https://class.coursera.org/comnetworks-002/lecture/subtitles?q=25_en&format=srt -o '1-1 Goals and Motivation (15:46).srt'
curl https://class.coursera.org/comnetworks-002/lecture/download.mp4?lecture_id=25 -o '1-1 Goals and Motivation (15:46).mp4'
curl https://d396qusza40orc.cloudfront.net/comnetworks/lect/1-readings.pdf -o '1-2 Uses of Networks (17:12).pdf'
curl https://class.coursera.org/comnetworks-002/lecture/subtitles?q=11_en&format=srt -o '1-2 Uses of Networks (17:12).srt'
curl https://class.coursera.org/comnetworks-002/lecture/download.mp4?lecture_id=11 -o '1-2 Uses of Networks (17:12).mp4'

到这里为止,我们已经成功完成爬取Coursera课程资源的目标,具体的代码放在gist上。使用时,我们只需要运行程序,并把课程名称作为参数传递给程序就可以了(这里的课程名称并不是整个课程的完整名字,而是在课程介绍页面地址中的缩略名字,比如Computer Networks这门课,课程名称是comnetworks-002)。

其实,这个程序可以看做一个简单的小爬虫程序了,下面粗略介绍下爬虫的概念。

一点都不简单的爬虫

关于什么是爬虫,wiki上是这样说的

A Web crawler is an Internet bot that systematically browses the World Wide Web, typically for the purpose of Web indexing.

爬虫的总体架构图如下(图片来自wiki):

Python爬取Coursera课程资源的详细过程

简单来说,爬虫从Scheduler中获取初始的urls,下载相应的页面,存储有用的数据,同时分析该页面中的链接,如果已经访问就pass,没访问的话加入到Scheduler中等待抓取页面。

当然有一些协议来约束爬虫的行为规范,比如许多网站都有一个robots.txt文件来规定网站哪些内容可以被爬取,哪些不可以。

每个搜索引擎背后都有一个强大的爬虫程序,把触角伸到网络中的所有角落,不断去收集有用信息,并建立索引。这种搜索引擎级别的爬虫实现起来非常复杂,因为网络上的页面数量太过庞大,只是遍历他们就已经很困难了,更不要说去分析页面信息,并建立索引了。

实际应用中,我们只需要爬取特定站点,抓取少量的资源,这样实现起来简单很多。不过仍然有许多让人头疼的问题,比如许多页面元素是javascript生成的,这时候我们需要一个javascript引擎,渲染出整个页面,再加以过滤。

更糟糕的是,许多站点都会用一些措施来阻止爬虫爬取资源,比如限定同一IP一段时间的访问次数,或者是限制两次操作的时间间隔,加入验证码等等。绝大多数情况下,我们不知道服务器端是如何防止爬虫的,所以要想让爬虫工作起来确实挺难的。

Python 相关文章推荐
Python中的ConfigParser模块使用详解
May 04 Python
在Python中移动目录结构的方法
Jan 31 Python
python日志记录模块实例及改进
Feb 12 Python
简单了解Django模板的使用
Dec 20 Python
python使用多进程的实例详解
Sep 19 Python
Python3 操作 MySQL 插入一条数据并返回主键 id的实例
Mar 02 Python
在python下实现word2vec词向量训练与加载实例
Jun 09 Python
Python爬虫爬取有道实现翻译功能
Nov 27 Python
Python基于Socket实现简易多人聊天室的示例代码
Nov 29 Python
详解Pycharm第三方库的安装及使用方法
Dec 29 Python
Python尝试实现蒙特卡罗模拟期权定价
Apr 21 Python
使用python开发vim插件及心得分享
Nov 04 #Python
Python学习笔记之os模块使用总结
Nov 03 #Python
Python中获取网页状态码的两个方法
Nov 03 #Python
Python random模块常用方法
Nov 03 #Python
Python求两个list的差集、交集与并集的方法
Nov 01 #Python
python多线程threading.Lock锁用法实例
Nov 01 #Python
python分割文件的常用方法
Nov 01 #Python
You might like
Mac系统完美安装PHP7详细教程
2017/06/06 PHP
php获取'/'传参的值简单方法
2017/07/13 PHP
javascript模拟地球旋转效果代码实例
2013/12/02 Javascript
100个不能错过的实用JS自定义函数
2014/03/05 Javascript
PHP使用方法重载实现动态创建属性的get和set方法
2014/11/17 Javascript
AngularJS HTML编译器介绍
2014/12/06 Javascript
使用jQuery管理选择结果
2015/01/20 Javascript
jQuery实现两款有动画功能的导航菜单代码
2015/09/16 Javascript
Jquery easyui开启行编辑模式增删改操作
2016/01/14 Javascript
第一次接触神奇的Bootstrap
2016/10/14 Javascript
利用React-router+Webpack快速构建react程序
2016/10/27 Javascript
Easyui的组合框的取值与赋值
2016/10/28 Javascript
JavaScript trim 实现去除字符串首尾指定字符的简单方法
2016/12/27 Javascript
JS闭包可被利用的常见场景小结
2017/04/09 Javascript
Vue v2.5 调整和更新不完全问题
2017/10/24 Javascript
基于JavaScript实现简单的音频播放功能
2018/01/07 Javascript
vue+iview 兼容IE11浏览器的实现方法
2019/01/07 Javascript
Nodejs核心模块之net和http的使用详解
2019/04/02 NodeJs
uni app仿微信顶部导航条功能
2019/09/17 Javascript
vue集成openlayers加载geojson并实现点击弹窗教程
2020/09/24 Javascript
使用Python的Zato发送AMQP消息的教程
2015/04/16 Python
Python自动化测试Eclipse+Pydev 搭建开发环境
2016/08/15 Python
python日志记录模块实例及改进
2017/02/12 Python
python素数筛选法浅析
2018/03/19 Python
python 画三维图像 曲面图和散点图的示例
2018/12/29 Python
Python时间和字符串转换操作实例分析
2019/03/16 Python
Python3.5 Pandas模块之DataFrame用法实例分析
2019/04/23 Python
Python如何把多个PDF文件合并代码实例
2020/02/13 Python
Python脚本去除文件的只读性操作
2020/03/05 Python
Python爬虫实现HTTP网络请求多种实现方式
2020/06/19 Python
信息技术专业大学生职业生涯规划书
2014/01/24 职场文书
省级四好少年事迹材料
2014/01/25 职场文书
关于建议书的格式范文
2014/05/20 职场文书
求职简历自我评价2015
2015/03/10 职场文书
如何写通讯稿
2015/07/22 职场文书
js前端面试常见浏览器缓存强缓存及协商缓存实例
2022/06/21 Javascript