網絡採集軟件核心技術剖析系列(1)---如何使用C#語言獲取博客園某個博主的全部隨筆鏈接及標題

一 本系列隨筆概覽及產生的背景

自己開發的豆約翰博客備份專家軟件工具問世3年多以來,深受廣大博客寫作和閱讀愛好者的喜愛。同時也不乏一些技術愛好者諮詢我,這個軟件裏面各種實用的功能是如何實現的。

該軟件使用.NET技術開發,爲回饋社區,現將該軟件中用到的核心技術,開闢一個專欄,寫一個系列文章,以饗廣大技術愛好者。

本系列文章除了講解網絡採編發用到的各種重要技術之外,也提供了不少問題的解決思路和界面開發的編程經驗,非常適合.NET開發的初級,中級讀者,希望大家多多支持。

很多初學者常有此類困惑,“爲什麼我書也看了,C#相關的各個方面的知識都有所瞭解,但就是沒法寫出一個像樣的應用呢?”,

這其實還是沒有學會綜合運用所學知識,鍛煉出編程思維,建立起學習興趣,我想該系列文章也許會幫到您,但願如此。

開發環境:VS2008

源碼位置:https://github.com/songboriceboy/NetworkGatherEditPublish

源碼下載辦法:安裝SVN客戶端(本文最後提供下載地址),然後checkout以下的地址:https://github.com/songboriceboy/NetworkGatherEditPublish

系列文章提綱擬定如下:

1.如何使用C#語言獲取博客園某個博主的全部隨筆鏈接及標題;
2.如何使用C#語言獲得博文的內容;
3.使用C#語言如何將html網頁轉換成pdf(html2pdf)
4.如何使用C#語言下載博文中的全部圖片到本地並可以離線瀏覽
5.如何使用C#語言合成多個單個的pdf文件到一個pdf中,並生成目錄
6.網易博客的鏈接如何使用C#語言獲取到,網易博客的特殊性;
7.微信公衆號文章如何使用C#語言下載;
8.如何獲取任意一篇文章的全部圖文
9.如何使用C#語言去掉html中的全部標籤獲取純文本(html2txt)
10.如何使用C#語言將多個html文件編譯成chm(html2chm)
11.如何使用C#語言遠程發佈文章到新浪博客
12.如何使用C#語言開發靜態站點生成器
13.如何使用C#語言搭建程序框架(經典Winform界面,頂部菜單欄,工具欄,左邊樹形列表,右邊多Tab界面)
14.如何使用C#語言實現網頁編輯器(Winform)

......

二 第一節主要內容簡介(如何使用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/
本文版權歸作者和51CTO共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
正在看本人博客的這位童鞋,我看你氣度不凡,談吐間隱隱有王者之氣,日後必有一番作爲!旁邊有&ldquo;推薦&rdquo;二字,你就順手把它點了吧,相得準,我分文不收;相不準,你也好回來找我!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章