本例使用 .NET 5、Visual Studio Code

准备

准备数据源

搜索一下,发现 这个网站 提供了通过所有的拼音来索引汉字,于是可以通过这个爬取所有汉字。


新建项目

1
2
dotnet new console -o XinHuaDictionarySpider
cd XinHuaDictionarySpider

添加依赖

  • EF Core: 数据库用
  • Html Agility Pack (下称 HAP): 分析 Html
  • System.Text.Encoding.CodePages: GB2812 编码需要

其中 Html Agility Pack 可用 AngleSharp 等其他 Html 解析库替代,本例以 Html Agility Pack 为例。

如果爬取的网页用不上 GB2812 编码的话就不需要 System.Text.Encoding.CodePages 了。

1
2
3
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package HtmlAgilityPack
dotnet add package System.Text.Encoding.CodePages

添加模型类

分析一下网站的内容页

可以构建如下模型类 DictionaryContext.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;

namespace XinHuaDictionarySpider
{
public class DictionaryContext : DbContext
{
public DbSet<Character> Characters { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlite("Data Source=Dic.db"); // 数据库路径
}

public class Character
{
/// <summary>
/// 字符ID,这里使用 Char 的 Int 值
/// </summary>
[Key]
public int CharId { get; set; }

/// <summary>
/// 汉字
/// </summary>
public string Char { get; set; } // 反正 Sqlite 没有 Char 类型

/// <summary>
/// 拼音
/// </summary>
public string PinYin { get; set; }

/// <summary>
/// 部首
/// </summary>
public string Radical { get; set; }

/// <summary>
/// 笔画数
/// </summary>
public int StrokeNum { get; set; }

/// <summary>
/// 解释
/// </summary>
public string Definition { get; set; }
}
}

本例 EF Core 部分难度为入门级别,可以访问 EF Core 入门 来了解

主函数

Usings

1
2
3
4
5
6
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using HtmlAgilityPack;

从链接加载 Html

这里可以用 HAP 自带的 HtmlWeb,当然直接用 WebClient 什么的也行,详见 Html Agility Pack Html Parser

要注意的是本例网站使用的是 GB2312 编码,因此不手动指定的话会乱码,如下

1
2
3
4
5
6
7
// 单单添加 System.Text.Encoding.CodePages 仍会报错,需要用此方法来注册一下 
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
HtmlWeb web = new HtmlWeb();
web.OverrideEncoding= Encoding.GetEncoding("GB2312");

// 通过如下方法来加载 HtmlDocument
var htmlDoc = web.Load("http://xh.5156edu.com/html3/5381.html");

获得所有拼音索引链接

F12 看到所有拼音页面都是 fontbox 类的 a 标签

因此可以这样写

1
2
3
4
5
6
7
8
// 获取所有拼音索引链接
var pinyinPages = new List<string>();
var pinyinDoc = web.Load("http://xh.5156edu.com/pinyi.html");
var pinyinNodes = pinyinDoc.DocumentNode.SelectNodes("//a[@class='fontbox']"); // HAP 是用 XPath 来选择的
foreach (var item in pinyinNodes)
{
pinyinPages.Add("http://xh.5156edu.com/" + item.GetAttributeValue("href", string.Empty));
}

获得所有汉字链接

与拼音类似

1
2
3
4
5
6
7
8
9
10
11
12
// 获得所有汉字链接
var charPages = new List<string>();
foreach (var pinyinPage in pinyinPages)
{
var pinyinPageDoc = web.Load(pinyinPage);
var charNodes = pinyinPageDoc.DocumentNode.SelectNodes(@"//a[@class='fontbox']");
foreach (var item in charNodes)
{
Console.WriteLine("准备链接中: " + item.InnerText + " http://xh.5156edu.com/" + item.GetAttributeValue("href", string.Empty));
charPages.Add("http://xh.5156edu.com/" + item.GetAttributeValue("href", string.Empty));
}
}

汉字页处理

基本思路都是通过 Html 某一部分的特征如类名等来选择需要的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 汉字页处理及向数据库中添加汉字
using (var db = new DictionaryContext())
{
db.Database.EnsureCreated(); // 确保数据库文件是存在的

foreach (var item in charPages)
{
var charDoc = web.Load(item);

var character = charDoc.DocumentNode.SelectSingleNode("//td[@class='font_22']").InnerText; // 汉字

if (db.Characters.Find((int)character[0]) == null) // 防止多音字重复添加,进行消重
{
Console.WriteLine($"[{db.Characters.Count() + 1}] Adding '{(int)character[0]} {character}' ...");

int strokeNum; // 笔画数
if (!Int32.TryParse(charDoc.DocumentNode.SelectSingleNode("//tr[@bgcolor='#E7ECF8'][1]/td[4]").InnerText, out strokeNum))
{
// 后经测试发现部分页面规格与大部分汉字有所不同,因此若索引有误则设置为 -1
strokeNum = -1;
}

var definitionHtml = charDoc.DocumentNode.SelectSingleNode("//td[@class='font_18']").InnerHtml; // 解释部分的 html
db.Add(new Character()
{
Char = character,
CharId = (int)character[0],
PinYin = charDoc.DocumentNode.SelectSingleNode("//td[@class='font_14']").InnerText,
StrokeNum = strokeNum,
Radical = charDoc.DocumentNode.SelectSingleNode("//tr[@bgcolor='#E7ECF8'][2]/td[2]").InnerText,
// 用正则表达式消去所有尖括号及其中的内容,并除掉 Non-breaking space
Definition = Regex.Replace(definitionHtml, "\\<.*?>", string.Empty).Replace("&nbsp;", string.Empty),
});

// 记得 SaveChanges
db.SaveChanges();
}
}
}

记个时

1
2
DateTime beginTime = DateTime.Now;
Console.WriteLine($"Done!\nTime cost: {DateTime.Now - beginTime}");

Build 一下然后运行

1
2
3
dotnet build -c Release
cd bin\Release\net5.0
dotnet .\XinHuaDictionarySpider.dll

耐心等他跑完吧


碎碎念

最后可以看到有三十几个字是有些问题的

这是因为这里用的选择方法还存在缺陷,不过总共两万多个字这些比例也不是很大

Github

XinHuaDictionarySpider