抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

现在大学上课,老师一般都是通过学校的课程网站来分享课程的课件等资源。但是,在果壳大课程网站上下载资源,总是需要一项一项的点入,再单击下载,显得十分麻烦,特别有时囤积了大量资源需要去下载,还得比对一下哪些没有下载,这对于我这种数据强迫症的人来说,十分不友好!
恰巧那会对爬虫挺感兴趣的,就寻思着拿这个练练手(重在学习),说做就做吧!Let’s go!

前期准备

首先,得先定我的需求:

  1. 可以选择课程,对该课程的所有课件实现一键下载;
  2. 鉴于我的数据强迫症,课件下载完毕后,应该能向我反馈下载的信息,主要就是新下载了哪些课件;
  3. 由于疫情的特殊原因,学校采取了网上授课的方式,但是家里网络不稳定,总是故障,考虑下载视频到本地观看,也便于课后复习;(想想以前,为了能课后复习,都是拿着电脑在上课的时候现场录的);

现在,需求以及清楚,接下来就是开始捣鼓课程网站的情况;

  • 正常情况下,我们首先需求登录,进入教务系统主页;
  • 然后,进入课程网站主页;
  • 然后,在自己的选课情况中,选择课程,进入到课程主页;
  • 然后,找到进入该课程主页的资源页面;
  • 进入相关资源,并点击下载;

Emmm,用的时候都还好,这么一捋愈发觉得麻烦了……

弯路:webdriver

果壳大的教育业务平台网址是:http://sep.ucas.ac.cn/ ,要想进行后面的操作,首先,就得实现教育业务平台的自动登录;

起初嘛,刚接触爬虫,对网络也不是特别了解。脑海里冒出来最简单的思路就是:利用 selenium 的 webdriver 模拟登录过程,然后获取 cookies,之后再利用 cookies 登录;

  1. pip install selenium 安装 selenium;
    • selenium 是 ThoughtWorks 提供的一个强大的基于浏览器的开源自动化测试工具。支持的浏览器包括 IE、Chrome 和 Firefox 等;
  2. 到相应的官网下载浏览器驱动,我这里下载的是火狐的浏览器驱动;

接下来,写段程序测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import requests
import time
from selenium import webdriver

'''
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36'
}
'''

def log_in( ):
user = input("请输入用户名:")
password = input("请输入密码:")
driver = webdriver.Firefox()
driver.get('http://sep.ucas.ac.cn/')

#time.sleep(3)
# 清空登录框
driver.find_element_by_xpath("./*//input[@id='menhuusername']").clear()
# 自动填入登录用户名
driver.find_element_by_xpath("./*//input[@id='menhuusername']").send_keys(user)
# 清空密码框
driver.find_element_by_xpath("./*//input[@id='menhupassword']").clear()
# 自动填入登录密码
driver.find_element_by_xpath("./*//input[@id='menhupassword']").send_keys(password)

time.sleep(3)
# 点击登录按钮进行登录
driver.find_element_by_class_name('loginbtn').click()

# 获取cookies
cookie_items = driver.get_cookies()
cookie = [item["name"] + "=" + item["value"] for item in cookie_items]
cookie_str = ';'.join(item for item in cookie)
with open('cookie.txt', 'w', encoding='utf-8') as f:
f.write(cookie_str)
f.close()
print("已获取到cookies!")

headers_cookie = {
"Cookie": cookie_str # 通过接口请求时需要cookies等信息
}

session = requests.session()
session.post('http://sep.ucas.ac.cn/', headers=headers_cookie)
print('登录系统成功……')
return session

if __name__ == '__main__':
session = log_in()

本以为,万事大吉,结果运行测试,emmmmm……才意识到,还得输入验证码!

那就继续造:

  • 要用到图形处理,所以 pip install pillow
    • 坑:安装 pillow,但是导入的时候是 PIL;
  • 抓取下来验证码,还不够,肯定还得识别验证码内容,选择百度文字识别的 OCR,pip install baidu_api

pillow 的原身是 PIL(Python Imaging Library),PIL 是 Python 图像处理标准库,功能非常强大,API 却非常简单易用;
但是 PIL 仅支持到 Python 2.7,后来由志愿者在此基础上创建了兼容的版本,即:Pillow,支持最新Python 3.x,又加入了许多新特性
百度文字识别的OCR,即:Optical Character Recognition,光学字符识别

然后,在上面代码的基础上,新增如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
### 导入下载的第三方库
from PIL import Image
from aip import AipOcr


### 查找验证码
png = driver.find_element_by_id('captcha_img') # 查找验证码元素
png.screenshot('captcha.png') # 对验证码截图并保存


