看到標題可能會有人覺得似曾相識,沒錯,這篇博文的來源正是根據楊中科老師的《百度美女圖片下載器開發教程》。觀看了該教程,覺得很有意思,於是乎想自己獨立完成一次,作為對之前基礎學習內容的回顧和運用。以博文的形式和大家分享整個開發過程主要是想借此機會來重新整理下思路、鍛煉下自己的表達能力。您如果對下面要用到知識點很熟悉,可忽略此文。
主要技術
-
Winform常用控件的基本使用
-
HttpWebRequest請求其他網站內容
-
Newtonsoft.Json.dll組件解析JSON數據
-
IO文件流的讀寫操作
-
多線程和跨線程更新UI控件
-
IrisSkin4.dll組件美化皮膚
需求分析
(1)首先,我們打開百度圖片,輸入搜索關鍵字“美女”,可見眾多美女撲面而來。想要批量下載這些“美女”就必須想辦法獲取每張圖片的請求地址,但是百度服務器不會給我們提供具體的下載鏈接,我們只能嘗試去分析、去測試。在拖動下拉框的時候我們會發現一個現象,那就是圖片是實時加載顯示的,並非是當我們打開該網頁一次性加載完的。我們也就大致明白圖片是通過AJAX請求來加載的,也就是通常說的異步請求。 透過現象看本質,我們按下"F12"進入到開發者模式,來看看它的異步請求到底是怎么回事。
執行操作:按下“F12”—>重新加載頁面—>查看XHR請求—>拖動滾動條。出現下圖界面:
(2)上圖中的兩個異步請求就是在拖動滾動條時產生的,也就是用來異步加載下一頁圖片的請求地址。接下來我們具體查看其中一個請求的響應內容:
觀察得知,響應內容為JSON格式的數據,其中我們在 imgs數組節點中找到了我們想要的東西。imgs數組包含索引為0-59共60個JSON對象,經過測試對比,每個對象對應一張圖片信息,其中objURL為每張圖片真實下載地址。60個對象,也就意味着每次請求會加載60張圖片,每次請求算一頁。
(3)接下來我們對比分析上面兩個向百度服務器異步請求URL:
(1)http://image.baidu.com/search/avatarjson?tn=resultjsonavatarnew&ie=utf-8&word=%E7%BE%8E%E5%A5%B3&cg=girl&pn=60&rn=60&itg=0&z=0&fr=&width=&height=&lm=-1&ic=0&s=0&st=-1&gsm=350700003c (2)http://image.baidu.com/search/avatarjson?tn=resultjsonavatarnew&ie=utf-8&word=%E7%BE%8E%E5%A5%B3&cg=girl&pn=120&rn=60&itg=0&z=0&fr=&width=&height=&lm=-1&ic=0&s=0&st=-1&gsm=7107000078
首先,它們都是GET請求並且請求地址相同。不過跟在地址后面的個別參數值有差異:其中一個是“pn”,另外一個是“msg”。
"pn"參數:其值分別為60、120,根據上述第(2)條, 故推測"pn"為圖片的加載分頁,比如pn=120,則請求第2頁的60張圖片;
"gsm"參數:推測是標示請求,由一個包含字母和數字的10位隨機數組成;
"word"參數:"%E7%BE%8E%E5%A5%B3" 通過UrlDecode解碼后,內容為“美女”,推測"word"為搜索關鍵字。
分析到此,我們就可以去向百度服務器發送一個請求,自己來定義搜索關鍵字和圖片的加載分頁。這也是實現了這個開發過程中最關鍵一步。
小結:通過前面三個部分的敘述,我們分析了美女圖片的請求過程和響應內容。如此以來,我們開發百度美女圖片下載器就有了大致方向:模擬瀏覽器向百度服務器發送請求,解析響應任容獲取圖片地址,下載保存到本地。
解決方案
界面搭建
根據分析結果,我們從請求地址中得到了兩個可控制元素:搜索關鍵字、圖片的加載分頁。緊接着我們就可以搭建好軟件界面:
這里大致簡述下軟件的工作流程:當用戶輸入搜索關鍵字和獲取頁數,后台開啟一個新線程請求百度服務器,循環加載每個異步請求,請求成功后接着解析每個響應JSON中的imgs數組中的每個圖片真實下載地址objURL,然后再通過循環依次請求每個objURL,最終以文件流的形式保存到本地文件夾中。
核心代碼解讀
開啟一個新線程(防止界面假死)處理下載請求:
string keyWord = Uri.EscapeDataString(txtKeyWord.Text); //獲取UrlEncode編碼后的關鍵字 int pageCount = Convert.ToInt32(numPage.Value); //獲取請求頁的數量 thread = new Thread(() => { ProcessDownload(keyWord, pageCount); }); thread.Start();
HttpWebRequest向百度服務器發送請求的處理:
private void ProcessDownLoad(string pKeyWord, int pPageCount) { string url; //定義每頁需要請求的地址 for (int i = 1; i <= pPageCount; i++) { url = string.Format("http://image.baidu.com/search/avatarjson?tn=resultjsonavatarnew&ie=utf-8&word={0}&cg=girl&pn={1}&rn=60&itg=0&z=0&fr=&width=&height=&lm=-1&ic=0&s=0&st=-1&gsm={2}", pKeyWord, i * 60, GenerationRandom(10)); HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url); using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { if (response.StatusCode == HttpStatusCode.OK) { #region 響應成功 using (Stream stream = response.GetResponseStream()) { using (StreamReader reader = new StreamReader(stream)) { string responseBody = reader.ReadToEnd(); try { //去解析響應體中的圖片地址 DownloadPage(responseBody); } catch (Exception ex) { AppendLog("程序遇到未知異常,異常信息:" + ex.Message); } } } #endregion } else { AppendLog(string.Format("下載失敗,失敗信息:{0}", response.StatusCode)); } } } }
用第三方組件Newtonsoft.Json.dll解析JSON響應內容:
private void DownloadPage(string responseBody) { JObject bodyRoot = (JObject)JsonConvert.DeserializeObject(responseBody); JArray imgsRoot = (JArray)bodyRoot["imgs"]; for (int i = 0; i < imgsRoot.Count; i++) { JObject img = (JObject)imgsRoot[i]; string url = Convert.ToString(img["objURL"]); try { DownloadImage(url); //根據url下載圖片 } catch (Exception ex) { AppendLog(ex.Message); } } }
下載圖片保存到本地:
private void DownloadImage(string imgUrl) { string savePath = Path.Combine(txtSaveDir.Text, (Path.GetFileName(imgUrl))); //設置圖片的保存目錄 HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(imgUrl); request.Referer = "http://www.baidu.com/"; //偽造該請求是百度自己的請求,欺騙服務器 using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { if (response.StatusCode == HttpStatusCode.OK) { using (Stream stream = response.GetResponseStream()) { using (FileStream fsWrite = new FileStream(savePath, FileMode.Create)) { stream.CopyTo(fsWrite); } } proDownload.BeginInvoke(new Action(() => { proDownload.Value += 1; //異步更新進度條,解決跨線程更新控件的問題 })); } else { ShowMsg("圖片下載失敗,錯誤信息:" + response.StatusCode); } } }
這里只列舉了部分核心代碼,有興趣的朋友請參考完整代碼:點擊下載
皮膚美化
關於Winform窗體美化,目前大致了解是有兩種方式:第一種方式是重寫Winform本身的控件,不過這需要非常熟悉控件的各個屬性和事件並且要求具有很高的GDI繪圖技術。第二種方式是借助第三方Winform皮膚組件。畢竟修為不夠<(^-^)>,我這里選擇了第二種方式,就是借助目前流行的“IrisSkin4.dll”組件實現皮膚美化效果。
“IrisSkin4.dll”怎樣使用呢?由於我們的下載器程序只有一個窗體,所以可以使用下面的簡單方式:
- 將IrisSkin4.dll程序集放到項目文件Debug文件夾下,然后在工程中添加“引用”;
- 在窗體后置類中添加命名空間:using Sunisoft.IrisSkin;
- 在窗體類的構造函數中調用InitializeSkin方法,初始化組件。

private void InitializeSkin() { SkinEngine skinObj = new SkinEngine(); //初始化組件對象 skinObj.SkinFile = "skin/DiamondRed.ssk"; //加載皮膚文件 skinObj.SkinAllForm = true; //所有窗體都應用該皮膚 skinObj.Active = true; }
這樣,美化工作就很簡單地完成。這里提供IrisSkin4.dll的下載地址給需要的朋友:點擊下載
效果演示
反思總結
先說說這次寫的東西,功能雖然已經實現,不過存在很多可擴展性,這里我們只是抓取關鍵字為“美女”的AJAX請求地址,我們可以分析其他關鍵字的請求找到一些規律,或許可以結合正則表達式實現更多類型圖片的批量下載。這次最大的收獲是了解了HttpWebRequest類的使用,以后用來從網上抓取數據或許很好用。
看過一遍楊老師的教程,但是在自己敲代碼的過程中還是停停頓頓,順着思路寫卻總有些細節顧及不到。看來只顧着往前學是不可行的,以后要抽出時間多回顧回顧以前學的基礎。
更正:在上述分析過程中,由於自己對“pn”參數沒領悟透徹將每頁請求的url設置成了公有的。其實應該放到ProcessDownLoad方法中for循環中動態計算每頁的“pn”參數值,這樣才能實現多頁下載。完整代碼和部分博文已更正。最后,非常感謝#27樓朋友的提醒更正。