My Github

自己动手写工具:百度图片批量下载器

开篇:在某些场景下,我们想要对百度图片搜出来的东东进行保存,但是一个一个得下载保存不仅耗时而且费劲,有木有一种方法能够简化我们的工作量呢,让我们在离线模式下也能爽爽地浏览大量的美图呢?于是,我们想到了使用网络抓取去帮我们去下载图片,并且保存到我们设定的文件夹中,现在我们就来看看如何来设计开发一个这样的图片批量下载器。

一、关于网络抓取与爬虫

  网络蜘蛛的主要作用是从Internet上不停地下载网络资源。它的基本实现思想就是通过一个或多个入口网址来获取更多的URL,然后通过对这些URL所指向的网络资源下载并分析后,再获得这些网络资源中包含的URL,以此类推,直到再没有可下的URL为止。

  网络蜘蛛的实现的一般步凑可以分为以下几步:

  (1) 指定一个(或多个)入口网址{ 如http://www.xx.com),并将这个网址加入到下载队列中(这时下载队列中只有一个或多个入口网址)}。
    (2) 负责下载网络资源的线程从下载队列中取得一个或多个URL,并将这些URL所指向的网络资源下载到本地{ 在下载之前,一般应该判断一下这个URL是否已经被下载过,如果被下载过,则忽略这个URL }。如果下载队列中没有URL,并且所有的下载线程都处于休眠状态,说明已经下载完了由入口网址所引出的所有网络资源。这时网络蜘蛛会提示下载完成,并停止下载。
    (3)分析这些下载到本地的未分析过的网络资源{ 一般为html代码 },并获得其中的URL{ 如标签<a>中href属性的值 }。
    (4)将第3步获得的URL加入到下载队列中,然后重新执行第2步。

二、关于图片批量下载器

2.1 手工下载工作量大

  在平常的使用中,我们经常会去百度图片搜索图片,然后保存到本地进行浏览或二次使用。但是,如果我们需要使用很多个同一题材的图片的时候,单个地手工去一张一张的下载保存效率就会显得很低下。这时候,我们不由得想找一个方法,让计算机帮我们去做这件事儿!

  但是,想破头颅都没想到办法。于是,我们打开F12开发者工具,发现了这么一个AJAX请求,有点意思:

  查看这个AJAX请求的HTTP报文信息,发现它返回了一大串的JSON数据,将其复制到JSON在线查看器(http://www.bejson.com/jsonview2/)中查看,原来所有的图片列表信息都在这个JSON中被返回到浏览器端。

2.2 批量下载爽爽看图

  (1)看到了上面的那个请求,我们的心中大概就有谱了。在此,我们先来对刚刚那个AJAX请求的地址来分析一下:

Request URL:http://image.baidu.com/i?tn=resultjsonavatarnew&ie=utf-8&word=%E5%AE%8B%E6%99%BA%E5%AD%9D&cg=star&pn=60&rn=60&z=&itg=0&fr=&width=&height=&lm=-1&ic=0&s=0
Request Method:GET
Status Code:200 OK

  ①这个AJAX请求首先是通过GET方式传递的,所有的参数都是通过QueryString的方式跟在URL地址后,也就是所有的参数都在后边跟着,包括我们输入的搜索词,每页的页容量(大小),当前是第几页等参数;

  ②再来看看这个请求地址后面的参数,找出我们所需要的几个重要参数。其中,word是搜索的关键词,只是后边经过了URL编码,rn是页容量(或者说是页大小,即一页有多少张图片,可以看出默认是60张图片),而pn则代表了是一共请求的图片数量,可以通过pn/rn得到当前是第几页,例如这里pn=60,rn=60,那么请求的是第一页。

  (2)现在我们来梳理一下我们这个下载器的工作流程:

  (3)下面我们来看看我们的实现后的图片下载器的样子如何:

三、关键代码实现

3.1 声明一个异步委托去执行图片下载操作,与UI线程分开防止界面卡死

            // 声明一个异步委托去处理图片下载操作
            Action downloadAction = new Action(() =>
            {
                ProcessDownload(keyword);
            });
            // 声明一个下载完成后的回调函数
            AsyncCallback callBack = new AsyncCallback(asyncResult =>
            {
                downloadAction.EndInvoke(asyncResult);
                progressBar.BeginInvoke(new Action(() =>
                {
                    progressBar.Value = progressBar.Maximum;
                }));
                txtLogs.BeginInvoke(new Action(() =>
                {
                    txtLogs.AppendText("下载图片操作结束!" + Environment.NewLine);
                }));
                btnStart.BeginInvoke(new Action(() =>
                {
                    btnStart.Enabled = true;
                }));
            });
            // 执行该异步委托
            IAsyncResult result = downloadAction.BeginInvoke(callBack, null);
            // 主线程继续干自己的事儿
            txtLogs.AppendText("正在下载图片中..." + Environment.NewLine);

  使用异步委托,关键在于设置其回调函数,这里在回调函数中结束线程操作,并通过UI控件的BeginInvoke实现安全地跨线程调用(类似于使用委托来操作)。

3.2 使用WebRequest向指定服务器端发出Http请求

        private void ProcessDownload(string keyword)
        {
            int pageCount = (int)numPageCount.Value;
            sumCount = pageCount * 60;
            for (int i = 0; i < pageCount; i++)
            {
                HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create("http://image.baidu.com/i?tn=resultjsonavatarnew&ie=utf-8&word=" + Uri.EscapeDataString(keyword) + "&pn=" + pageCount * 60 + "&cg=girl&rn=60&itg=0&lm=-1&ic=0&s=0");
                using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode == HttpStatusCode.OK)
                    {
                        using (Stream stream = response.GetResponseStream())
                        {
                            try
                            {
                                // 下载指定页的所有图片
                                DownloadPage(stream);
                            }
                            catch (Exception ex)
                            {
                                // 跨线程访问UI线程的txtLogs
                                txtLogs.BeginInvoke(new Action(() =>
                                    {
                                        txtLogs.AppendText(ex.Message + Environment.NewLine);
                                    }));
                            }
                        }
                    }
                    else
                    {
                        MessageBox.Show("获取第" + pageCount + "页失败:" + response.StatusCode);
                    }
                }
            }
        }

  这里使用了try..catch将下载时碰到的异常信息填充到了TextBox文本框中。

