【澳门新葡新京】NET备份博客园随笔分类文章,Dapper走一波爬虫

(转载请注明来源:

之前用.NET做网页采集实现采用正则表达式去匹配解析,比较繁琐,花费时间较多,若是Html复杂的话真是欲哭无泪。

    最近因为公司业务需要,又有机会撸winform了,这次的需求是因为公司有项目申报的这块业务,项目申报前期需要关注政府发布的相关动态信息,政府部门网站过多,人工需要一个一个网站去浏览和查阅,有时候还会遗漏掉,因此呢,我们打算用爬虫+移动端web来做,我主要负责爬虫和web
Api。

很早就听过包HtmlAgilityPack,其是在.NET下用XPath来解析的HTML的一个类库(包)。但是一直没时间尝试,简单了解了下HtmlAgilityPack的API后,发现真是HTML解析利器,于是花些时间做一个例子记录下。

爬虫篇

 

    爬虫主要采用.Net强大的开源解析HTML元素的类库HtmlAgilityPack,操作过XML的童鞋应该很快就可以上手,通过分析XPath来解析HTML,非常的方便的,还有一款不错的叫Jumony,没用过,对HtmlAgilityPack比较熟悉,所以首选了HtmlAgilityPack来作为主力军。

本次是以下载博客园随笔分类文章为例,采用两部分实现,第一部分是将采集到的文章放到集合变量中,第二部分是通过操作集合变量将文章下载到本地,

    HtmlAgilityPack的基本使用可以参考这篇
《开源项目Html Agility
Pack实现快速解析Html》。

这样做效率较低,因为可以直接边采集文章边下载。暂时没有考虑效率问题,仅仅只是实现功能。下面简单阐述下。

    效果图,多图慎入:

 

澳门新葡新京 1澳门新葡新京 2

获取随笔分类

 澳门新葡新京 3

 

 

根据输入的博客名取得对应的随笔分类。

 

 

采集广西财政厅例子

澳门新葡新京 4澳门新葡新京 5

  澳门新葡新京 6

   /// <summary>

        /// 获取博客分类

        /// </summary>

        /// <param name=" uname"></param>

        /// <returns></returns>

        private static List< BlogType> GettBlogTypeList(string uname)

        {

            string url = "http://www.cnblogs.com/" + uname + "/mvc/blog/sidecolumn.aspx?blogApp=" + uname;

            string htmlStr = CommonHelper .GetRequestStr(url);

            HtmlDocument doc = new HtmlDocument();

            doc.LoadHtml(htmlStr);

            var nodes = doc.DocumentNode.SelectNodes("//div[@id='sidebar_postcategory']//a"); //随笔分类

            if (nodes == null || nodes.Count <= 0)

                return null ;

            List<BlogType > list = new List< BlogType>();

            for (int i = 0; i < nodes.Count; i++)

            {

                var aUrl = nodes[i].Attributes["href" ].Value;

                var name = nodes[i].InnerText;

                list.Add( new BlogType () { BlogTypeUrl = aUrl, BlogTypeName = name.Contains( "(") ? name.Split('(')[0] : name,BlogTypeNameShow=name });

            }

            return list;

        }



  public class BlogType

    {

        public string BlogTypeUrl { get; set; }

        public string BlogTypeName { get; set; }

        public string BlogTypeNameShow { get; set; }

    }

     因为是政府发布的出来的信息,所以信息的对外开放的,只是机器代替人工来浏览,不会被和谐的,主要采集文章的标题、日期和文章内容,以广西财政厅网站为例子。

View Code

 

澳门新葡新京, 

First

 如获取到的随笔分类如下:

    加载网站这个就不用说了,先查看网站的字符编码,如图<meta http-equiv=”Content-Type”
content=”text/html; charset=utf-8″ />
,然后设置HtmlAgilityPack中的OverrideEncoding属性,再开始加载,不然采集到的是乱码,没有意义。

 

澳门新葡新京 7

澳门新葡新京 8

 

 

htmlAgilityPack.OverrideEncoding = Encoding.UTF8;

采集分类的文章

 

 

Second

采用两步实现,第一步获取只包含标题和url的文章,第二步再获取文章内容。

  澳门新葡新京 9

     

 

澳门新葡新京 10澳门新葡新京 11

    分析文章列表,浏览器F12查看HTML标签情况,可以分析出XPath为:

 /// <summary>

        /// 根据分类获取博客

        /// </summary>

        /// <param name=" blogTypes"></param>

        /// <param name=" useTime"></param>

        /// <returns></returns>

        public static Dictionary< BlogType,List <BlogInfo>> GetBlogsByType( List<BlogType > blogTypes,out long useTime)

        {

            Stopwatch sw = new Stopwatch();

            sw.Start();

            Dictionary<BlogType , List< BlogInfo>> dic = new Dictionary< BlogType, List <BlogInfo>>();          

            foreach (var blogType in blogTypes)

            {

                List<BlogInfo > list = new List< BlogInfo>();

                HtmlDocument doc = new HtmlDocument();

                doc.LoadHtml( CommonHelper.GetRequestStr(blogType.BlogTypeUrl));

                var typeNameNode = doc.DocumentNode.SelectSingleNode("//div[@class='entrylist']/h1");

                string typeName = typeNameNode.InnerText;

                var listPosttitleNodes = doc.DocumentNode.SelectNodes("//div[@class='entrylistPosttitle']/a");

                if (listPosttitleNodes != null && listPosttitleNodes.Count > 0)

                {

                    for (int i = 0; i < listPosttitleNodes.Count; i++)

                    {

                        Console.WriteLine("正在爬取文章【{0}】..." , listPosttitleNodes[i].InnerText);

                        list.Add( new BlogInfo ()

                        {

                            BlogUrl = listPosttitleNodes[i].Attributes[ "href"].Value,

                            BlogTitle = listPosttitleNodes[i].InnerText,

                            BlogTypeName = typeName

                        });

                    }

                }

                dic.Add(blogType,list);

            }

            sw.Stop();

            useTime = sw.ElapsedMilliseconds;

            return dic;

        }





     /// <summary>

        /// 获取详细的博客信息

        /// </summary>

        /// <param name=" dic"></param>

        /// <param name=" useTime"></param>

        /// <returns></returns>

        public static Dictionary< BlogType, List <BlogInfo>> GetBlogDetail( Dictionary<BlogType , List<BlogInfo >> dic, out long useTime)

        {

            Stopwatch sw = new Stopwatch();

            sw.Start();

            HtmlDocument doc = new HtmlDocument();

            for(int k=0;k<dic.Keys.Count;k++)

            {

                var blogType = dic.Keys.ElementAt(k);

                var list = dic[blogType];

                for (int i = 0; i < list.Count; i++)

                {

                    Console.WriteLine("正在获取文章【{0}】内容..." , list[i].BlogTitle);

                    doc.LoadHtml( CommonHelper.GetRequestStr(list[i].BlogUrl));

                    var bodyNode = doc.DocumentNode.SelectSingleNode("//div[@id='cnblogs_post_body']");

                    var dateNode = doc.DocumentNode.SelectSingleNode("//span[@id='post-date']");

                    var userNode = doc.DocumentNode.SelectSingleNode("//div[@class='postDesc']/a[1]");

                    list[i].BlogContent = bodyNode == null ? "内容获取失败" : bodyNode.InnerHtml;

                    list[i].BlogPostTime = dateNode == null ? "发布时间获取失败" : dateNode.InnerText;

                    list[i].BlogName = userNode == null ? "用户获取失败" : userNode.InnerText;

                }

                dic[blogType] = list;

            }

            sw.Stop();

            useTime = sw.ElapsedMilliseconds;

            return dic;

        }



    public class BlogInfo

    {

        public string BlogUrl { get; set; }

        public string BlogName { get; set; }

        public string BlogTitle { get; set; }

        public string BlogContent { get; set; }

        public string BlogTypeName { get; set; }

        public string BlogPostTime { get; set; }

    }
//ul[@class='dzjzw_list_main_ul']//li

View Code

     文章内容的链接的XPath标签:

 

//a

 下载到本地

    文章发布的时间XPath标签:

 

//span[@class='date']

根据上面采集到的文章再一步步下载到本地,期间分两步,第一步下载图片,第二步下载文章内容。

 

 

示例流程代码:

澳门新葡新京 12澳门新葡新京 13

//获取第一页的内容
HtmlNode  row = GetHtmlDoc(htmlWeb, url);
//根据xpath获取列表
var list = row.SelectNodes("//ul[@class='dzjzw_list_main_ul']//li");
 foreach (var data in list)
{
      HtmlNode node = HtmlNode.CreateNode(data.OuterHtml);
      HtmlNode a = node.SelectSingleNode("//a");
      HtmlNode date = node.SelectSingleNode("//span['date']");
     ....
}

/// <summary>
/// 这里偶尔会浏览网页失败的,所以失败了多浏览几次
/// </summary
public static HtmlNode GetHtmlDoc(HtmlWeb htmlWeb, string url)
{
            try
            {
                var doc = GetDoc(htmlWeb, url);
                if (doc == null)
                {
                    int againIdx = 0;
                    while (againIdx++ < 5)
                    {
                        System.Threading.Thread.Sleep(1000);
                        doc = GetDoc(htmlWeb, url);
                        if (doc != null)
                            break;
                    }
                    if (doc == null)
                    {
                        var htmlData = HttpHelper.Get<string>(url).Result;//.GetStringAsync(url).Result;
                        return HtmlNode.CreateNode(htmlData);
                    }
                    else
                    {
                        return doc.DocumentNode;
                    }
                }
                return doc.DocumentNode;
            }
            catch
            {
                Log.Error("未能正确访问地址:" + url);
                return null;
            }
}

/// <summary>
/// 加载网页
/// </summary>
public static HtmlDocument GetDoc(HtmlWeb htmlWeb, string url)
{
            try
            {
                return htmlWeb.Load(url);
            }
            catch (Exception ex)
            {
                return null;
            }
}
 /// <summary>

        /// 下载

        /// </summary>

        /// <param name=" dic"></param>

        /// <param name=" uname"></param>

        /// <param name=" useTime"></param>

        /// <returns></returns>

        public static string DowanloadBlog( Dictionary<BlogType , List< BlogInfo>> dic, string uname,out long useTime)

        {

            Stopwatch sw = new Stopwatch();

            sw.Start();

            int countFlag = 0;

            for (int i = 0; i < dic.Keys.Count; i++)

            {

                var blogType = dic.Keys.ElementAt(i);

                var blogList = dic[blogType];

                var dicPath = AppDomain .CurrentDomain.BaseDirectory +"BlogFiles\\" + uname + "\\" + blogType.BlogTypeName;

                Console.WriteLine("<<开始处理分类【{0}】<<" , blogType.BlogTypeName);

                FileHelper.CreatePath(dicPath);

                var blogModel = new BlogInfo();

                for (int j = 0; j < blogList.Count; j++)

                {

                    countFlag++;

                    try

                    {

                        Console.WriteLine("~~~~开始处理文章{0}【{1}】~~~~" , countFlag,blogModel.BlogTitle);

                        blogModel = blogList[j];

                        var filePath = dicPath + "\\" + FileHelper.FilterInvalidChar(blogModel.BlogTitle, "_") + ".html" ;

                        HtmlDocument doc = new HtmlDocument();

                        doc.DocumentNode.InnerHtml = blogModel.BlogContent;



                        //处理图片

                        Console.WriteLine("~~开始处理图片" );

                        var imgPath = dicPath + "\\images" ;

                        FileHelper.CreatePath(imgPath);

                        SaveImage(doc, imgPath);

                        Console.WriteLine("~~处理图片完成" );



                        //去掉a标签

                        var aNodes = doc.DocumentNode.SelectNodes("//a");

                        if (aNodes != null && aNodes.Count > 0)

                        {

                            for (int a = 0; a < aNodes.Count; a++)

                            {

                                if (aNodes[a].Attributes["href" ] != null && aNodes[a].Attributes[ "href"].Value != "#" )

                                {

                                    aNodes[a].Attributes[ "href"].Value = "javascript:void()" ;

                                }

                            }

                        }

                        doc.DocumentNode.InnerHtml = "<div id='div_head'>" + uname + " " + blogType.BlogTypeName + "</div><div id='div_title'>" + blogModel.BlogTitle + "<div><div id='div_body'>" + doc.DocumentNode.InnerHtml + "</div>";

                        doc.Save(filePath, Encoding.UTF8);

                        Console.WriteLine("~~~~处理文章{0}【{1}】完毕~~~~" ,countFlag,blogModel.BlogTitle);

                    }

                    catch (Exception ex)

                    {

                        string errorMsg = DateTime .Now.ToString("yyyyMMdd HH:mm:ss") + "\r\n" + "url=" + blogModel.BlogUrl + "\r\n" + "title=" + blogModel.BlogTitle + "\r\n" + "errorMsg=" + ex.Message + "\r\n" + "stackTrace=" + ex.StackTrace + "\r\n\r\n\r\n";

                        Console.WriteLine("error>>处理文章【{0}】出现错误,开始记录错误信息~~" , blogModel.BlogTitle);

                        FileHelper.SaveTxtFile(dicPath, "errorLog.txt" , errorMsg, false);

                        Console.WriteLine("error>>处理文章【{0}】出现错误,记录错误信息完成~~" , blogModel.BlogTitle);

                    }

                }

                Console.WriteLine("<<处理分类【{0}】完成<<" , blogType.BlogTypeName);



            }

            sw.Start();

            useTime = sw.ElapsedMilliseconds;

            return AppDomain .CurrentDomain.BaseDirectory + "BlogFiles\\" + uname;

        }



 /// <summary>

        /// 保存图片

        /// </summary>

        /// <param name=" doc"></param>

        /// <param name=" filePath"></param>

        public static void SaveImage( HtmlDocument doc, string filePath)

        {

            var imgNodes = doc.DocumentNode.SelectNodes("//img");

            if (imgNodes != null && imgNodes.Count > 0)

            {

                for (int i = 0; i < imgNodes.Count; i++)

                {

                    try

                    {                     

                        string src = imgNodes[i].Attributes["src" ].Value;

                        string fileName = "" ;

                        if (src != null && src.Contains("/"))

                        {

                            fileName = src.Substring(src.LastIndexOf( "/") + 1);

                            Console.WriteLine("~~开始下载图片【{0}】~~" , fileName);

                            string imgPath = filePath + "\\" + fileName;

                            imgNodes[i].Attributes[ "src"].Value = imgPath;

                            byte[] imgByte = CommonHelper .GetRequestByteArr(src);

                            if (imgByte != null )

                            {

                                FileHelper.SaveImage(imgPath, imgByte);

                                Console.WriteLine("~~下载图片【{0}】完成~~" , fileName);

                            }

                            else

                            {

                                Console.WriteLine("~~下载图片【{0}】失败~~" , fileName);

                            }

                        }

                    }

                    catch (Exception ex)

                    {

                        throw new Exception( "SaveImage_Error:" + ex.Message);

                    }



                }

            }

        }

都可以使用 HtmlNode.InnerText
来获取到相关值,非常的方便。

View Code

 

 

Third

程序入口

    文章详细内容也如此,通过分析XPath来分析即可,最头疼的是翻页的问题,因为政府网站使用的技术一般都是比较那个的,你懂的,有些使用到oncilck来触发的,有些表单提交,要具体问题具体分析了,用Fiddler和浏览器的F12大法来分析翻页数据来源,在这里的例子翻页也比较简单,通过拼接URL来进行翻页即可。

 

澳门新葡新京 14

主要代码如下

 

                

 

澳门新葡新京 15澳门新葡新京 16

Fourth

    var types = GettBlogTypeList(uname);

                    long time1 = 0;

                    long time2 = 0;

                    long timeDownload = 0;

                    Console.WriteLine("正在爬取,请耐心等待..." );

                    var blogList = GetBlogsByType(types,out time1);

                    var blogDetailList = GetBlogDetail(blogList,out time2);

                    Console.WriteLine("爬取完毕,开始下载..." );

                    string filePath=DowanloadBlog(blogDetailList, uname,out timeDownload);

                    Console.WriteLine("**处理完毕,爬取用时{0}ms,下载用时{1}ms,{2}" , time1+time2, timeDownload, filePath);

                    handlerRight = false;

    爬取到的之后,再来一个钉钉通知,在群里拉入一个机器人,可以参考钉钉的开发文档。

View Code

澳门新葡新京 17澳门新葡新京 18

 

澳门新葡新京 19

演示效果

  

 

    这样我们爬取的消息就第一时间通知到群里的小伙伴啦,是不是很炫酷,哈哈哈。 

 

    项目demo已经上传,

澳门新葡新京 20

    码云:

 

    Git:

文件存储在项目bin目录下,一个用户一个文件夹

澳门新葡新京 21

 澳门新葡新京 22

 澳门新葡新京 23

澳门新葡新京 24.png)

 

