開篇:在某些場景下,我們想要對百度圖片搜出來的東東進行保存,但是一個一個得下載保存不僅耗時而且費勁,有木有一種方法能夠簡化我們的工作量呢,讓我們在離線模式下也能爽爽地瀏覽大量的美圖呢?於是,我們想到了使用網絡抓取去幫我們去下載圖片,並且保存到我們設定的文件夾中,現在我們就來看看如何來設計開發一個這樣的圖片批量下載器。
一、關於網絡抓取與爬蟲
網絡蜘蛛的主要作用是從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