3.3 使用第三方JSON组件解析JSON数据

        private void DownloadPage(Stream stream)
        {
            using (StreamReader reader = new StreamReader(stream))
            {
                string jsonData = reader.ReadToEnd();
                // 解析JSON,分析JSON
                JObject objectRoot = JsonConvert.DeserializeObject(jsonData) as JObject;
                JArray imgsArray = objectRoot["imgs"] as JArray;
                for (int i = 0; i < imgsArray.Count; i++)
                {
                    JObject img = imgsArray[i] as JObject;
                    string objUrl = (string)img["objURL"];
                    //txtLogs.AppendText(objUrl + Environment.NewLine); // 测试获取图片路径
                    try
                    {
                        // 下载具体的某一张图片
                        DownloadImage(objUrl);
                        // 更新进度条
                        progressBar.BeginInvoke(new Action(() =>
                            {
                                progressBar.Value = i * 100 / sumCount;
                            }));
                        // 更新文本框
                        txtLogs.BeginInvoke(new Action(() =>
                            {
                                txtLogs.AppendText("已下载:" + objUrl + Environment.NewLine);
                            }));
                    }
                    catch (Exception ex)
                    {
                        // 跨线程访问UI线程的txtLogs控件
                        txtLogs.BeginInvoke(new Action(() =>
                            {
                                txtLogs.AppendText("【异常:" + ex.Message + "" + Environment.NewLine);
                            }));
                    }
                }
            }
        }

  这里使用的是Newtonsoft.Json组件,在返回的JSON数据中,找到imgs集合,对其进行遍历,找出其中的objURL并一一地进行下载到本地。

3.4 伪造URLRerfer并使用FileStream将其保存到本地

        private void DownloadImage(string objUrl)
        {
            string destFileName = Path.Combine(destDir, Path.GetFileName(objUrl));
            HttpWebRequest request =
                (HttpWebRequest)HttpWebRequest.Create(objUrl);
            // 欺骗服务器判断URLReferer
            request.Referer = "http://image.baidu.com";
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                if (response.StatusCode == HttpStatusCode.OK)
                {
                    using (Stream stream = response.GetResponseStream())
                    {
                        using (FileStream fileStream = new FileStream(destFileName, FileMode.Create))
                        {
                            stream.CopyTo(fileStream);
                        }
                    }
                }
                else
                {
                    throw new Exception("下载" + objUrl + "失败,错误码:" + response.StatusCode);
                }
            }
        }

  这里通过在客户端伪造URLRerfer让服务器端误以为是自己的站内请求(伪造我们的请求不是骗它流量的),然后通过FileStream将返回的图片响应流保存到指定的文件夹中。

四、个人开发小结

4.1 运行结果演示

  这里我们批量下载一页(60张)的美女图片到指定的文件夹中,看看下载器是否真的帮助我们下载了图片:

  (1)程序的运行过程:

  (2)下载后的图片文件夹:

4.2 更改搜索名词

  这里我们将“美女”改为了“宋智孝”后,发现下载器未能成功下载图片。经过分析,原来百度图片搜索中,每个搜索词所生成的AJAX请求都不同,因此本下载器目前不具有通用性,也就是说每次更换搜索词都需要改代码,主要是改HttpWebRequest那的URL地址。

  (1)更改URL处的代码:

  (2)程序的运行过程:

  (3)下载的图片文件:

4.3 不是小结的小结

  本次我们实现了一个小工具,它可以帮我们下载我们想要搜索的图片到执行的图片文件夹中,让我们可以离线爽爽地看美图。设计开发这样一个工具,最重要的莫过于:分析Http报文、解析返回数据、线程创建与同步、异步操作、文件流、进度条的更新(跨线程的调用)等等,本次开发中都多多少少涉及到了其中的一些东东。当然,不足之处还有很多,例如工具的通用性不足,每次更换搜索词都需要更改代码,可配置型不高等等。这里提供一个我的代码实现DEMO,有兴趣的朋友也可以自行修改并进行扩展。

参考资料

  (1)杨中科,《自己动手写美女图片下载器》:http://www.rupeng.com/Courses/Index/14

  (2)冰封的心,《C#2.0实现抓取网络资源的网络蜘蛛》:http://www.cnblogs.com/yibinboy/articles/1236356.html

附件下载

  MyPictureDownloader v1.0:https://github.com/EdisonChou/EDC.MyPictureDownloader.Sample  

 

posted @ 2014-12-22 22:52  EdisonZhou  阅读(10523)  评论(24编辑  收藏  举报