使用HtmlAgilityPack解析Html实现信息采集

项目需求经常会遇到一种场景,需要对远程网站特定页面自动抓取内容保存下来,比如抓取大网站的新闻存到本地作为自己网站的内容发布。本文将介绍使用HtmlAgilityPack组件来手动实现该功能,文章底部有该Demo的源码下载。

HtmlAgilityPack简介:HtmlAgilityPack是一款开源的Html解析类库,可方便地解析Html节点(包括批量节点和单个节点)。

抓取内容比较常见的情形是给定新闻列表页地址,从列表中批量抓取具体内容,比如一次性抓取该列表页20条记录的标题、详情等。

本例解析“国家电网北京市公司”网站新闻动态栏目的内容,地址为http://www.bj.sgcc.com.cn/html/main/col34/column_34_1.html

1、实现思路:

1)通过WebRequest获取列表页源码。

2)找到列表位置,找出循环内容的规律。

3)循环从每条记录详情的链接地址获取Html源代码,解析出需要的内容后插入到数据库中。

 

2、创建.NET项目

创建ASP.NET MVC项目,通过NuGet包管理器引用HtmlAgilityPack,安装到项目中

当然也可以采用手动引用dll文件的方式实现对该类库的引用,在此不做细述。

 

3、解析列表

列表页地址为http://www.bj.sgcc.com.cn/html/main/col34/column_34_1.html,网页源代码找到列表位置的html,是这样的:

在开始解析这段html之前,需要稍微了解下XPath 语法

XPath(XML Path Language) 使用路径表达式来选取 XML 文档中的节点或节点集。节点是通过沿着路径 (path) 或者 (steps) 来选取的。

可以参考https://www.w3school.com.cn/xpath/xpath_syntax.asp来深入了解XPath,本文将通过实际应用来解释其用法。

新闻列表的循环体是个ul,我们通过他的class="list"来定位到唯一的元素块。

<ul objparam="titlenum:25,pagesize:20" tag="_columninfolist" objid="6016" class="list">
    <li> <a target="_self" href="/html/main/col8/2020-09/04/20200904083721729904716_1.html">公司全力确保服贸会安全可靠供电 </a> <span>2020-09-04</span> </li>
    <li> <a target="_self" href="/html/main/col8/2020-09/01/20200901170631439110530_1.html">华灯“体检”工作启动 喜迎国庆节到来 </a> <span>2020-09-01</span> </li>

用以下代码获得所有<li>的集合:

string url = "http://www.bj.sgcc.com.cn/html/main/col34/column_34_1.html"; //要抓取页面的地址

//使用WebRequest获取指定网页的源代码
System.Net.WebRequest rGet = System.Net.WebRequest.Create(url);
System.Net.WebResponse rSet = rGet.GetResponse();
System.IO.Stream s = rSet.GetResponseStream();
System.IO.StreamReader reader = new System.IO.StreamReader(s, System.Text.Encoding.GetEncoding("utf-8")); //注意编码与源网页一致
//sourceHtml为页面返回的html
string sourceHtml = reader.ReadToEnd(); 

HtmlAgilityPack.HtmlDocument doc = new HtmlAgilityPack.HtmlDocument();
doc.LoadHtml(sourceHtml);
HtmlAgilityPack.HtmlNodeCollection list = doc.DocumentNode.SelectNodes("//ul[@class='list']/li");

最后一句doc.DocumentNode.SelectNodes("//ul[@class='list']/li");得到了所有<li>的集合。我们看一下SelectNodes方法里面的参数"//ul[@class='list']/li"。

首先双斜杠//含义:从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。

//ul[@class='list']的含义是,找到文档中class等于list的ul元素。也可以通过元素的id、style等定位,原则就是能得到唯一的元素。

后面紧跟的/li会匹配到上个节点内的所有<li>元素,这样就得到了所有的循环内容集合。

 

4、通过循环分别解析出新闻详情

在这一步,在循环内分别获取每条新闻的详情页地址,然后WebRequest获取内容页源代码,再解析出标题、发表时间和详情。

先获取一条新闻详情页的源代码,看看Html内容,找到需要的部分:

看得出,包含它们最近的节点是<div objid="6011" class="txtcon">这个元素块,我们需要在这个元素块内分别解析出红框处对应的标题、发布时间、详情。

HtmlAgilityPack.HtmlNode sectionNode = docContent.DocumentNode.SelectSingleNode("//div[@class='txtcon']");

title = sectionNode.SelectSingleNode("h1/span").InnerText;
dateandtime = sectionNode.SelectSingleNode("div[2]/span/span").InnerText;
detail = sectionNode.SelectSingleNode("div[@class='cont']").InnerText;

1)解析标题的内容时,节点内的元素块只有唯一的<h1>,通过h1/span可以得到准确的标题内容。

