背景
由于之前做视频搬运,需要批量保存某不存在的网站视频(保险起见就不透露具体网址了,胆小嘿嘿~~),这是一个长期而固定的事情,几乎每天都要去关注有没有新的视频然后下载保存,既然如此重复,作为一个开发者,当然要想着有没有什么更省力的方法,虽然是做前端的,还是觉得这个场景用python来搞定最合适,在此记录下解决的过程。
我的日常场景是浏览视频标题和视频封面,来大致判断这个视频是不是值得下载(每个视频都预览一遍太慢了),但是时间久了,发现过程中有以下痛点:
痛点
- 网站自带的保存按钮访问路径比较长,点击去保存又返回继续挑选效率很低。
- 最烦恼的是命名问题,网站保存视频文件名是编号,要是标题就好了,保存久了我每次都回忆不起来视频是什么内容。
- 由于需要代理访问,下载比较慢,同时下载又边浏览找视频就会很卡,想把查找和下载分离。
根据上面痛点想出的解决方案:
- 使用python爬取页面,然后自动下载视频,但是这样会有一个问题,我并不是需要所有视频,有些是垃圾视频不需要下载,python似乎还没法做到识别我对哪些视频是有价值的,所以这一步人工绕不开,我选择直接把目标视频的详情页直接右键另存为本地html文件,随后再用python批量处理这些本地的html文件。
- 解析html文件取出视频的标题和下载url,按格式存为本地json
- 读取json,循环下载整个list的视频,并按配置给视频命名
目标功能点
- 递归读取html列表
- 研究html内格式、解密url
- 保存解析产物为json
- 读取json下载保存视频
核心代码
递归读取html目录
这一步可以将每一个html文件的路径从Downloads文件夹取出来。
def get_all_path():
global train_path, all_path, labels
train_path, all_path, labels = "./Downloads", [], []
# 递归获取文件夹内文件列表
def get_label_and_wav(path, file):
dirs = os.listdir(path)
for a in dirs:
# print(a)
# # 是否为文件夹
# print(os.path.isfile(path + "/" + a))
if os.path.isfile(path + "/" + a):
all_path.append(dirs)
if file != "":
labels.append(file)
else:
get_label_and_wav(str(path) + "/" + str(a), a)
# 循环遍历这个文件夹
return all_path, labels
# 第一步、加载文件,获取文件路径以及标签
[all_path, labels] = get_label_and_wav(train_path, "")
loop_parse_html(paths=all_path[0])
循环html文件列表
有一个小坑是python生成json会自动使用单引号,不符合json标准,需要做下ensure_ascii配置,见注释
# 循环html路径列表,解析每一个链接和标题存入json
def loop_parse_html(paths):
global res_json
res_json = []
for index in range(len(paths)):
v = paths[index]
v_len = len(v)
# 当前后缀四位
v_end = v[v_len - 4:len(v)]
# 跳过非html结尾文件
if v_end == "html":
parse_html(position=index, path=f"{train_path}/{paths[index]}")
sleep(0.1)
# 这里将ensure_ascii配置为false,不然会强制存为单引号格式的json,不符合标准,也会导致中文是编码显示
res_json = json.dumps(res_json, ensure_ascii=False)
# 创建一个本地json文件存入结果
file = open('urls.json', 'w')
file.write(str(res_json))
file.close()
重点,解析单个html文件
实际上这里也可以直接用request库从网络爬取,区别不大,只是一个取本地一个取请求,本质结果都是返回一个html文件传入xpath返回结构化的html对象而已,也就是下面的html_tree,由于我的场景特殊需要筛选,才用这种本地html的形式。
另外通过分析html内容,发现下载链接是一个js生成的a标签的前部分,也就是类似于下方,关键的href链接是被编码过的,也需要处理下。
<a href="http://21312jhg312@#$@#$@#$">
编码就是前端的enencodeURIComponent,查了下python里可以使用unquote库来解码
from urllib.parse import unquote # url解码库,类似于js里的decodeURIComponent
注:xpath是一个流行的html解析库,不过在此处也需要特殊处理,留意代码注释的标签闭合问题,通过自己创建一个html解析器来解决xpath对未闭合标签不识别的问题。
# 解析单个html
def parse_html(position, path):
position = position + 1
print(f'开始第{position}次解析:{path}')
# 因为现在html存在未闭合的标签,如title标签,xpath解析会报错,因为闭合不完整
# 所以这里自己创建一个html解析器parser
parser = etree.HTMLParser(encoding="utf-8")
# 使用etree解析返回一个html结构对象
html_tree = etree.parse(path, parser=parser)
# 文本形式可直接打印的html,此处暂时不用,用xpath结构化解析
# result = etree.tostring(html_tree, pretty_print=True, encoding="utf-8").decode("utf-8")
# print(result)
print(f'解析下载链接')
# 通过解析获取目标视频链接a标签,实际是一个a标签被加密了
aim_video_url = html_tree.xpath('// *[ @ class = "demo-class"] / script / text()')[0]
# 再次处理链接格式
# 找到a标签首位,截取出来
a_start = aim_video_url.find('("')
a_end = aim_video_url.find('")')
aim_video_url = aim_video_url[a_start + 2:a_end]
# 解码a标签
aim_video_url = unquote(aim_video_url)
# 取出a标签的href属性就是下载链接了,这里是字符串就用截取了,不用xpath解析
href_start = aim_video_url.find('https')
href_end = aim_video_url.find('>')
aim_video_url = aim_video_url[href_start:href_end]
print(f"链接解析完成:{aim_video_url}")
print(f'解析标题')
# 目标标题
aim_title = html_tree.xpath('/html//title/text()')[0]
# 处理下标题格式,去掉无用的字符、空格、回车换行等
aim_title = aim_title.replace(" ", "")
aim_title = aim_title.replace("\n", "")
useless_str_start = aim_title.find("Chinesehomemadevideo")
aim_title = aim_title[0:useless_str_start]
print(f"标题解析完成:{aim_title}\n")
write_json_file(title=aim_title, link=aim_video_url)
存入全局的json变量,在最开始的循环html列表结束后,json变量会被写入本地json文件
# 将标题和下载链接按键值对存入json文件,供下载脚本读取
def write_json_file(title, link):
temp = {
"title": f"{title}",
"link": f"{link}"
}
res_json.append(temp)
进入下载保存流程
这一块其实比较简单
读取json本地配置
def get_video(index):
if index > len(json_res) - 1:
print(f'下载完毕,一共下载了{index}次')
quit(200)
else:
print(f'开始第{index + 1}次下载')
save_video(index)
def main():
with open('urls.json', encoding='utf-8') as res:
print('开始读取json配置')
# 把json配置读出来保存到全局变量
global json_res
json_res = json.load(res)
get_video(index=0)
循环下载,组合文件名将视频存入指定目录
这里做了一个简单的请求时间随机发起
def save_video(index):
url = json_res[index].get("link")
title = json_res[index].get("title")
video_content = requests.get(url).content
with open('./output/' + title + '.mp4', 'wb') as f:
f.write(video_content)
print(f"第{index + 1}次下载完成\n")
ran = random.randint(0, 10)+random.randint(0, 10)/10+random.randint(0, 10)/100
# 随机休息1-10秒两位小数的时间
print(f'休息{ran}秒~')
# sleep(ran)
get_video(index + 1)
到这一步就全部完成
结语
我作为前端对python的应用或许不是那么精通,如有纰漏,欢迎指出~