最近想把抖音里的点过赞的视频保存下来,几百条视频一个一个下载太麻烦了,于是动手写了一个爬虫程序,实现批量下载喜欢的视频。源码见文末
网上很多Python的例子,教大家怎么下载抖音视频,甚至还有人专门去反编译抖音的APK,获取其中的签名算法。我这里没有反编译,只是做了一个爬虫来下载。由于抖音加入了一定的反爬虫机制和类似于Ajax这样的技术,使用传统的WebRequest和HttpClient获取到的数据有可能为空,所以这里我们用真正的浏览器CefSharp来请求数据。
运行截图
做得简单了一点,下载开始后不能暂停,视频数目较多时运行也会较慢
思路
通过在PC浏览器上调用开发者选项分析http://v.douyin.com/5bjheV/的请求和响应数据,发现抖音是有相关的API接口的,只不过其中一些必要的GET参数是靠什么算法生成的我们不得而知,例如_signature,该参数可能是由Javascript动态生成的,缺少此参数就会获取不到数据。不过这并不影响我们获取视频信息,我们只要把网页和JavaScript在CefSharp里运行一下,拿到运行结果即可,不需要去破译签名算法。
用CefSharp来截获请求URL,分析请求资源的类型,如果是json,就说明这个URL是我们需要的请求地址(最重要的签名信息包含在URL地址里面),然后再拿该URL获取json,解析取出里面的视频id,最后替换https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200ff00000bjvpflveqk80nq02s530&line=0里面的video_id即可拿到视频源文件(这一步里可能会有一次地址重定向)。不过可惜的是,这样拿到的视频文件是含有水印的,如果还想去水印,可以百度一下“抖音去水印”,把视频地址批量导进去即可。
做法
安装CefSharp
CefSharp的介绍和安装就不多说了,在Nuget里搜索安装,然后添加引用把几个dll包含进去就行了,需要注意的是平台要选择“x86”或“x64”,不能选择“AnyCPU”。
关于如何截获URL
自定义一个类MyRequestHandler,实现IRequestHandler接口。该接口有很多方法需要实现,当然我们可以只挑我们关心的方法实现,其他有返回值的添加返回语句,返回null或者true、false,这样就会自动调用CefSharp的默认实现方法。下面这个方法是用来判断请求地址的,需要我们根据需求实现。
public IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling)
{
if (request.Headers["Accept"].Contains("json"))
{
SignatureCaptured?.Invoke(request.Url);//引发事件
}
return null;
}
使用CefSharp
ChromiumWebBrowser browser = null;
MyRequestHandler handler;
public Form1()
{
browser = new ChromiumWebBrowser();
browser.IsBrowserInitializedChanged += OnBrowserInitialized;
handler = new MyRequestHandler();//自定义handler来截获请求的URL
browser.RequestHandler = handler;
handler.SignatureCaptured += OnSignatureCaptured;
//browser.Dock = DockStyle.Fill;
InitializeComponent();
System.Net.ServicePointManager.DefaultConnectionLimit = 50;//设置webrequest的并发数
this.Controls.Add(browser);
}
捕获到json请求地址时
引发一个事件,在该事件中获取并解析json,然后调用下载函数
/// <summary>
/// 获取到json的请求URL时发生,主要目的是获取URL中的签名等参数
/// </summary>
/// <param name="obj">api的请求URL</param>
private void OnSignatureCaptured(string obj)
{
Debug.WriteLine(obj);
//为了不阻塞UI线程,在新线程中下载
task = Task.Run(new Action(() =>
{
var postVid = GetUserVideos(obj);//发布的视频
var likeVid = GetUserVideos(obj.Replace("post", "like"));//点赞的视频
postCnt.Invoke(new Action(() =>
{
progressBar1.Value = 0;
progressBar1.Maximum = postVid.Count + likeVid.Count;
postCnt.Text = postVid.Count.ToString();
likeCnt.Text = likeVid.Count.ToString();
}));
Download(postVid, "作品");
Download(likeVid, "喜欢");
}));
}
请求json数据
因为Ajax用的是XML请求方式,所以这里用的不是HttpRequest类,而是XMLHttp类,需要在引用里添加COM组件,搜索并找到Microsoft Xml v3.0
/// <summary>
/// 请求json数据
/// </summary>
/// <param name="requestUrl"></param>
/// <returns></returns>
private string GetJsonData(string requestUrl)
{
MSXML2.XMLHTTP xmlhttp = new MSXML2.XMLHTTPClass();
//xmlhttp.setRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 SE 2.X MetaSr 1.0");
xmlhttp.open("GET", requestUrl, false, null, null);
xmlhttp.send(null);
Debug.WriteLine(xmlhttp.responseText);
return xmlhttp.responseText;
}
解析json获得视频id
Newtonsoft.Json很强大,再配合dynamic关键字,使C#解析json变得十分方便
/// <summary>
/// 获取短视频ID
/// </summary>
/// <param name="jsonData">json数据</param>
/// <param name="vidSet">包含视频ID的集合</param>
/// <returns>如果还有更多,则返回最大光标,否则返回0</returns>
private long GetVideoID(string jsonData, ref HashSet<string> vidSet)
{
dynamic obj = JsonConvert.DeserializeObject(jsonData);
for (int i = 0; i < obj.aweme_list.Count; i++)
{
var vid = obj.aweme_list[i].video.download_addr.uri.ToString();
vidSet.Add(vid);
}
if (Convert.ToBoolean(obj.has_more))
{
return Convert.ToInt64(obj.max_cursor);
}
else
{
return 0;
}
}
循环调用API获取所有视频
每次返回的json都只是部分数据,可能还有下一页,因此需要循环调用,直到没有更多数据返回
/// <summary>
/// 获取用户发布的视频或点赞的视频
/// </summary>
/// <param name="url"></param>
/// <returns>视频Id集合</returns>
private HashSet<string> GetUserVideos(string url)
{
HashSet<string> set = new HashSet<string>();
long cursor = 0;
do
{
url = System.Text.RegularExpressions.Regex.Replace(url, @"max_cursor=(\d)*", "max_cursor=" + cursor);
Console.WriteLine(url);
//var pms = GetQueryParameters(obj);
string jsonData = GetJsonData(url);
cursor = GetVideoID(jsonData, ref set);
} while (cursor > 0);
return set;
}
下载视频
拿到视频ID后就可以下载视频了
/// <summary>
/// 批量下载短视频
/// </summary>
/// <param name="vidSet">视频ID集合</param>
/// <param name="dir">下载目录</param>
private void Download(HashSet<string> vidSet, string dir)
{
//如果目录不存在则创建
if (Directory.Exists(dir) == false)
{
Directory.CreateDirectory(dir);
}
foreach (var item in vidSet)
{
try
{
//判断文件是否已经存在
string path = Path.Combine(dir, item + ".mp4");
if (File.Exists(path))
{
OnDownloadComplete(item);
continue;
}
//构造http请求
var http = WebRequest.CreateHttp(string.Format("https://aweme.snssdk.com/aweme/v1/playwm/?video_id={0}&line=0", item));
http.Timeout = 5000;
http.Accept = "*/*";
http.Headers.Set(HttpRequestHeader.AcceptLanguage, "zh-CN,zh;q=0.8");
http.AddRange("bytes", 0);
http.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 SE 2.X MetaSr 1.0";
var response = http.GetResponse();
OnDownloading(item, http.Address.ToString());
//将响应流拷贝到文件
var fs = File.Create(path);
var stream = response.GetResponseStream();
stream.CopyTo(fs);
//关闭流
stream.Close();
fs.Close();
OnDownloaded(item);
}
catch (Exception ex)
{
OnDownloadError(item, ex.Message);
}
}
}
由于嵌入了CefSharp,所以源码体积会有点大。如果重新编译失败,检查引用里有没有黄色叹号,如果有,请删掉后重新添加引用。