2)解析发布时间时,div[2]/span/span其中的div[2]表示该节点内的第二个div元素,也就是发表时间所在的<div style="padding-left:220px;">这个部分。

3)解析详情时用了div[@class='cont'],通过class属性来定位,当然也可以用类似解析发布时间时的div[3]。

用一个单独的详情页测试解析没问题后,下一步就可以嵌套到上一步的循环中了。

 

5、组合列表页和详情页,完成循环解析插入

首先将获取网页源代码的方法提取出来,方便调用。方法为string GetUrlContent(string url),传入参数为页面地址。

HtmlAgilityPack.HtmlNodeCollection list = doc.DocumentNode.SelectNodes("//ul[@class='list']/li"); 
foreach (HtmlAgilityPack.HtmlNode item in list)
{
    HtmlAgilityPack.HtmlNode a = item.SelectSingleNode("a");
    href = a.Attributes["href"].Value;
    string contentUrl = "http://www.bj.sgcc.com.cn/" + href;
    string contentDetail = GetUrlContent(contentUrl);
    docContent.LoadHtml(contentDetail);
    HtmlAgilityPack.HtmlNode sectionNode = docContent.DocumentNode.SelectSingleNode("//div[@class='txtcon']");
    title = sectionNode.SelectSingleNode("h1/span").InnerText;
    dateandtime = sectionNode.SelectSingleNode("div[2]/span/span").InnerText;
    detail = sectionNode.SelectSingleNode("div[@class='cont']").InnerText;
  
    Response.Write(title + "<br />" + dateandtime + "<br />" + detail + "<hr />");
}

循环体内简单粗暴的用Response.Write()显示出解析到的内容,此处更换成插入数据操作即可。

看一下运行后的结果:

 

 

 

使用后记

1、关于定时采集问题

可以采用定时任务实现,比如设定每间隔2小时采集一次数据,或每天凌晨2点采集。

定时任务可以采用很多种方式实现,简单点的在global中的Application_Start()设置一个定时器,完善点的可以使用Quartz.Net之类的组件。

2、采集内容重复问题

需考虑的问题是刚采集到的信息是否以前曾经采集过,避免同一条信息多次插入数据库。常用的思路有两种,一是每次采集完成后记录当前的采集时间,下次采集的时候用where条件判断信息发布时间要在上次的采集时间之后;二是采集到的信息根据标题检索一遍数据库,没有相同标题的信息才插入。

3、分页采集

信息列表都是用页码分页的,或者ajax动态填充列表。如果要同时采集多页的信息,找到第二页页码的地址解析或找出异步ajax的方法返回的内容,再进行解析。

4、分析页面Html结构

分析列表页或详情页内容块时,一定要用代码获取WebRequest的内容分析,不能在网页上按F12查看网页源代码。原因是很多内容是动态生成的,按F12查看不能真实反映页面结构,此时解析Html经常会遇到错误。

5、留意详情页可能有多种结构

笔者遇到多个网站的详情页是不同结构的,一种情况是详情页直接跳转到外部地址,还有就是本站详情页有多种结构的。这常见在大型网站、站群等情况下。处理方式是以使用比例最高的详情页结构为标准进行解析(或判断有第二种、第三种常见的结构时用另一套解析规则),对不符合代码结构的直接跳过本次循环,并且要用try...catch忽略掉异常。

6、详情中图片的保存

详情中经常会有图片,如果需要保存到本地,可以用正则表达式解析出图片元素,把图片保存到本地路径,同时替换掉原图片路径。另外,需要考虑详情中可能有超链接,需要补足URL加上域名。

7、特殊结构页面的解析

现在很多网页不是简单的Html结构,比如前端开发框架的数据动态渲染,比如用单独的js文件保存信息列表数据等,内容呈现方式各不相同。此时就需要根据实际情况判断如何使用HtmlAgilityPack进行解析,甚至是否适合用HtmlAgilityPack都需要做下斟酌。

8、自定义解析规则

案例中解析规则是写在C#代码中的,如果目标网站结构有所变化,就需要及时调整解析规则,项目重新编译、发布、更新。早期用正则表达式进行数据采集时,有的CMS系统提供了自定义采集规则功能,在此用HtmlAgilityPack也是同样的思路,设置界面放置各参数输入框,对列表页和详情页元素块定位分别由专门人员录入解析的XPath,保存到数据库。采集信息时从数据库获取该规则的列表页地址、列表解析规则和详情解析规则,然后进行解析。

这种方式有一定的局限性,有的页面很难通过简单的写个规则就能实现解析,比如上面说到的详情页有多种结构的,以及列表页非常复杂的等等。最终还是要根据实际情况分析,有针对性的思考解决方案。

9、采用异步方式采集

可以采用异步方式抓取内容,或采用多线程更灵活的处理抓取、解析和保存。另外,有的网站考虑安全因素,短时间内频繁访问会被阻止(例如在1秒中内打开10个页面),此时可以设置访问频率低一些。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值