爬虫系统的意义

爬虫的意义在于采集大批量数据,然后基于此进行加工/分析,做更有意义的事情。谷歌,百度,今日头条,天眼查都离不开爬虫。

今日目标

今天我们来实践一个最简单的爬虫系统。根据Url来识别网页内容。

网页内容识别利器:HtmlAgilityPack

GitHub地址

HtmlAgilityPack官网

HtmlAgilityPack的stackoverflow地址

至今Nuget已有超过900多万的下载量,应用量十分庞大。它提供的文档教程也十分简单易用。

Parser解析器

HtmlParse可以让你解析HTML并返回HtmlDocument

  • FromFile从文件读取
/// <summary>
/// 从文件读取
/// </summary>
public void FromFile() {
var path = @"test.html";
var doc = new HtmlDocument();
doc.Load(path);
var node = doc.DocumentNode.SelectSingleNode("//body");
Console.WriteLine(node.OuterHtml);
}
  • 从字符串加载
/// <summary>
/// 从字符串读取
/// </summary>
public void FromString()
{
var html = @"<!DOCTYPE html>
<html>
<body>
<h1>This is <b>bold</b> heading</h1>
<p>This is <u>underlined</u> paragraph</p>
<h2>This is <i>italic</i> heading</h2>
</body>
</html> ";
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html);
var htmlBody = htmlDoc.DocumentNode.SelectSingleNode("//body");
Console.WriteLine(htmlBody.OuterHtml);
}
  • 从网络加载
/// <summary>
/// 从网络地址加载
/// </summary>
public void FromWeb() {
var html = @"https://www.cnblogs.com/";
HtmlWeb web = new HtmlWeb();
var htmlDoc = web.Load(html);
var node = htmlDoc.DocumentNode.SelectSingleNode("//div[@id='post_list']");
Console.WriteLine("Node Name: " + node.Name + "\n" + node.OuterHtml);
}

Selectors选择器

选择器允许您从HtmlDocument中选择HTML节点。它提供了两个方法,可以用XPath表达式筛选节点。XPath教程

SelectNodes() 返回多个节点

SelectSingleNode(String) 返回单个节点

简介到此为止,更全的用法参考 http://html-agility-pack.net

查看网页结构

我们以博客园首页为示例。用chrome分析下网页结构,可采集出推荐数,标题,内容Url,内容简要,作者,评论数,阅读数。

编码实现

建立一个Article用来接收文章信息。


public class Article
{
/// <summary>
///
/// </summary>
public string Id { get; set; }
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 概要
/// </summary>
public string Summary { get; set; }
/// <summary>
/// 文章链接
/// </summary>
public string Url { get; set; }
/// <summary>
/// 推荐数
/// </summary>
public long Diggit { get; set; }
/// <summary>
/// 评论数
/// </summary>
public long Comment { get; set; }
/// <summary>
/// 阅读数
/// </summary>
public long View { get; set; }
/// <summary>
///明细
/// </summary>
public string Detail { get; set; }
/// <summary>
///作者
/// </summary>
public string Author { get; set; }
/// <summary>
/// 作者链接
/// </summary>
public string AuthorUrl { get; set; }
}

然后根据网页结构,查看XPath路径,采集内容

/// <summary>
/// 解析
/// </summary>
/// <returns></returns>
public List<Article> ParseCnBlogs()
{
var url = "https://www.cnblogs.com";
HtmlWeb web = new HtmlWeb();
//1.支持从web或本地path加载html
var htmlDoc = web.Load(url);
var post_listnode = htmlDoc.DocumentNode.SelectSingleNode("//div[@id='post_list']");
Console.WriteLine("Node Name: " + post_listnode.Name + "\n" + post_listnode.OuterHtml);
var postitemsNodes = post_listnode.SelectNodes("//div[@class='post_item']");
var articles = new List<Article>();
var digitRegex = @"[^0-9]+";
foreach (var item in postitemsNodes)
{
var article = new Article();
var diggnumnode = item.SelectSingleNode("//span[@class='diggnum']");
//body
var post_item_bodynode = item.SelectSingleNode("//div[@class='post_item_body']");
var titlenode = post_item_bodynode.SelectSingleNode("//a[@class='titlelnk']");
var summarynode = post_item_bodynode.SelectSingleNode("//p[@class='post_item_summary']");
//foot
var footnode = item.SelectSingleNode("//div[@class='post_item_foot']");
var authornode = footnode.ChildNodes[1];
var commentnode = item.SelectSingleNode("//span[@class='article_comment']");
var viewnode = item.SelectSingleNode("//span[@class='article_view']");
article.Diggit = int.Parse(diggnumnode.InnerText);
article.Title = titlenode.InnerText;
article.Url = titlenode.Attributes["href"].Value;
article.Summary = titlenode.InnerHtml;
article.Author = authornode.InnerText;
article.AuthorUrl = authornode.Attributes["href"].Value;
article.Comment = int.Parse(Regex.Replace(commentnode.ChildNodes[0].InnerText, digitRegex, ""));
article.View = int.Parse(Regex.Replace(viewnode.ChildNodes[0].InnerText, digitRegex, ""));
articles.Add(article);
}
return articles;
}

查看采集结果

看到结果就惊呆了,竟然全是重复的。难道是Xpath语法理解不对么? 

