看到标题可能会有人觉得似曾相识,没错,这篇博文的来源正是根据杨中科老师的《百度美女图片下载器开发教程》。观看了该教程,觉得很有意思,于是乎想自己独立完成一次,作为对之前基础学习内容的回顾和运用。以博文的形式和大家分享整个开发过程主要是想借此机会来重新整理下思路、锻炼下自己的表达能力。您如果对下面要用到知识点很熟悉,可忽略此文。
主要技术
-
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楼朋友的提醒更正。