### 验证码处理
# 用 pillow 库对验证码进行图像处理,提高验证码的识别率;
# 处理方法:
# 1.先将图像转换成灰度模式
# 2.通过对阈值的调整使得多余的噪点消失
img = Image.open('captcha.png')
img = img.convert('L') # P模式转换为L模式(灰度模式默认阈值127)
count = 165 # 设定阈值
table = []
for i in range(256):
if i < count:
table.append(0)
else:
table.append(1)
img = img.point(table, '1')
img.save('captcha1.png') # 保存处理后的验证码


### 验证码识别
# 调用 baidu_api 的通用文字识别接口

# 识别码
APP_ID = '***'
API_KEY = '***'
SECRET_KEY = '***'

# 初始化对象
client = AipOcr(APP_ID, API_KEY, SECRET_KEY)

# 读取图片
def get_file_content(file_path):
with open(file_path, 'rb') as f:
return f.read()
image = get_file_content('captcha.png')

# 定义参数变量
options = {'language_type': 'ENG', } # 识别语言类型,默认为'CHN_ENG'中英文混合

# 调用通用文字识别
result = client.basicGeneral(image, options) # 高精度接口 basicAccurate
for word in result['words_result']:
captcha = (word['words'])

# 输出,检查结果
print('识别结果:' + captcha)

# 清空验证码框
driver.find_element_by_xpath("./*//input[@id='menhucaptcha']").clear()
# 自动填入验证码
driver.find_element_by_xpath("./*//input[@id='menhucaptcha']").send_keys(captcha)

至此,总算是完成了登录过程,麻不麻烦?
肯定麻烦啊!而且有个很大的问题,就是每次运行会启动 webdriver,把程序拖得很慢,十分影响使用体验,所以我后来才改用了其他方法;

但是也不得不说,这段弯路也让我学到了挺多东西,还是很有意义的!

正文

其实说正解不太准确,只是说这个办法更加简单易行罢了;