重温下XPath语法

XPath 使用路径表达式在 XML 文档中选取节点。节点是通过沿着路径或者 step 来选取的

表达式 描述
nodename 选取此节点的所有子节点。
/ 从根节点选取。
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
. 选取当前节点。
.. 选取当前节点的父节点。
@ 选取属性。

XPath 通配符可用来选取未知的 XML 元素

通配符 描述
* 匹配任何元素节点。
@* 匹配任何属性节点。
node() 匹配任何类型的节点。

我测试了几个语法如:

//例1,会返回20个
var titlenodes = post_item_bodynode.SelectNodes("//a[@class='titlelnk']");
//会报错,因为这个a并不直接在bodynode下面,而是在子级h3元素的子级。
var titlenodes = post_item_bodynode.SelectNodes("a[@class='titlelnk']");

然后又实验了一种:

//Bingo,这个可以,但是强烈指定了下级h3,这就稍微麻烦了点。
var titlenodes = post_item_bodynode.SelectNodes("h3//a[@class='titlelnk']");

这里就引申出了一个小问题:如何定位子级的子级?用通配符*可以么?

//返回1个。
var titlenodes= post_item_bodynode.SelectNodes("*//a[@class='titlelnk']")

能正确返回1,应该是可以了,我们改下代码看下效果。 然后和博客园首页数据对比,结果吻合。

所以我们可以得出结论:

XPath搜索以//开头时,会匹配所有的项,并不是子项。

直属子级可以直接跟上 node名称。

只想查子级的子级,可以用*代替子级,实现模糊搜索。

改过后代码如下:

public List<Article> ParseCnBlogs()
{
var url = "https://www.cnblogs.com";
HtmlWeb web = new HtmlWeb();
//1.支持从web或本地path加载html
var htmlDoc = web.Load(url);
var post_listnode = htmlDoc.DocumentNode.SelectSingleNode("//div[@id='post_list']");
//Console.WriteLine("Node Name: " + post_listnode.Name + "\n" + post_listnode.OuterHtml);
var postitemsNodes = post_listnode.SelectNodes("div[@class='post_item']");
var articles = new List<Article>();
var digitRegex = @"[^0-9]+";
foreach (var item in postitemsNodes)
{
var article = new Article();
var diggnumnode = item.SelectSingleNode("*//span[@class='diggnum']");
//body
var post_item_bodynode = item.SelectSingleNode("div[@class='post_item_body']");
var titlenode = post_item_bodynode.SelectSingleNode("*//a[@class='titlelnk']");
var summarynode = post_item_bodynode.SelectSingleNode("p[@class='post_item_summary']");
//foot
var footnode = post_item_bodynode.SelectSingleNode("div[@class='post_item_foot']");
var authornode = footnode.ChildNodes[1];
var commentnode = footnode.SelectSingleNode("span[@class='article_comment']");
var viewnode = footnode.SelectSingleNode("span[@class='article_view']");
article.Diggit = int.Parse(diggnumnode.InnerText);
article.Title = titlenode.InnerText;
article.Url = titlenode.Attributes["href"].Value;
article.Summary = titlenode.InnerHtml;
article.Author = authornode.InnerText;
article.AuthorUrl = authornode.Attributes["href"].Value;
article.Comment = int.Parse(Regex.Replace(commentnode.ChildNodes[0].InnerText, digitRegex, ""));
article.View = int.Parse(Regex.Replace(viewnode.ChildNodes[0].InnerText, digitRegex, ""));
articles.Add(article);
}
return articles;
}

感谢apgk也提供了一种办法,也是ok的。

var titlenodes = post_item_bodynode.SelectNodes(post_item_bodynode.XPath+"//a[@class='titlelnk']");

源码

点击 推荐
查看源码。

总结

demo到此结束。谢谢观看!

下篇继续构思如何构建自定义规则,让用户可以在页面自己填写规则去识别。

最新文章

  1. css实现定高的元素在不定高的容器中水平垂直居中(兼容IE8及以上)
  2. iOS时间问题
  3. linux下gimp的使用
  4. HybridApp iOS ATS解决方案
  5. LESS中文版函数手册
  6. 利用Mathematica计算伴随矩阵
  7. iOS常用的设计模式
  8. JS获取日期和时间
  9. 【HDOJ】2150 Pipe
  10. 如何查询一个库文件属于哪个rpm包
  11. MAKE gnu
  12. GCD之信号量机制二
  13. 多个DbContext修改同一张表测试
  14. d3.js做的柱状图
  15. vue.js学习第一天,了解vue.js
  16. 华容道 [NOIP 2013]
  17. 查看服务器CPU的个数、CPU的核数、多核超线程数、机器硬件型号
  18. VS2008:Failed to return new Code Element
  19. Linux学习之批量修改文件名
  20. SourceTree安装和使用

热门文章

  1. leetcode-14最长公共前缀
  2. Android平台下利用zxing实现二维码开发
  3. java中获取系统的当前时间
  4. Bresenham算法的实现思路
  5. shell编程-test命令(七)
  6. Spring入门详细教程(四)
  7. 大杀器:VS2017 查看或调试liunx代码(转载)
  8. web前端(13)—— 了解JavaScript,JavaScript的引入方式
  9. Sqlserver还原master
  10. http网站转换成https网站