用CefSharp做万能爬虫,批量下载抖音用户发布的作品以及点赞视频

最近想把抖音里的点过赞的视频保存下来,几百条视频一个一个下载太麻烦了,于是动手写了一个爬虫程序,实现批量下载喜欢的视频。源码见文末

网上很多Python的例子,教大家怎么下载抖音视频,甚至还有人专门去反编译抖音的APK,获取其中的签名算法。我这里没有反编译,只是做了一个爬虫来下载。由于抖音加入了一定的反爬虫机制和类似于Ajax这样的技术,使用传统的WebRequest和HttpClient获取到的数据有可能为空,所以这里我们用真正的浏览器CefSharp来请求数据。

2019.10.9 14:00更新------------------------------------------------------------------

经测试本软件偶尔会失效(有时又会正常),距离这篇博客诞生只有一个月左右的时间,不得不说抖音反爬策略改的太快了。但是好在没有太大的改动,稍微把源程序改一下便能继续使用。为防止和谐以及再次失效,具体方法我就不贴出来了,请自行研究。其实对付反爬无非就是伪装,不管他怎么改,相信道高一尺魔高一丈,一种方法不行时多试试其他办法,总会找到漏洞的。

--------------------------------------------------------------------------------本篇博客不再更新

运行截图

第1版:

该版本只实现了基本功能。

当文件存在时,跳过并继续下载下一个。

为防止url过期,真实视频地址只在即将下载时才获取。

用CefSharp做万能爬虫,批量下载抖音用户发布的作品以及点赞视频_第1张图片

第2版:

新增视频封面、分享数、评论数、点赞数显示。

新增下载管理功能,可以暂停下载。为保证数据完整,点击暂停后并不会马上暂停,而是等待当前视频下载完成后再暂停

用CefSharp做万能爬虫,批量下载抖音用户发布的作品以及点赞视频_第2张图片

第3版:

当视频数量过多时,增加分页显示功能。默认每页显示18条视频

新增无水印下载功能

新增点击封面,直接播放视频的功能

有一个BUG未解决:偶见界面卡死,等待一会就好了,目前原因未知

用CefSharp做万能爬虫,批量下载抖音用户发布的作品以及点赞视频_第3张图片

思路

通过在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=v0200fea0000bl0juqr2ap9c5omulth0&line=0里面的video_id即可拿到视频源文件(这一步里可能会有一次地址重定向)。不过可惜的是,这样拿到的视频文件是含有水印的,网上有人说把playwm改成play可以去水印,大家可以亲自尝试

做法

安装CefSharp

CefSharp的介绍和安装就不多说了,在Nuget里搜索安装,然后添加引用把几个dll包含进去就行了,需要注意的是平台要选择“x86”或“x64”,不能选择“AnyCPU”。

关于如何截获URL

自定义一个类MyRequestHandler,实现IRequestHandler接口。该接口有大概10多个方法需要实现,当然我们可以只挑我们关心的方法实现,对于不关心的方法可以这样做:没有返回值要求的保持函数体为空,有返回值要求的需要添加返回语句,根据返回值类型以及要求,可以返回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,然后调用下载函数

        /// 
        /// 获取到json的请求URL时发生,主要目的是获取URL中的签名等参数
        /// 
        /// api的请求URL
        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请求,需要注意的是这里用的不是HttpWebRequest类或者HttpClient类,而是XMLHttp类。在引用里添加COM组件,搜索并找到Microsoft Xml v3.0

       /// 
        /// 请求json数据
        /// 
        /// 
        /// 
        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变得十分方便

       /// 
        /// 获取短视频ID
        /// 
        /// json数据
        /// 包含视频ID的集合
        /// 如果还有更多,则返回最大光标,否则返回0
        private long GetVideoID(string jsonData, ref HashSet 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都只是部分数据,可能还有下一页,因此需要循环调用,直到没有更多数据返回

        /// 
        /// 获取用户发布的视频或点赞的视频
        /// 
        /// 
        /// 视频Id集合
        private HashSet GetUserVideos(string url)
        {
            HashSet set = new HashSet();
            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后就可以下载视频了

        /// 
        /// 批量下载短视频
        /// 
        /// 视频ID集合
        /// 下载目录
        private void Download(HashSet 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))
                    {
                        OnDownloaded(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,所以源码体积会有点大。

编译前要先还原Nuget包。如果重新编译失败,首先检查平台是否为x86而非AnyCPU,然后检查引用里有没有黄色叹号,如果有,请删掉后重新添加引用。

https://pan.baidu.com/s/1U4_HyT9GdFJctQ56UkjaRw

https://github.com/DoraemonHC/doraemonhc.github.com/releases

你可能感兴趣的:(.Net/C#)