首页 > Python基础教程 >
-
DotNet应用之爬虫入门系列(三):常见文本结构的处理
爬虫在获取服务器正确的响应后,往往需要处理响应流或文本,从而获得需要的数据并持久化。本文文字篇幅不会很长,主要向有兴趣的读者介绍一下爬虫经常接触的两种文本结构——Html和Json——的处理方式及相关细节。值得开发新手注意的是,这两种文本结构并不仅限于爬虫领域出现,而是任何开发领域都可能会接触到的,本文虽不能面面俱到,但可以引为向导。
- Html
完全理解该节需要掌握:
- 知道什么是HTML;
- 知道什么是XPath;
- 了解XPath的基本语法;
- 了解XPath的常用函数;
- 熟悉HtmlAgilityPack或AngleSharp之一。
Html格式的响应文本经常出现在静态页面或后端渲染(服务器在收到Http请求后,加载数据并据此生成网页Html,再返回浏览器)的网站。
在.Net平台,常用的Html解析工具包有html-agility-pack和AngleSharp。笔者并不清楚两者的优劣高下(欢迎知情读者赐教),尽管后者在README中写道:
The advantage over similar libraries likeHtmlAgilityPackis that the exposed DOM is using the official W3C specified API, i.e., that even things likequerySelectorAll
are available in AngleSharp. Also the parser uses the HTML 5.1 specification, which defines error handling and element correction. The AngleSharp library focuses on standards compliance, interactivity, and extensibility. It is therefore giving web developers working with C# all possibilities as they know from using the DOM in any modern browser.
但本文还是只给出HtmlAgilityPack的代码示例(笔者目前还没用过AngleSharp)。读者不必纠结和担心,这两个项目的维护都很活跃,选择谁都可以,只需要熟悉API即可。同时,读者在使用解析包前还需要粗略了解XPath,回头可以跟下文代码互相印证。
让我们以博客园首页的新闻页为例。
假设我们想抓取最近的新闻列表,实体类型定义如下:
public sealed class NewsEntity
{
public int Id { get; set; }
public string Url { get; set; }
public string Title { get; set; }
/// <summary>
/// 摘要
/// </summary>
public string Roundup { get; set; }
public string Publisher { get; set; }
public DateTime? PublishTime { get; set; }
/// <summary>
/// 推荐数
/// </summary>
public int DiggCount { get; set; }
public int CommentCount { get; set; }
public int ViewCount { get; set; }
public DateTime CollectTime { get; set; }
}
打开浏览器开发者工具可以查看该网页的Html结构,笔者将自上而下、自外而内地向读者介绍。
让我们先进入head。新手在写爬虫时,往往会碰到浏览器上显示正常而程序抓取却是乱码的情况,这时候第一反应应该是检查自己的编码(Encoding)是否正确。而下图中<meta charset="utf-8">这一行,就描述了网页文字采用的编码是UTF-8。
另外,也可以在响应头(Response Headers)里检查Content-Type字段。但这里有两点需要注意:并不是每个响应都会包含这个字段;就算返回了该字段,它里面的charset值也不保证和Html里的一致。因此,应当首先查看Html head里charset值。
接着查看body。
找到并进入新闻列表所在的位置(使用谷歌浏览器调试时,只要鼠标放在div上,浏览器就会示意相应的网页区域)。
继续进入单个post_item,就可以看到单条新闻的信息。
那么如何将Html文本中的数据提取出来呢?上文说的Html解析工具包就登场了,通过Nuget管理器安装即可。
因为本文侧重讲文本解析,所以我们假设已经获得了正确的响应文本。
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
public class HtmlReadTest
{
public static List<NewsEntity> GetNewsEntities(string html)
{
var doc = new HtmlDocument();
try
{
doc.LoadHtml(html);
}
catch
{
//Do something...
return null;
}
var root = doc.DocumentNode;
var postListNode = root.SelectSingleNode("//div[@id='post_list']");
var postItems = postListNode.SelectNodes("//div[@class='post_item']");
if (postItems == null || postItems.Count < 1)
{
return null;
}
var result = new List<NewsEntity>(postItems.Count);
foreach (var item in postItems)
{
var temp = CreateNewsEntity(item);
if (temp != null)
{
result.Add(temp);
}
}
return result;
}
private static NewsEntity CreateNewsEntity(HtmlNode node)
{
var titleNode = node.SelectSingleNode(".//a[contains(@class, 'title')]");
if (titleNode is null)
{
return null;
}
//获取标题
string title = titleNode.InnerText?.Trim();
if (string.IsNullOrEmpty(title))
{
return null;
}
//获取链接
string url = titleNode.GetAttributeValue("href", string.Empty);
//获取摘要
var roundupNode = node.SelectSingleNode(".//p[@class='post_item_summary']");
string roundup = roundupNode?.InnerText?.Trim() ?? string.Empty;
//获取推荐数
var diggNode = node.SelectSingleNode(".//span[@class='diggnum']");
int diggCount = 0;
if (diggNode != null)
{
string diggStr = diggNode.InnerText?.Trim();
if (!string.IsNullOrEmpty(diggStr))
{
int.TryParse(diggStr, out diggCount);
}
}
string publisher = string.Empty;
DateTime? publishTime = null;
int comment = 0, view = 0;
var footNode = node.SelectSingleNode(".//div[@class='post_item_foot']");
if (footNode != null)
{
//获取发布时间
var publishTimeStr = footNode.InnerText;
publishTime = MatchDate(publishTimeStr);
//获取发布者
var publisherNode = footNode.SelectSingleNode("./a[@href]");
publisher = publisherNode?.InnerText?.Trim() ?? string.Empty;
//获取阅读数
var commentNode = footNode.SelectSingleNode("./span[contains(@class, 'comment')]");
string commentStr = commentNode.InnerText;
comment = MatchNumber(commentStr);
//获取评论数
var viewNode = footNode.SelectSingleNode("./span[contains(@class, 'view')]");
string viewStr = viewNode.InnerText;
view = MatchNumber(viewStr);
}
return new NewsEntity()
{
Title = title,
Url = url,
Roundup = roundup,
Publisher = publisher,
PublishTime = publishTime,
DiggCount = diggCount,
CommentCount = comment,
ViewCount = view,
CollectTime = DateTime.Now
};
}
private static DateTime? MatchDate(string text)
{
if (string.IsNullOrEmpty(text))
{
return null;
}
var match = Regex.Match(text, "(\\d{4}-\\d{2}-\\d{2})\\s(\\d{2}:\\d{2})");
if (!match.Success)
{
return null;
}
string dateStr = match.Value;
if (!DateTime.TryParse(dateStr, out var date))
{
return null;
}
return date;
}
private static int MatchNumber(string text)
{
if (string.IsNullOrEmpty(text))
{
return 0;
}
var match = Regex.Match(text, "\\d+");
if (!match.Success)
{
return 0;
}
int.TryParse(match.Value, out int result);
return result;
}
}
可以看到我们获取具体数据主要使用的API是SelectNodes、SelectSingleNode和GetAttributeValue,非常容易上手;再配合XPath以及局部小范围使用正则表达式,就可以写出健壮、清晰、易维护的代码。以上代码读者可自行尝试并验证。
- Json
完全理解该节需要掌握:
- 知道什么是Json;
- 知道什么是JPath;
- 了解JPath的基本语法;
- 熟悉Newtonsoft.Json。
Json格式的响应文本通常出现在前后端分离开发、前端渲染(通过Ajax请求、异步加载等方式载入页面数据)的页面。
在.Net平台,最广为人知的Json解析工具包就是Newtonsoft.Json了。
我们假设某网站有一个接口以Json格式文本返回班级信息,如下:
{
"Id": 601,
"TeacherName": "大木",
"Data": [
{
"Id": 1,
"Name": "菜须鲲"
},
{
"Id": 2,
"Name": "吾亦烦"
},
{
"Id": 3,
"Name": "路寒"
}
]
}
一种反序列化的方式是定义一个结构相同的类:
public sealed class Class
{
public int Id { get; set; }
public string TeacherName { get; set; }
public List<Student> Data { get; set; }
}
public sealed class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
然后只需要一行代码就可以将Json文本转化为Class类:
using Newtonsoft.Json;
public static Class DeserializeClass(string json)
{
return JsonConvert.DeserializeObject<Class>(json);
}
但是爬虫程序在绝大多数很多情况下,可能并不需要Json文本里的全部数据,亦或者是需要根据文本里的数据做一些处理再放入实体类。如果每一个Json格式的文本都要写一个对应的类,代码就会显得非常臃肿(想一个有意义的命名也会令人头疼)。所幸Newstonsoft.Json.Linq为我们提供了一些“模版类”,可以让我们轻松地应对这种情况。
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public static Class DeserializeClass(string json)
{
JToken source;
try
{
source = JsonConvert.DeserializeObject<JToken>(json);
}
catch
{
//Do something...
return null;
}
if (source is null)
{
return null;
}
int classId = source.Value<int>("Id");
string tName = source.Value<string>("TeacherName");
var result = new Class()
{
Id = classId,
TeacherName = tName
};
JArray datas = source.Value<JArray>("Data");
if (datas != null)
{
var students = new List<Student>(datas.Count);
foreach (var data in datas)
{
students.Add(GetStudent(data));
}
result.Data = students;
}
return result;
}
private static Student GetStudent(JToken item)
{
int id = item.Value<int>("Id");
string name = item.Value<string>("Name");
return new Student() { Id = id, Name = name };
}
虽然对该示例来说,使用JToken和JArray“模板类”使得解析的代码变长了很多,但在更复杂的情况下,这样写更具有灵活性,不需要去写仅用于Json反序列化的类型。另外,对于一些层层嵌套的复杂Json文本,我们也可以通过JPath来简化代码,比如如果我们只想获取学生信息:
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public static List<Student> GetStudents(string json)
{
JToken source;
try
{
source = JsonConvert.DeserializeObject<JToken>(json);
}
catch
{
//Do something...
return null;
}
var datas = source?.SelectTokens("$.Data[*]");
if(datas is null || !datas.Any())
{
return null;
}
var result = new List<Student>();
foreach (var data in datas)
{
result.Add(GetStudent(data));
}
return result;
}
对比上节HTML的解析,可以看到JPath与XPath的相近之处,使用它们可以简化解析的步骤,让代码更简便、重点更突出。以上代码感兴趣的读者可以自行尝试并验证。
- 小结
本文介绍了开发领域常见的两种文本结构——Html和Json,并介绍了爬虫领域关注的反序列化工具和使用方法,同时示例代码也涉及了一些常见的文本细节处理。
笔者精力有限,不能面面俱到,有兴趣的读者可以自行研究,不要拘泥于本文代码示例,应当举一反三;如有纰漏错误,欢迎指正。
最后,向本文提及的开源项目的所有贡献者致敬!