写在最前:互联网并非法外之地,爬虫仅供技术交流
运行环境
- python 3.7.4
- requests 2.10.0
爬取目标
- EDA技术与应用(2020秋)1.1.2 EDA技术概述 教学视频
分析视频字幕接口
找接口就只能凭借经验去network里面翻找,或者借助于浏览器调试,没有过多的技巧。
一、从资源回溯寻找接口
-
带有视频接口的json文件URL分析
https://www.xuetangx.com/api/v1/lms/service/playurl/7ED5FE6BE6C6DAC39C33DC5901307461/?appid=10000
跟其他视频比较,可以得出:
有一个请求参数,这个参数似乎是固定的,所以不用管。
而
7ED5FE6BE6C6DAC39C33DC5901307461
是一个路径变量,不同视频有着不同的该参数。 -
带有字幕接口的json文件URL分析
https://www.xuetangx.com/api/v1/lms/service/s_t_g_p/
这个数据是通过POST请求的,数据查看后发现需要一个json对象
{"c_d":"7ED5FE6BE6C6DAC39C33DC5901307461"}
而且这个
c_d
与上文视频的路径变量一致。所以最后得出请求方案:
c_d = val # method: GET # 视频动态URL url_video = "https://www.xuetangx.com/api/v1/lms/service/playurl/{}/?appid=10000".format(c_d) # method: POST # 字幕URL url_subtitle = "https://www.xuetangx.com/api/v1/lms/service/s_t_g_p/" data = {"c_d":"7ED5FE6BE6C6DAC39C33DC5901307461"}
-
视频接口和字幕接口URL分析
上面的两个json文件直接提供的视频和字幕的接口URL,所以即使他们的URL还带了其它的参数,我们也不再需要关心这些。
可能会担心的就是鉴权问题,但是我已经尝试过了,字幕和视频的接口以及这两个json文件都不需要专门的头部信息进行鉴权。
我们只要找到上面的两个文件,就可进行视频字幕的下载。
所以我们现在需要找到
c_d
。
二、从未知变量回溯寻找接口
-
带有c_d(ccid)的json文件
我们可以在这个文件下的
data
中的content_info
中的media
下找到一个ccid
与c_d
相同,所以我们可以把这里获得的ccid
当成变量给下游的URL。同时我们还在这个文件下找到了视频相关附件的链接,
https://qn-next.xuetangx.com/15679498483925.pptx
,同样不需要鉴权就可以下载。 -
带有c_d(ccid)的json文件URL分析
https://www.xuetangx.com/api/v1/lms/learn/leaf_info/4227236/6195112/?sign=NCIAE08091001906
我们可以看到这个新的路径给爬取增加了不少难度,它多出了两个路径变量(4227236/6195112)和一个请求参数(sign=NCIAE08091001906`)。
而且从这里开始就已经需要鉴权了,头文件得带上相应的参数才可以进行访问。
-
带有id的json文件
https://www.xuetangx.com/api/v1/lms/learn/leaf_info/4227236/6195112/?sign=NCIAE08091001906
中的6195112
是这个文件的leaf_list
中每一个json对象的id
。我们成功的解决了下游URL
的一个变量。 -
带有c_d(ccid)的json文件URL分析
https://www.xuetangx.com/api/v1/lms/learn/course/chapter?cid=4227236&sign=NCIAE08091001906
很幸运的是,下游URL的两个未解决变量在这里出现了,经过这个URL,总体的未知变量没有增多。
经过两个路由后,我们最后可以得出这样的请求方案:
cid = val1 sign = val2 # method: GET # 章节动态URL url_chapter = "https://www.xuetangx.com/api/v1/lms/learn/course/chapter?cid={}&sign={}".format(cid, sign) leaf_id = response(url_chapter) # method: GET # 小节动态URL url_leaf = "https://www.xuetangx.com/api/v1/lms/learn/leaf_info/{}/{}/?sign={}".format(cid, vid, sign)
三、回溯到头再顺流而下
-
未解决的问题
-
"NCIAE08091001906"到底是什么?cid是课程id吗?
我们可以通过退出再登录,使用其它账户来判断它们是否与用户身份相关;通过等待一段时间看它们是否改变,判断是否与时间有关。我们会发现它们既与用户身份无关也与时间无关
我们还可以通过浏览器的调试模式去判断这一点。
最后我们可以得出sign和cid都是课程识别码。
虽然你可以在进入这门课程学习后,在顶上的URL找到这两个参数。但我依旧想更清楚的解释它们是什么,sign(course_sign)是一门课程的标识,而cid(classroom_id)是一门课程每个学期的标识。这些信息都可以在更高的源头追溯到。
但这次我们就先追溯到这里。
-
关于鉴权的问题。
我们在爬虫的时候需要考虑清楚地告诉对方服务器我们是什么?
所以我们需要去看浏览器为我们生成的请求头和其它请求条件呢,这我们可以自己搭一个本地服务,去看
requests
的请求头和浏览器的有什么区别。再通过不断试错,找到当前请求需要的请求头和其它请求条件。
很庆幸的是,
学堂在线
我们需要补充修改的请求头参数非常简单。示例如下:-
方式一
# 这里的代码请不要尝试,sessionid我已经安全退出,失去效力。 # 没有安全退出的话可以保存两周,在此期间可以任意爬取。当然这也跟浏览器的设置有关。 headers = { "xtbz": "xt" } cookies = { "sessionid": "z3rvy7fpp4tqbc4opmzkq1amlvmqde7d" } requests.get("https://www.xuetangx.com/api/v1/lms/learn/leaf_info/4227236/6195112/?sign=NCIAE08091001906",headers=headers,cookies=cookies)
-
方式二
# 方式一直接带上cookie是更好的选择,至少在学堂在线是这样的。 headers = { "xtbz": "xt", "cookies": "sessionid=z3rvy7fpp4tqbc4opmzkq1amlvmqde7d" } requests.get("https://www.xuetangx.com/api/v1/lms/learn/leaf_info/4227236/6195112/?sign=NCIAE08091001906",headers=headers,cookies=cookies)
-
-
-
正式顺流而下
-
找到sign和cid
-
找到cookies
只需要sessionid就好,其它浏览器找cookies自行百度。
-
根据sign和cid请求数据
import json,requests,time cid = "4227236" sign = "NCIAE08091001906" # 请求头 仅供参考 headers = { "xtbz": "xt" } cookies = { "sessionid": "z3rvy7fpp4tqbc4opmzkq1amlvmqde7d" } # 章节信息 url_chapter = "https://www.xuetangx.com/api/v1/lms/learn/course/chapter?cid={}&sign={}".format(cid, sign) time.sleep(0.2) chapter = json.loads(requests.get(url_chapter,headers=headers,cookies=cookies).content) ## 第一章的第一节的所有小节 leaf_list = chapter[\'data\'][\'course_chapter\'][0][\'section_leaf_list\'][0][\'leaf_list\'] ## 第一章的第一节的所有视频小节 video_leaf_list = list(filter(lambda item:item[\'leaf_type\']==0, leaf_list)) ## 第一章的第一节的第一个视频小节的id vid = video_leaf_list[0][\'id\'] # 视频小节信息 url_leaf = "https://www.xuetangx.com/api/v1/lms/learn/leaf_info/{}/{}/?sign={}".format(cid, vid, sign) time.sleep(0.2) video = json.loads(requests.get(url_leaf,headers=headers,cookies=cookies).content) ## ppt等附件 url_file = video[\'data\'][\'content_info\'][\'download\'][0][\'file_url\'] time.sleep(0.2) file = requests.get(url_file).content with open(\'1.pptx\',\'wb\') as f: f.write(file) ccid = video[\'data\'][\'content_info\'][\'media\'][\'ccid\'] ## 视频 time.sleep(0.2) url_video = json.loads(requests.get("https://www.xuetangx.com/api/v1/lms/service/playurl/{}/?appid=10000".format(ccid)).content)[\'data\'][\'sources\'][\'quality10\'][0] time.sleep(0.2) content_video = requests.get(url_video).content with open(\'1.mp4\',\'wb\') as f: f.write(content_video) ## 字幕 time.sleep(0.2) url_subtitle = json.loads(requests.post("https://www.xuetangx.com/api/v1/lms/service/s_t_g_p/",data={"c_d": ccid},headers=headers).content)[\'data\'][0][\'data\'] time.sleep(0.2) content_subtitle = requests.get(url_subtitle).text with open(\'1.txt\',\'w\') as f: f.write(content_subtitle)
-
写在最后
上面的代码主要是提供一个思路,实际只用于抓取EDA技术与应用(2020秋)第一章的第一节的第一个视频小节,因为我们不可以保证每一门课的第一章的第一节都有视频小节,也不能保证每一个小节都有附件,每一个视频都有字幕,爬取其它视频还要做容错处理。
如果想一次爬所有视频也可以实现,用for循环就可以。请记得不要过度频繁地发送请求,会给服务器造成巨大的压力,服务器针对此也有很多的反爬手段。
爬取的字幕是json数据,想要变成字幕文件还得做相应处理。
这篇文章还有后续,会继续完善相应功能。