按随笔分类生成不同的文件夹

Last

 

    评论区有提问说部分网站是动态渲染数据的,用XPath分析不到结果,如果数据不是来源于当前界面的HTML,那XPath是分析不到,这时候你需要看它的数据源来自哪里,用Fiddler或者浏览器F12抓一下数据源,类似这个网站(http://www.nnhrss.gov.cn/ecdomain/framework/nnrsw/djiaakgphfalbboelieaiobfgheoldlc.jsp),

 澳门新葡新京 25

澳门新葡新京 26

 

 

生成.html文件,一个分类的所有图片都存在该分类下的images下。

查看好数据源,分析出它的数据源来自这个:
(http://www.nnhrss.gov.cn/ecdomain/portal/portlets/newslist/newslistcomponent.jsp?goPage=1&pageNum=1&siteID=nnrsw&pageID=djiaakgphfalbboelieaiobfgheoldlc&moduleID=djichlhahfalbboelieaiobfgheoldlc&moreURI=/ecdomain/framework/nnrsw/djiaakgphfalbboelieaiobfgheoldlc/djichlhahfalbboelieaiobfgheoldlc.do&var_temp=eobjphbogdcnbboekapmnnfcbdankadp&currfolderid=null&showChildFlag=false&displayPageLinkFlag=true),
再用xpath直接对数据源分析即可。

 

————————————————————————————————————————————————————————————————————

 澳门新葡新京 27

根据评论区的大神也提供了其他好用的爬虫库,了解一下:

澳门新葡新京 28.png)

1> 一线码农    node+cheerio

 完整源码放在github下,

2>newjajk     
 Chromedriver

欢迎指出程序bug,提出优化意见,(●’◡’●)

3>ZUOXIANGE 
 anglesharp

 

相关文章