一 本系列隨筆概覽及產生的背景
自己開發的豆約翰博客備份專家軟件工具問世3年多以來,深受廣大博客寫作和閱讀愛好者的喜愛。同時也不乏一些技術愛好者咨詢我,這個軟件里面各種實用的功能是如何實現的。
該軟件使用.NET技術開發,為回饋社區,現將該軟件中用到的核心技術,開辟一個專欄,寫一個系列文章,以饗廣大技術愛好者。
本系列文章除了講解網絡采編發用到的各種重要技術之外,也提供了不少問題的解決思路和界面開發的編程經驗,非常適合.NET開發的初級,中級讀者,希望大家多多支持。
很多初學者常有此類困惑,“為什么我書也看了,C#相關的各個方面的知識都有所了解,但就是沒法寫出一個像樣的應用呢?”,
這其實還是沒有學會綜合運用所學知識,鍛煉出編程思維,建立起學習興趣,我想該系列文章也許會幫到您,但願如此。
開發環境:VS2008
源碼位置:https://github.com/songboriceboy/NetworkGatherEditPublish
源碼下載辦法:安裝SVN客戶端(本文最后提供下載地址),然后checkout以下的地址:https://github.com/songboriceboy/NetworkGatherEditPublish
系列文章提綱如下:
二 第一節主要內容簡介(如何使用C#語言獲取博客園某個博主的全部隨筆鏈接及標題)
獲取某個博主的全部博文鏈接及標題的解決方案,演示demo如下圖所示:可執行文件下載
三 基本原理
要想采集的某個博主的全部博文網頁地址,需要分2步:
1.通過分頁鏈接獲取到網頁源代碼;
2.從獲取到的網頁源代碼中解析出文章地址和標題;
第一步,首先找到分頁鏈接,比如我的博客
第一頁 http://www.cnblogs.com/ice-river/default.html?page=1
第二頁 http://www.cnblogs.com/ice-river/default.html?page=2
我們可以寫個函數把這些分頁地址字符串保存至一個隊列中,如下代碼所示,
下面的代碼中,我們默認保存了500頁,500頁*20篇=10000篇博文,一般夠用了,除非對於特別高產的博主。
還有一點,有心的朋友們可能會問,500頁是不是太多了,有的博主只有2,3頁,我們有必要去采集500個分頁來獲取全部博文鏈接么?
這里因為我們不知道某個博主到底寫了多少篇博文(分成幾頁),所以,我們先默認取500頁
,后面會講到一種判斷已經獲取到全部文章鏈接的辦法,其實我們並不會每個博主都訪問500個分頁。
protected void GatherInitCnblogsFirstUrls() { string strPagePre = "http://www.cnblogs.com/"; string strPagePost = "/default.html?page={0}&OnlyTitle=1"; string strPage = strPagePre + this.txtBoxCnblogsBlogID.Text + strPagePost; for (int i = 500; i > 0; i--) { string strTemp = string.Format(strPage, i); m_wd.AddUrlQueue(strTemp); } }
至於獲取某個網頁的源文件(就是你在瀏覽器中,對某個網頁右鍵---查看網頁源代碼功能)
C#語言已經為我們提供了一個現成的HttpWebRequest類,我將其封裝成了一個WebDownloader類,具體細節大家可以參考源代碼,主要函數實現如下:
public string GetPageByHttpWebRequest(string url, Encoding encoding, string strRefer) { string result = null; WebResponse response = null; StreamReader reader = null; try { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)"; request.Accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*"; if (!string.IsNullOrEmpty(strRefer)) { Uri u = new Uri(strRefer); request.Referer = u.Host; } else { request.Referer = strRefer; } request.Method = "GET"; response = request.GetResponse(); reader = new StreamReader(response.GetResponseStream(), encoding); result = reader.ReadToEnd(); } catch (Exception ex) { result = ""; } finally { if (reader != null) reader.Close(); if (response != null) response.Close(); } return result; }
第一個參數傳入的就是,我們上面形成的500個分頁地址,函數的返回值就是網頁的源代碼(我們想要的文章地址和標題就在其中,接下來我們要把它們解析出來)。
第二步:從獲取到的網頁源代碼中解析出文章地址和標題
我們要利用大名鼎鼎的HtmlAgilityPack類庫,HtmlAgilityPack是一個HTML文檔的解析利器,通過它我們可以方便的獲得網頁的標題,正文,分類,日期等等,理論上任何元素,相關的文檔網上有很多,這里就不多說了。這里我們給HtmlAgilityPack增加了一個擴展方法以提取出任意網頁源文件的全部超級鏈接GetReferences和鏈接對應的文本GetReferencesText。
private void GetReferences() { HtmlNodeCollection hrefs = m_Doc.DocumentNode.SelectNodes("//a[@href]"); if (Equals(hrefs, null)) { References = new string[0]; return; } References = hrefs. Select(href => href.Attributes["href"].Value). Distinct(). ToArray(); }
private void GetReferencesText() { try { m_dicLink2Text.Clear(); HtmlNodeCollection hrefs = m_Doc.DocumentNode.SelectNodes("//a[@href]"); if (Equals(hrefs, null)) { return; } foreach (HtmlNode node in hrefs) { if (!m_dicLink2Text.Keys.Contains(node.Attributes["href"].Value.ToString())) if(!HttpUtility.HtmlDecode(node.InnerHtml).Contains("img src") && !HttpUtility.HtmlDecode(node.InnerHtml).Contains("img ") && !HttpUtility.HtmlDecode(node.InnerHtml).Contains(" src")) m_dicLink2Text.Add(node.Attributes["href"].Value.ToString(), HttpUtility.HtmlDecode(node.InnerHtml)); } int a = 0; } catch (System.Exception e) { System.Console.WriteLine(e.ToString()); } }
但是注意到,到此為止我們是獲取到了某個網頁中的全部鏈接地址,這其實距離我們想要的還差點,所以我們需要在這些鏈接地址集合中過濾出我們真正想要的博文地址。
這時我們需要用到強大的正則表達式工具,同樣C#中提供了現成的支持類,但是需要我們對正則表達式有所了解,這里就不講解正則表達式的相關知識了,不懂的請自行百度之。
首先我們需要觀察博文鏈接地址的格式:
隨便找幾篇博文:
http://www.cnblogs.com/ice-river/p/3475041.html
http://www.cnblogs.com/zhijianliutang/p/4042770.html
我們發現鏈接和博主ID有關,所以博主ID我們需要有個變量( this.txtBoxCnblogsBlogID.Text)進行記錄,
上面的鏈接模式用正則表達式可以表示如下:
"www\.cnblogs\.com/" + this.txtBoxCnblogsBlogID.Text + "/p/.*?\.html$";
簡單解釋一下:\代表轉義,因為.在正則表達式中有重要含義;$代表結尾,html$的意思就是以html結尾。.*?是什么,很重要且不太好理解
正則有兩種模式,一種為貪婪模式(默認),另外一種為懶惰模式,以下為例: (abc)dfe(gh) 對上面這個字符串使用(.*)將會匹配整個字符串,因為正則默認是盡可能多的匹配。 雖然(abc)滿足我們的表達式,但是(abc)dfe(gh)也同樣滿足,所以正則會匹配多的那個。 如果我們只想匹配(abc)和(gh)就需要用到以下的表達式 (.*?) 在重復元字符*或者+后面跟一個?,作用就是在滿足的條件下盡可能少匹配。
所以,上面的正則表達式的意思就是“含有www.cnblogs.com/接着博主ID然后再接着/p/然后再接着任意多個字符直到遇到html結尾為止”。
然后,我們就可以通過C#代碼來過濾符合這個模式的全部鏈接了,主要代碼如下:
MatchCollection matchs = Regex.Matches(normalizedLink, m_strCnblogsUrlFilterRule, RegexOptions.Singleline); if (matchs.Count > 0) { string strLinkText = ""; if (links.m_dicLink2Text.Keys.Contains(normalizedLink)) strLinkText = links.m_dicLink2Text[normalizedLink]; if (strLinkText == "") { if (links.m_dicLink2Text.Keys.Contains(link)) strLinkText = links.m_dicLink2Text[link].TrimEnd().TrimStart(); } PrintLog(strLinkText + "\n"); PrintLog(normalizedLink + "\n"); lstThisTimesUrls.Add(normalizedLink); }
判斷全部文章鏈接獲取完成:之前,我們是計划采集500個分頁地址,但是有可能該博主的全部博文只有幾頁,那么我們該如何判斷全部文章都下載完成了呢?
辦法其實很簡單,就是我們使用2個集合,一個是當前下載的全部文章集合,一個是本次下載到的文章集合,如果本次下載的全部文章,之前下載的全部集合中都有了,那么說明全部文章都下載完成了。
程序中,我將這個判斷封裝成了一個函數,代碼如下:
private bool CheckArticles(List<string> lstThisTimesUrls) { bool bRet = true; foreach (string strTemp in lstThisTimesUrls) { if (!m_lstUrls.Contains(strTemp)) { bRet = false; break; } } foreach (string strTemp in lstThisTimesUrls) { if (!m_lstUrls.Contains(strTemp)) m_lstUrls.Add(strTemp); } return bRet; }
四 其他比較重要的知識
1.BackgroundWorker工作者線程的使用,因為我們的采集任務是一個比較耗時的工作,所以我們不應該放到界面主線程去做,我們應該啟動一個后台線程,c#中最方便的后台線程使用方法就是利用BackgroundWorker類。
2.由於我們需要在解析出每一篇文章的地址及標題后,在界面上打印出來,同時因為我們不能在工作者線程中去修改界面控件,所以這里我們需要使用C#中的代理delegate技術,通過回調的方式來實現在界面上輸出信息。
TaskDelegate deles = new TaskDelegate(new ccTaskDelegate(RefreshTask)); public void RefreshTask(DelegatePara dp) { //如果需要在安全的線程上下文中執行 if (this.InvokeRequired) { this.Invoke(new ccTaskDelegate(RefreshTask), dp); return; } //轉換參數 string strLog = (string)dp.strLog; WriteLog(strLog); } protected void PrintLog(string strLog) { DelegatePara dp = new DelegatePara(); dp.strLog = strLog; deles.Refresh(dp); } public void WriteLog(string strLog) { try { strLog = System.DateTime.Now.ToLongTimeString() + " : " + strLog; this.richTextBoxLog.AppendText(strLog); this.richTextBoxLog.SelectionStart = int.MaxValue; this.richTextBoxLog.ScrollToCaret(); } catch { } }
出處: http://www.cnblogs.com/ice-river/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
正在看本人博客的這位童鞋,我看你氣度不凡,談吐間隱隱有王者之氣,日后必有一番作為!旁邊有“推薦”二字,你就順手把它點了吧,相得准,我分文不收;相不准,你也好回來找我!