欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

python实战笔记之(8):下载知乎视频

程序员文章站 2024-03-16 14:26:52
...

这篇想写很久了,今天专门搞了搞,现在把用python下载知乎视频的整个流程码下来。

(1)目标站点分析

比如这篇知乎文章https://www.zhihu.com/question/279247693/answer/442578073里有一个小视频,我们想把它下载下来,首先对该网页进行分析。先来看一下原始请求:

python实战笔记之(8):下载知乎视频

原始请求有没有返回视频的链接呢?我们点开“Preview”和“Response”看一看:

python实战笔记之(8):下载知乎视频

python实战笔记之(8):下载知乎视频

这不就是我们想要的东西吗!接下来就是常规操作了,requests请求到网页代码之后可以用正则表达式或者BeautifulSoup和PyQuery之类的解析库提取到视频的URL。

现在让我们看一看上面的视频URL打开之后是什么东西:

python实战笔记之(8):下载知乎视频

显然,这是一个可以播放的视频,问题是我们怎样才能把它下载下来呢?一步一步来分析:

python实战笔记之(8):下载知乎视频

首先,我们发现原始请求返回的状态码是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会发生什么:

python实战笔记之(8):下载知乎视频

请求这个网址之后,点击播放视频,会跳出来很多新的请求,其中有一个含有“m3u8”的请求,打开该请求的URL,发现浏览器下载了一个m3u8文件:

python实战笔记之(8):下载知乎视频

这么小不可能是视频吧,果然打开之后并不能播放:

python实战笔记之(8):下载知乎视频

那这个m3u8文件究竟是什么?

M3U8文件是指UTF-8编码格式的M3U文件。M3U文件是记录了一个索引纯文本文件,打开它时播放软件并不是播放它,而是根据它的索引找到对应的音视频文件的网络地址进行在线播放。

我们用文本编辑器打开它如下:

python实战笔记之(8):下载知乎视频

果然,里面有很多像链接一样的东西,但并不是真正的链接。这时我们注意到原来的请求还返回了很多带有“ts”的请求,而且这些请求的URL的后半部分和m3u8文件里一毛一样!

python实战笔记之(8):下载知乎视频

怀着激动的心情打开“ts”请求的URL,浏览器下载了一个.ts文件,ts文件是什么?

ts是日本高清摄像机拍摄下进行的封装格式,全称为MPEG2-TS。ts即"Transport Stream"的缩写。MPEG2-TS格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。

用播放器打开它,虽然只有2秒,但它正是原视频的开头部分。

python实战笔记之(8):下载知乎视频

python实战笔记之(8):下载知乎视频

也就是说,只要把这一个个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,突破口就在这里:

python实战笔记之(8):下载知乎视频

点开“Preview”,我们发现m3u8请求的url正是在这里:

python实战笔记之(8):下载知乎视频

追本溯源,这个请求的url又是从哪里来的呢?

python实战笔记之(8):下载知乎视频

首先,第二个js请求是由原始请求获得的,因为原始请求的Response中包含了js请求的url:

python实战笔记之(8):下载知乎视频

python实战笔记之(8):下载知乎视频

所以这个关键的url:https://lens.zhihu.com/api/videos/1001598833150816256就必然是包含在js中的,看一下Preview果不其然:

python实战笔记之(8):下载知乎视频

python实战笔记之(8):下载知乎视频

但是这个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()

下载好的文件类似于这样:

python实战笔记之(8):下载知乎视频