C# Crawler: CatSpider# Development Notes
Introduction (PS: I used the # symbol here in place of the word Sharp) CatSpider is the data collection module in my graduation project. Originally, crawler-type applications would of course be developed with Python, but when using request_html for parsing, Python's dynamic typing really disgusted me, and I felt this library wasn't very mature either, html5lib wasn't easy to use, and I didn't feel like digging deeper. I'd seen some experts building crawlers on the .net core platform, so I decided to give it a try too. I didn't expect the results to be so good, especially together with LinqPa
目录9 个章节
Introduction
(PS: I used the # symbol here in place of the word Sharp)
CatSpider is the data collection module in my graduation project. Originally, crawler-type applications would of course be developed with Python, but when using request_html for parsing, Python's dynamic typing really disgusted me, and I felt this library wasn't very mature either, html5lib wasn't easy to use, and I didn't feel like digging deeper. I'd seen some experts building crawlers on the .net core platform, so I decided to give it a try too. I didn't expect the results to be so good, especially together with LinqPad—write a code snippet then Dump it directly for data display, super convenient.
After the code tested fine, I wrote it straight into the project, used a lightweight ORM to write to the database, felt great. However, scraping some websites is more troublesome due to anti-crawling mechanisms, and in this regard it's not as good as Python, because Python has lots of wheels—I can just find what others have made and integrate it. After all, crawling isn't the main focus of this project, so I can't waste too much time and energy. So how do you integrate C# and Python modules together? emmm of course it's RPC, though adding a Flask in the Python crawler for calling would also work, but data exchange performance would take a big hit.
Got a bit off topic, continuing to document the .net core crawler~
Network Requests
If it were Python, then I think the requests library is the only best choice. In NetCore, there's now a very useful library HttpClient. Unlike requests, this one is official, made very easy to use, and all calls are asynchronous.
The official recommendation is to use the singleton pattern, so I made an HttpHelper static class to use~
public static class HttpHelper
{
private static readonly HttpClientHandler handler;
private static readonly HttpClient client;
public static HttpClientHandler Handler { get => handler; }
public static HttpClient Client { get => client; }
static HttpHelper()
{
handler = new HttpClientHandler();
client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36");
}
}
Using it is very simple:
var data = await HttpHelper.Client.GetStringAsync("http://example.com");
Parsing HTML Documents
Parsing is equally convenient. With the AngleSharp library, you can quickly locate page elements like CSS selectors, much better than Python's commonly used BeautifulSoup.
For convenience, I wrapped a method to quickly parse documents:
public static async Task<IHtmlDocument> GetHtmlDocument(string url)
{
var html = await client.GetStringAsync(url);
return new HtmlParser().ParseDocument(html);
}
public static async Task<IHtmlDocument> GetHtmlDocument(string url, string charset)
{
var res = await client.GetAsync(url);
var resBytes = await res.Content.ReadAsByteArrayAsync();
var resStr = Encoding.GetEncoding(charset).GetString(resBytes);
return new HtmlParser().ParseDocument(resStr);
}
The second method has a charset parameter to specify the document encoding. Some older domestic websites don't use utf-8 and don't indicate the encoding either, so Chinese characters downloaded this way are all garbled and need to be handled.
Starting Data Collection
Here are some simple examples, crawling a list:
Using linq, the world feels so beautiful, hahaha
public static async Task<List<ListArticle>> CrawlHotList()
{
var dom = await HttpHelper.GetHtmlDocument("https://www.com/");
var links = dom.QuerySelectorAll(".hot-list ul li a");
var hotList = links.Select((elem, result) =>
new ListArticle{
Title = elem.TextContent,
Link = elem.GetAttribute("href"),
Source = "来源"});
return new List<ListArticle>(hotList);
}
Crawling article content, the code is especially simple:
public static async Task<Article> CrawlArticle(string url)
{
var dom = await HttpHelper.GetHtmlDocument(url);
var data = new Article
{
Title = dom.QuerySelector("#cb_post_title_url").TextContent,
Source = "来源",
Content = dom.QuerySelector(".postBody").TextContent,
Link = url,
PublishTime = DateTime.Parse(dom.QuerySelector("#post-date").TextContent),
AddTime = DateTime.Now,
Author = dom.QuerySelector(".postDesc a").TextContent
};
return data;
}
And what about when you encounter large amounts of data? That's when you need to bring in parallel tasks. C#'s high-performance advantage over Python shows here. Here's the code:
public static async Task<List<CnBlogListArticle>> CrawlList2(int page = 10)
{
var http = HttpHelper.Client;
var parser = new HtmlParser();
var data = await Task.WhenAny(
Enumerable.Range(1, page)
.Select(async page =>
{
string pageData = await http.GetStringAsync($"https://www.cnblogs.com/sitehome/p/{page}");
IHtmlDocument doc = await parser.ParseDocumentAsync(pageData);
return doc.QuerySelectorAll(".post_item").Select(tag => new CnBlogListArticle
{
Title = tag.QuerySelector(".titlelnk").TextContent, Page = page,
UserName = tag.QuerySelector(".post_item_foot .lightblue").TextContent,
PublishTime = DateTime.Parse(Regex.Match(tag.QuerySelector(".post_item_foot").ChildNodes[2].TextContent, @"(\d{4}\-\d{2}\-\d{2}\s\d{2}:\d{2})", RegexOptions.None).Value),
CommentCount = int.Parse(tag.QuerySelector(".post_item_foot .article_comment").TextContent.Trim()[3..^1]),
ViewCount = int.Parse(tag.QuerySelector(".post_item_foot .article_view").TextContent[3..^1]),
BriefContent = tag.QuerySelector(".post_item_summary").TextContent.Trim(),
});
})).ConfigureAwait(true);
return new List<CnBlogListArticle>(await data);
}
You can also use IEnumerable's AsParallel() method to parallelize LINQ. Won't expand on that.
Data Persistence
Data persistence is doable. The .net core platform has many useful ORMs, such as Microsoft's official EF Core, SqlSugar, Dapper, and so on, but EF Core feels relatively heavy, and when I was doing this, I hadn't yet learned how to use it standalone.
Then I found a lightweight ORM made by a Chinese developer, Chloe. Looking at the docs it seemed simple to use, so I gave it a try. Model code:
[Table("ListArticles")]
public class ListArticle
{
[Column("Id", IsPrimaryKey = true)]
[AutoIncrement]
public int Id { get; set; }
public string Title { get; set; }
public string Source { get; set; }
public string Link { get; set; }
}
This ORM requires adding attributes on the model class to define the primary key, table name, etc. EF Core doesn't need this—it follows conventions completely. In this regard it's not as convenient as EF Core.
And it can't auto-generate tables, so I had to create tables manually. Thumbs down.
Next, the usual operations, create a DBContext. Most ORMs are similar:
public class SQLiteConnectionFactory : IDbConnectionFactory
{
/// <summary>
/// 数据库连接字符串,如下
/// Data Source=dapperTest.db
/// </summary>
string _connString = null;
public SQLiteConnectionFactory(string connString)
{
this._connString = connString;
}
public IDbConnection CreateConnection()
{
// 得先安装Sqlite的驱动
// Microsoft.Data.Sqlite
// System.Data.Sqlite
SQLiteConnection conn = new SQLiteConnection(_connString);
return conn;
}
}
Oh right, you need to configure the connection first:
public static class SQLiteContextFactory
{
public static SQLiteContext GetContext()
{
string connString = "Data Source=CatSpider.db";
return new SQLiteContext(new SQLiteConnectionFactory(connString));
}
}
Using it is very simple:
var context = SQLiteContextFactory.GetContext();
obj = context.Insert(obj);
For more operations, go look at the docs. This article won't expand on that.
Providing an HTTP API
Basic functionality is implemented. Previously, considering interoperability with other languages or modules, I thought we could interact via HTTP APIs (though now I don't think it's the best solution)
This is simple, just find a lightweight server framework. I found one called Nancy, which sounds like a person's name, but it turned out to be a web framework.
Using it is very simple, just start it:
private string host = "http://localhost";
private int port = 50010;
private NancyHost nancy;
public Program()
{
var uri = new Uri($"{host}:{port}/");
nancy = new NancyHost(uri);
}
public void Start()
{
nancy.Start();
logger.Debug($"nancy server started at {host}:{port}");
Console.ReadKey();
nancy.Stop();
}
static void Main(string[] args)
{
new Program().Start();
}
Defining APIs
This framework has a Module concept, pretty much like a Controller. Defining them is simple. I'll put up the test code; business code won't be posted for now:
public class MainModule : NancyModule
{
public MainModule()
{
Get("/", _ => "hello");
Get("404", _ => HttpStatusCode.NotFound);
Get("test", _ =>
{
var response = (Response)JsonConvert.SerializeObject(new int[] { 1, 2, 3 });
response.ContentType = "application/json";
return response;
});
Get("test2", _ => JsonConvert.SerializeObject(new int[] { 1, 2, 3 }));
}
}
Logging
Finally, a word about logging. I used nlog, a lightweight logging engine.
First, configure NLog.config and set it to copy automatically to the output folder on build:
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
throwConfigExceptions="true">
<targets>
<!-- 配置说明:https://github.com/NLog/NLog/wiki/ColoredConsole-target -->
<target name="f1" xsi:type="File" fileName="CatSpiderLog.txt"/>
<target name="n1" xsi:type="Network" address="tcp://localhost:4001"/>
<target name="c1" xsi:type="Console" encoding="utf-8"
error="true"
detectConsoleAvailable="true" />
<target name="c2" xsi:type="ColoredConsole" encoding="utf-8"
useDefaultRowHighlightingRules="true"
errorStream="true"
enableAnsiOutput="true"
detectConsoleAvailable="true"
DetectOutputRedirected="true">
</target>
</targets>
<rules>
<logger name="*" maxLevel="Debug" writeTo="c2" />
<logger name="*" maxLevel="Debug" writeTo="f1" />
<!--<logger name="*" minLevel="Info" writeTo="f1" />-->
</rules>
</nlog>
The official recommendation is to use one logger instance per class:
private static Logger logger = LogManager.GetCurrentClassLogger();
Usage:
logger.Debug($"列表:{obj}");
Very convenient.
That's about it. I'll continue writing about other things when I have time~
Welcome to Connect
For questions, please leave a message on the WeChat official account backend. I'll reply to every message~
- WeChat Official Account: 画星星高手
- Coding livestream: https://live.bilibili.com/11883038
- Zhihu: https://www.zhihu.com/people/dealiaxy
- Column: https://zhuanlan.zhihu.com/deali
- Jianshu: https://www.jianshu.com/u/965b95853b9f
_EOF_
C# Crawler: CatSpider# Development Notes
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
开发现代化的.NetCore控制台程序:(2)创建一个C#项目模板
又一个.NetCore模板?FluentConsole.Templates,一键生成骨架,包含DI、日志、配置。安装?dotnet new install,然后dotnet new flu-cli -n MyProject。太简单了吧,谁在乎?
2023-11-10 · C# · 约 1 分钟
一次小而美的重构:使用 C# 在 Avalonia 中生成真正好看的词云
重构StarBlogPublisher中词云展示功能的过程。作者对比了两个流行的C#词云库,最终选择了功能更强大的Sdcb.WordCloud。文章详细说明了如何使用该库获取词频数据并生成词云图像,并展示了重构前后的效果对比。
2025-04-26 · C# · 约 1 分钟
上位机开发之假装有设备,使用 C# 模拟串口设备
用C#模拟串口设备?这年头还学这种过时技术?不过行吧,至少比某些只会玩AI的卷王强点。搞搞清楚协议文档,装个破com0com驱动,写点死板代码应付下调试需求。至于什么物联网平台、工业控制?那是别人该干的事,我随便搞搞就够了。
2025-08-26 · C# · 约 1 分钟
试一下用DotNetCore来做机器学习,ML-Net框架跑起来
ML.Net简介 官网:https://dotnet.microsoft.com/apps/machinelearning-ai/ml-dotnet 官方介绍:The ML.NET command line interface (CLI), provides tools for building machine learning models with ML.NET. ML.NET 是面向.NET
2021-12-18 · C# · 约 1 分钟
讨论
共 0 条加载评论中…