网络采集软件核心技术剖析系列(1)---如何使用C#语言获取博客园某个博主的全部随笔链接及标题

一 本系列随笔概览及产生的背景

自己开发的豆约翰博客备份专家软件工具问世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/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
正在看本人博客的这位童鞋,我看你气度不凡,谈吐间隐隐有王者之气,日后必有一番作为!旁边有“推荐”二字,你就顺手把它点了吧,相得准,我分文不收;相不准,你也好回来找我!
posted @ 2014-11-20 17:18  际为软件事务所  阅读(5498)  评论(35编辑  收藏  举报