python实战笔记之(8):下载知乎视频
这篇想写很久了,今天专门搞了搞,现在把用python下载知乎视频的整个流程码下来。
(1)目标站点分析
比如这篇知乎文章https://www.zhihu.com/question/279247693/answer/442578073里有一个小视频,我们想把它下载下来,首先对该网页进行分析。先来看一下原始请求:
原始请求有没有返回视频的链接呢?我们点开“Preview”和“Response”看一看:
这不就是我们想要的东西吗!接下来就是常规操作了,requests请求到网页代码之后可以用正则表达式或者BeautifulSoup和PyQuery之类的解析库提取到视频的URL。
现在让我们看一看上面的视频URL打开之后是什么东西:
显然,这是一个可以播放的视频,问题是我们怎样才能把它下载下来呢?一步一步来分析:
首先,我们发现原始请求返回的状态码是301,而且细心一些就可以看到网页的URL变了,从原来的https://www.zhihu.com/video/1001598833150816256变成了一个新的URLhttps://v.vzuu.com/video/1001598833150816256,如上图。状态码301代表什么呢,同样百度一下:
301表示永久重定向(301 moved permanently),表示请求的资源分配了新url,以后应使用新url。
这样就可以理解了,也就是请求视频的URL之后,转到了一个新的URL,我们可以用一个函数获取到这个新的URL:
def get_real_url(url, try_count=1):
if try_count > 3:
return None
try:
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36'
}
response = requests.get(url, headers=headers, timeout=10)
if response.status_code >= 400:
return get_real_url(url, try_count+1)
return response.url
except RequestException:
return get_real_url(url, try_count+1)
然后,我们看一下请求这个新的URL会发生什么:
请求这个网址之后,点击播放视频,会跳出来很多新的请求,其中有一个含有“m3u8”的请求,打开该请求的URL,发现浏览器下载了一个m3u8文件:
这么小不可能是视频吧,果然打开之后并不能播放:
那这个m3u8文件究竟是什么?
M3U8文件是指UTF-8编码格式的M3U文件。M3U文件是记录了一个索引纯文本文件,打开它时播放软件并不是播放它,而是根据它的索引找到对应的音视频文件的网络地址进行在线播放。
我们用文本编辑器打开它如下:
果然,里面有很多像链接一样的东西,但并不是真正的链接。这时我们注意到原来的请求还返回了很多带有“ts”的请求,而且这些请求的URL的后半部分和m3u8文件里一毛一样!
怀着激动的心情打开“ts”请求的URL,浏览器下载了一个.ts文件,ts文件是什么?
ts是日本高清摄像机拍摄下进行的封装格式,全称为MPEG2-TS。ts即"Transport Stream"的缩写。MPEG2-TS格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。
用播放器打开它,虽然只有2秒,但它正是原视频的开头部分。
也就是说,只要把这一个个ts文件下载下来,就能组成一个完整的视频。
对比以下内容:
# m3u8请求的url
https://vdn.vzuu.com/Act-ss-m3u8-sd/4fcea71e5b2743368ad3b023a224b4ae/1d3ebaf8-8775-11e8-8301-0242ac112a0c.m3u8?auth_key=1534097100-0-0-3f1616ac0b8a4c236d7a5a03756092fa&expiration=1534097100&disable_local_cache=0
# m3u8文件中的url
1d3ebaf8-8775-11e8-8301-0242ac112a0c-00001.ts?auth_key=1534097100-0-0-76229a4cf68e95b2590d4be99053665c
# ts请求的url
https://vdn.vzuu.com/Act-ss-m3u8-sd/4fcea71e5b2743368ad3b023a224b4ae/1d3ebaf8-8775-11e8-8301-0242ac112a0c-00001.ts?auth_key=1534097100-0-0-76229a4cf68e95b2590d4be99053665c
显然,ts请求的url是由m3u8请求url的前面部分和m3u8文件中的url组合而成的,所以我们只要获得了m3u8请求的url就能构造出ts请求的url,从而把视频流下载下来。那么怎样得到m3u8请求的url呢?
原始请求的下面是一个获取js的GET请求,再下面也是一个GET请求,我们来看一下这个请求,并记住这个重要的url,突破口就在这里:
点开“Preview”,我们发现m3u8请求的url正是在这里:
追本溯源,这个请求的url又是从哪里来的呢?
首先,第二个js请求是由原始请求获得的,因为原始请求的Response中包含了js请求的url:
所以这个关键的url:https://lens.zhihu.com/api/videos/1001598833150816256就必然是包含在js中的,看一下Preview果不其然:
但是这个js太难解析了,写程序时就直接跳过这一步,也就是直接用正则表达式解析https://v.vzuu.com/video/1001598833150816256,得到后面那一串用于区分不同视频的数字,然后和https://lens.zhihu.com/api/videos/拼凑在一起,得到https://lens.zhihu.com/api/videos/1001598833150816256。当然,也可以直接解析https://www.zhihu.com/video/1001598833150816256得到那一串数字。
(2)流程框架
1.抓取视频的URL
利用requests请求目标站点,得到HTML代码,然后用PyQuery解析出视频的URL(可能有多个),类似于“https://www.zhihu.com/video/xxxxxxx”。
2.获取视频的真实URL
访问上面得到的视频URL,会发生重定向,返回其重定向后的真实URL,类似于“https://v.vzuu.com/video/xxxxxxx”。(这一步可以省略)
3.获取m3u8请求的URL
用正则表达式解析“https://v.vzuu.com/video/xxxxxxx”,得到后面那一串数字,然后和“https://lens.zhihu.com/api/videos/”拼凑在一起,得到URL“https://lens.zhihu.com/api/videos/xxxxxxx”,访问该网址,从其Response中解析出m3u8请求的URL。
4.下载视频
根据m3u8请求的URL和m3u8请求的Response构造出ts请求的URL,下载ts视频流并合并为一个mp4文件。简单一些可以使用FFmpeg解析m3u8并下载合并视频,但为了了解其原理,这部分我自己写了一个解析方法。
(3)爬虫代码
import os
import re
import json
import requests
from requests import RequestException
from pyquery import PyQuery as pq
def get_page(url):
try:
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36'
}
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
return response.text
return None
except RequestException:
return None
def parse_page(html):
doc = pq(html)
items = doc('.url').items()
for item in items:
yield item.text()
def get_real_url(url, try_count=1):
if try_count > 3:
return None
try:
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36'
}
response = requests.get(url, headers=headers, timeout=10)
if response.status_code >= 400:
return get_real_url(url, try_count+1)
return response.url
except RequestException:
return get_real_url(url, try_count+1)
def get_m3u8_url(url):
try:
path_pattern = re.compile('(\d+)', re.S).search(url).group(1)
get_play_url = 'https://lens.zhihu.com/api/videos/' + path_pattern
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36'
}
content = requests.get(get_play_url, headers=headers).text
data = json.loads(content) # 将json格式的字符串转化为字典
if data and 'playlist' in data.keys():
m3u8_url = data.get('playlist').get('sd').get('play_url')
return m3u8_url
except Exception:
return None
def get_m3u8_content(url, try_count=1):
if try_count > 3:
print('Get M3U8 Content Failed', url)
return None
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36'
}
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
return response.text
return get_m3u8_content(url, try_count+1)
except RequestException:
return get_m3u8_content(url, try_count+1)
def get_ts(url, try_count=1):
if try_count > 3:
print('Get TS Failed', url)
return None
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36'
}
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
return response
return get_ts(url, try_count+1)
except RequestException:
return get_ts(url, try_count+1)
def download_ts(m3u8_url, video_url, video_count):
print('准备下载', video_url)
download_path = 'E:/PycharmProjects/zhihu_vedio/'
try:
all_content = get_m3u8_content(m3u8_url)
file_line = all_content.split('\n') # 读取文件里的每一行
# 通过判断文件头来确定是否是M3U8文件
if file_line[0] != '#EXTM3U':
raise BaseException('非M3U8链接')
else:
unknow = True # 用来判断是否找到了下载的地址
for index, line in enumerate(file_line):
if "EXTINF" in line:
unknow = False
# 拼出ts片段的URL
pd_url = m3u8_url.rsplit('/', 1)[0] + '/' + file_line[index + 1] # rsplit从字符串最后面开始分割
response = get_ts(pd_url)
c_fule_name = str(file_line[index + 1]).split('?', 1)[0]
source_path = c_fule_name.split('-', 1)[0] # 区分不同源的视频流
print('正在下载', c_fule_name)
with open(download_path + c_fule_name, 'wb') as f:
f.write(response.content)
f.close()
if unknow:
raise BaseException('未找到对应的下载链接')
else:
print('下载完成,准备合并视频流...')
merge_file(download_path, source_path, video_count)
except Exception:
return None
def merge_file(download_path, source_path, video_count):
os.chdir(download_path) # 修改当前工作目录
merge_cmd = 'copy /b ' + source_path + '*.ts video' + str(video_count) + '_' + source_path + '.mp4'
split_cmd = 'del /Q ' + source_path + '*.ts'
os.system(merge_cmd)
os.system(split_cmd)
def main():
url = 'https://www.zhihu.com/question/279405182/answer/410204397' # 含有知乎小视频的链接
html = get_page(url)
video_count = 0
if html:
video_urls = parse_page(html)
for video_url in video_urls:
if video_url:
real_url = get_real_url(video_url)
if real_url:
m3u8_url = get_m3u8_url(real_url)
if m3u8_url:
video_count += 1
download_ts(m3u8_url, video_url, video_count)
if __name__ == '__main__':
main()
下载好的文件类似于这样: