前言
浏览知乎的时候发现知乎里的视频没有提供保存到本地的功能。搜索了一波解决方法后,发现有个答主做的小程序应该是最佳解决方案,不过答主并没有给出实现方式。正好最近在学小程序开发,所以就研究了一下,做了一个叫"积木小盒"的微信小程序,将知乎视频下载功能实现了。
使用
仅需两步:
1.
在知乎回答页点击右上角,出现弹出框,再点击复制链接
2.
打开"积木小盒"微信小程序,点击知乎视频。粘贴链接后,点击视频下载,即可爬取知乎回答页的所有视频并自动保存到本地相册。
功能分析
使用小程序十分方便,一些其他解决方案比如在pc端打开网页查找元素,找到相应视频链接再下载,操作上其实比不上小程序。一方面对于非开发的小白用户来说,使用控制台查看元素其实还是有一点成本的。另一方面,这种方式需要借助pc端,无法全部在手机上操作完成,大部分用户的使用习惯还是会更倾向于在手机上便捷操作。
原理
知乎视频下载的实现原理其实和在网页中查看元素,找到视频链接下载视频的操作是一样的。它的实现分为两步:1.利用cheerio爬取视频链接 2.使用小程序下载和保存视频。很显然,第一步才是实现的关键。
1. 利用cheerio爬取视频链接
由于微信小程序访问的api地址受白名单限制,所以爬取操作都放在node.js服务端进行。我们只需要实现一个接口,传入复制的知乎链接,返回爬取的该网页链接下所有视频的下载地址。这样在爬取阶段我们只需要将服务端域名 https://www.readingblog.cn 添加到访问白名单中,而不是将各式各样的知乎域名添加到小程序白名单中。
接下来让我们看看服务端是怎么实现的:
const axios = require('axios');
const cheerio = require('cheerio');
const videoUrlPrefix = 'https://lens.zhihu.com/api/v4/videos/';
exports.urlFetchAll = async (pageUrl) => {
const pageRes = await axios.get(pageUrl);
const videoSrcList = [];
if (pageRes.status === 200) {
$ = cheerio.load(pageRes.data);
const videoBox = $('.video-box');
for (let i = 0, len = videoBox.length; i < len; i ++) {
const videoPageUrl = $(videoBox[i]).attr('href');
const params = videoPageUrl.split('/');
const videoId = params[params.length - 1];
const videoRes = await axios.get(`${videoUrlPrefix}${videoId}`);
if (videoRes.status === 200) {
const videoSrc = videoRes.data.playlist.LD.play_url;
videoSrcList.push(videoSrc);
}
}
}
return videoSrcList;
};
还是比较容易看出它的实现思路的,这里使用了页面爬取和接口爬取相结合的方式。由于知乎网页使用了js动态渲染,爬取页面没有能够直接爬取到视频元素的链接。所以在页面爬取阶段,我们其实只获取了它的videoId。经过对整个页面元素的过滤、筛选,我们发现带video-box类名的href属性会直接带上videoId,我们直接爬取它。再通过抓包分析,我们发现只要向 https://lens.zhihu.com/api/v4... 接口传入videoId就可以返回视频的下载地址,所以通过知乎链接爬取视频下载地址的接口就实现了。
2. 使用小程序将视频保存到本地
实现了爬取接口整个功能主体就完成了,剩下的我们在微信小程序调用这个接口,传入用户输入的知乎链接地址,获取视频下载地址。再调用wx的api,downloadFile和saveVideoToPhotosAlbum分别下载和保存视频。
Page({
data: {
zhihuPageUrl: '',
},
handleUrlChange(e) {
this.setData({
zhihuPageUrl: e.detail.value
})
},
downloadVideo() {
const pageUrl = this.data.zhihuPageUrl;
if (!this.data.zhihuPageUrl) {
wx.showToast({
title:'链接为空!',
icon:'none',
duration:2000
});
return;
}
wx.request({
url: vedioFetchUrl,
data: {
pageUrl,
},
header: {
'content-type': 'application/json'
},
success: (res) => {
res = res.data;
if (res.code === 1) {
const total = res.data.length;
wx.showToast({
title:`开始下载!`,
icon:'success',
duration:2000
});
res.data.forEach((url, index) => {
wx.downloadFile({
url,
success: (res) => {
const filePath = res.tempFilePath;
wx.saveVideoToPhotosAlbum({
filePath: filePath,
success() {
wx.showToast({
title:`${index + 1}/${total}下载成功!`,
icon:'success',
duration:2000
});
}
})
}
})
})
} else {
wx.showToast({
title:'请求失败!',
icon:'none',
duration:2000
});
}
}
});
},
})
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。