之所以会突然又提出新的办法,是因为有一次,我发现果壳大的综合信息网( http://onestop.ucas.ac.cn/ )也可以登录到教育业务平台,而且在这里登录不需要验证码!
这下子,终于可以去掉上面那繁琐的验证码处理过程了。

但是,这只解决了一个问题,还是无法让我摆脱 webdriver。于是,我寻思这么难顶的资源下载方式,难道就没有前人 “种个树”?再仔细搜了搜,还真有!

原作者的程序是一键下载课程网站所有课程所有课件,呃……,对我来说有点夸张了,毕竟几十门课程,怎么得也有个几百项资源吧?也许对于一个爬虫来说爬取这些资源不算什么,但是,还有好多资源我可能不那么需要,事后还得整理。不过无妨,程序框架在这了,修改起来也简单。

确定修改目标:

  1. 能够输出选课的课程目录,供按课程批量下载课件;
  2. 加入视频下载功能;

开干!!!

网站登录

首先,是登录信息,这里采用了直接将登录信息保存在 txt 文本文件里,避免了我原先那样每次运行脚本都需要手动输入的尴尬。简单的文本处理:

1
2
3
4
5
6
7
8
try:
#读取登录信息,第一行存账号,第二行存密码
config = open("user.txt", encoding='utf-8')
line = config.readline().split()
username = line[0]
password = line[1]
except IOError as e:
print(e)

重点来了,这次登录改用了 requests 库的 session 会话对象,构造 post 表单的方式实现登录,并且由于 session 对象的特性,也便于我们后续其他页面的操作;

session 的特性体现在它的作用时间:从用户到达某个特定的 Web 页开始,到该用户离开 Web 站点,或在程序中利用代码终止某个 Session 结束。
引用 Session 则可以让一个用户访问多个页面,之间的切换也会保留该用户的信息;
说白了,就是一旦我们使用 session 成功的登录了某个网站后,则在再次使用该 session对象求求该网站的其他网页都会默认使用该 session 之前使用的 cookie 等参数;
详细用法参见文章:Python Requests库:HTTP for Humans

  1. 构造请求头:
    • 打开网页( http://onestop.ucas.ac.cn/ ),然后进入开发者模式;
      findheader
    • Accept:用户代理期望的 MIME 类型列表,不用管;
    • Accept-Encoding:用户代理支持的压缩方法,不用管;
    • Accept-language:用户代理期望的页面语言,不用管;
    • Connection:决定当前的事务完成后,是否会关闭网络连接。如果该值是“keep-alive”,网络连接就是持久的,不会关闭,使得对同一个服务器的请求可以继续在该连接上完成。因此,需要设置;
    • Cookie:就不用说了,我们目标就是自动获取登录后的 cookie;
    • host:指明服务器域名,需要设置;
    • upgrade-insecure-requests:用来向服务器端发送信号,表示客户端优先选择加密及带有身份验证的响应,并且它可以成功处理 upgrade-insecure-requests CSP 指令。
    • User-Agent;指明用户代理软件的应用类型、操作系统、软件开发商以及版本号;
    • 更多详情可见:『MDN web docs
  2. 构造 post 表单:
    • 打开网页( http://onestop.ucas.ac.cn/ ),然后进入开发者模式(未登录状态);
      buildpost1
      • Preserve log:保留 log 信息;
      • XHR:(XMLHttpRequest)筛选出与服务器的交互信息;
    • 然后,开发者模式设置完成后,在浏览器输入信息登录(不要关闭开发窗口);
      buildpost2
    • 很明显,我所需要的信息应该在 Name = 0,的那条记录里,打开这条记录;
      buildpost3
    • 在 Form Data 里就有我们构造 post 表单所需要去构造的信息,为:用户名、密码、是否记住密码,这三个字段;

这一部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
session = requests.session()  # 创建 session 对象
login_url = 'http://onestop.ucas.ac.cn/' # 更换为不需要验证码登录的地址

### 构造请求头
headers= {
'Host': 'onestop.ucas.ac.cn',
"Connection": "keep-alive",
'Referer': 'http://onestop.ucas.ac.cn/home/index',
'X-Requested-With': 'XMLHttpRequest', # 指明 Ajax 请求(异步),注意,这样返回的数据是 json 类型
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
}
### 构造表单数据
post_data = {
"username": username,
"password": password,
"remember": 'checked',
}
html = session.post(login_url, data=post_data, headers=headers).text # 请求,并建立 session
# 将返回的 json 数据转换为 html 文本保存
res = json.loads(html)
html = session.get(res['msg']).text
print('登录系统成功!')
# save_html(html) # 用来保存 html 文本做检测

这样,我们就有了一个建立了连接的 session,以后就可以利用该 session 完成其他页面的操作;

进入课程网站

首先,查找进入课程网站的 url:
tocoursesite1

利用之前的 session 访问:h_k = session.get(url)

这里有个注意点,在我们直接点击课程网站图标时,会进入一个跳转页面,而我们刚刚 session 访问到的就是这个跳转页面,所以实际上我们还并没有进入到课程网站页面中去;

为了便于分析,将 session.get() 到的对象转换成文本文件存储:

1
2
3
4
5
6
7
8
9
10
### 存储函数
def save_html(html):
f = open('test.html','w',encoding='utf-8')
f.write(html)
f.close

### 调用
url = "http://sep.ucas.ac.cn/portal/site/16/801" # 跳转页面地址
h_k = session.get(url) # 访问跳转,并获取返回的对象
save_html(h_k.text) # 转换为文本文件保存下来

打开 h_k.text,我们知道跳转页面里,提示信息会有 “点击这里跳转” 这种选项,在这个文本里 ctrl F,输入:“跳转”,就可以看到,确实存在一个标签,如下:
(当然了,也可以在跳转的时候,强制停止刷新网页,然后在跳转页面用开发者模式查找 “这里” 这个字段的 href)

1
2
3
4
5
6
7
8
9
<div class="row-fluid">

<div class="span12" style="text-align:center;">

<h4>2秒钟没有响应请点击<a href="https://course.ucas.ac.cn/portal/plogin?Identity=fbd361f2-73cc-48b7-a5ec-37528b27a058&roleId=801"><strong>这里</strong></a>直接跳转</h4>

</div>

</div><!--//container-fluid:end-->

接下来,利用正则表达式,获取这个 url 里,Identity 的值(身份认证信息),重新构造 url,直接进入到课程网站页面:

1
2
3
4
5
6
7
8
# 利用正则表达式找Request URL,Identity后的身份认证信息
key = re.findall(r'"https://course.ucas.ac.cn/portal/plogin\?Identity=(.*)"', h_k.text)[0]

### 利用得到的身份认证信息,打开课程网站系统
url = "http://course.ucas.ac.cn/portal/plogin/main/index?Identity=" + key
page = session.get(url)
print('课程网站系统进入成功!')
return page

获取课程信息

先进入主页:

1
2
3
4
5
# 利用 BeautifulSoup 的 find_all 方法,找到课程网站的主页地址,并进入主页
mycourseBS = BeautifulSoup(courseSite.text,"lxml") # 利用 lxml 解析 text 文本
url_mycourse = mycourseBS.find_all('a',{"class":'Mrphs-toolsNav__menuitem--link'})[0] # 找 class 名为 xxx 的 a 标签
url_mycourse = url_mycourse["href"] # 获取 href ,即获取 url
coursePage = session.get(url_mycourse) # 访问进入主页

在我的课程里,获取课程信息:
courseinfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 利用 BeautifulSoup 的 find_all 方法,在课程网站主页,寻找课程信息,并利用元组的形式记录在course_list中
coursePageBS = BeautifulSoup(coursePage.text,"lxml")
Course_info = coursePageBS.find_all('li',{"class":"fav-sites-entry"})
length = len(Course_info) # 标签数,即:课程总数
print("*****************************************************************")
print("所选课程总数为:",length)
print(("已选课程列表:"))
for i in range(0,length-1):
info = Course_info[i]
tag = info.div.a
courseName = tag["title"] #课程名字
print(" ",i,courseName)
courseUrl = tag["href"] #课程链接
course_list.append((courseName,courseUrl)) #利用元组的形式保存
print("*****************************************************************")
return course_list

课件下载

后面的页面跳转等处理,其实都类似,这里只介绍一些关键点,毕竟主要的目的在于学习:

进入课程资源页面,这里直接将关键的 BeautifulSoup 查找语句列出:

1
2
### 访问进入某课程后,在课程页面里,利用 “资源” 模块的 title 查找,拿到其 href,即 url
url = h_bs.find_all(title="资源 - 上传、下载课件,发布文档,网址等信息")[0].get("href")

查找所有资源链接:

  • 这里文件夹的处理,涉及到 onclick(),展开文件夹,更新 html 页面,但是这里我没态弄太明白,后面再琢磨。可以的话,可以在评论区给我留言一些相关知识讲解文章;

下载文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
### className:文件夹名
def download_kj(url, fileName, className, session):
### 文件名称处理
# \xa0(不间断空白符&nbsp)转gbk(汉字内码扩展规范)会有错,去掉;
fileName = fileName.replace(u"\xa0", " ").replace(u"\xc2", "")
# 去掉不合法的文件名字符
fileName = re.sub(r"[/\\:*\"<>|?]", "", fileName)
className = re.sub(r"[/\\:*\"<>|?]", "", className)

# 路径构造,os.getcwd()获取当前路径
dir = os.getcwd() + "/" + className
file = os.getcwd() + "/" + className + "/" + fileName
# 没有课程文件夹则创建
if not os.path.exists(dir):
os.mkdir(dir)
# 存在该文件,返回
if os.path.exists(file):
print("%s已存在,就不下载了" % fileName)
return 0
print("开始下载%s..." % fileName)
s = session.get(url)
with open(file, "wb") as data:
data.write(s.content)
return 1

视频下载

进入课程资源页面:

1
2
h_bs = BeautifulSoup(h.text, "lxml")
url = h_bs.find_all(title="课程视频 - 课程视频")[0].get("href")

又分为:课程视频(录播),直播视频(回放),这两部分处理方法一样,以第一项为例:

由于视频页可能包含多页,先抓取各个页的链接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 当时写的比较傻,采用循环加载下一页做的,其实可以直接抓取网页下面显示的页数,然后在原来 url 的基础上构造为: url+"&pageNum="+i 即可;
allpageURL.append(url)
flag =1
i = 0
while flag:
s = session.get(allpageURL[i])
page = re.search('<span><a href="([^上]*?)">下一页</a></span>',s.text, re.S) # 其实就是获取:"&pageNum="+i
if page :
page = page.groups()[0]
pageURL = 'http://course.ucas.ac.cn' + page
allpageURL.append(pageURL)
else:
flag = 0
i = i+1

接下来,就是循环在每页,不断的获取所有视频播放的 url,然后进入到播放页面,再找到视频源的 url 即可;

视频下载:由于果壳大视频采用的 .m3u8 流媒体格式,我使用到了 ffmpeg(需要提前在电脑上安装);用 subprocess 模块来产生子进程,调用 ffmpeg 完成下载;

1
2
3
4
5
6
### 按照获取到的视频链接调用ffmpeg进行下载,也可以尝试多进程下载,提高下载速度
def download_sp(spName, spUrl):
ins = 'ffmpeg -i ' + spUrl + ' -c copy ' + spName +'.mp4'
p = subprocess.Popen(ins)
p.wait()
print('下载完毕')

写在最后

其实这个脚本是挺久之前弄的了,但是总觉得之前边写边学,零零碎碎,慢慢的又觉得忘的差不多了。这当然不行,于是,重新梳理总结了一下当时的编写历程。

通过这次,主要学习到的知识点:

  1. 利用 webdriver 模拟登陆,以及遇到验证码时,将验证码抓取下来处理,并完成识别;
  2. 利用 session 构造 post 表单的方式,实现网站登录;
  3. 正则表达式的使用;
  4. BeautifulSoup 的查找方法;
  5. subprocess 的简单使用;
  6. 等等

不足:

  • 还需要学习 js 的处理方法;

最后还是需要强调一下,重在学习,利用脚本下载的资源,仅供自己学习使用,请不要传播!

脚本源码