开发者社区> 余二五> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

网络采集软件核心技术剖析系列(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
            {
            }
        }









本文转自 xchsp 51CTO博客,原文链接:http://blog.51cto.com/freebird/1586526,如需转载请自行联系原作者

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Linux下通过ioctl系统调用来获取和设置网络信息
#include  #include  #include  #include  #include  #include  #include  #include  #include   // 获取IP地址,子网掩码,MAC地址 int GetLocalNetInfo(     const char* l...
1094 0
linux下网络排错与查看
基本的故障排除错误 故障的排除一定是先简单后复杂的,有的人把上述的文件反复配置,就是上不了网,一直都认为是系统出了故障,想重装机子。结果发现原来是网线压根就没插上。 排错要慢慢的按部就班的来: (1)首先看网线是否插好,灯亮就是OK的。
1248 0
linux下判断网络是否连接
本文改写自网上的一个程序,原始程序中为阻塞式调用,而且有现成创建的过程,非常不利于集成到自己程序中,因此对原始程序进行改造,使其可以完成发送一个imcp包的方式来判断网络连通,只需要调用改进后的  bool NetIsOK() 函数即可,该函数返回true即表示网络状态良好,否则表示网络状态不连同,本程序中只发送了一个icmp包,在实际应用中可以根据需要改进为发送多个imcp包。
1482 0
+关注
文章
问答
文章排行榜
最热
最新
相关电子书
更多
《云网络概念手册》
立即下载
实时数据分析演示
立即下载
如何个人网站全过...[云我客].1512036196.pdf
立即下载