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

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

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

运行截图

做得简单了一点,下载开始后不能暂停,视频数目较多时运行也会较慢

用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,所以源码体积会有点大。如果重新编译失败,检查引用里有没有黄色叹号,如果有,请删掉后重新添加引用。

源代码:https://doraemonhc.github.io/assets/douyin.rar

上一篇:从CefSharp 1中的javascript调用.Net – wpf


下一篇:C# 使用CefSharp展示网站