NetCore爬虫:CatSpider# 开发笔记
(PS:我这里用了#号代替了Sharp这个单词)
CatSpider
是毕设里的数据采集模块,本来爬虫类的应用肯定使用python来开发嘛,不过用request_html
做解析的时候,python的动态类型真的把我恶心到了,而且感觉这个库也不是很成熟,html5lib
也不好用,也没心思去深入了,之前看到有大佬用.net core
平台做爬虫,于是我也来试试,没想到效果贼好,特别是配合LinqPad,写个代码段然后直接Dump做数据展示超级方便。
代码测试没问题之后直接写到项目里面,用了轻量级的ORM写入数据库,美滋滋。不过有些网站的采集比较麻烦,有反爬机制,这方面就不如python了,因为python的轮子很多,我直接找别人做的整合一下就好了,毕竟爬虫不是本项目的主要内容,不能浪费太多时间和精力。那么怎么把C#和python的模块整合在一起呢,emmm当然是RPC了,不过在python爬虫里面加个Flask
来调用也行,不过数据交换性能就要打很大折扣了。
有点偏题了,继续记录.net core
爬虫~
网络请求
如果是python的话,那么我觉得requests
库是唯一最佳选择,在NetCore里面,现在有个很好用的库HttpClient
,和requests
不同,这个是官方的,做得非常好用,调用是全部异步的。
官方推荐使用单例模式,所以我做了个HttpHelper
静态类来使用~
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");
}
}
使用起来很简单:
var data = await HttpHelper.Client.GetStringAsync("http://example.com");
解析html文档
解析同样很方便,通过AngleSharp
库,可以像CSS选择器那样快速定位网页元素,比python常用的BeautifulSoup
好用很多。
为了使用方便,我封装了一个方法快速解析文档:
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);
}
第二个方法带有一个charset
参数,可以指定文档的编码,有些国内老的网站不是用utf-8,也不标明什么编码,这样下载下来中文都是乱码的,得处理一下。
开始采集数据
这里放一些简单的例子,爬取列表:
用了linq感觉世界真美好,哈哈哈
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);
}
采集文章内容,代码特别简单:
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;
}
还有遇到大量数据的时候怎么办呀,这时候就要上并行任务了,C#对比python高性能的优势就体现出来了,上代码:
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);
}
还可以利用IEnumerable
的AsParallel()
方法将LINQ并行化。不展开了。
数据持久化
数据持久化这能搞,.net core
平台有很多好用的ORM,比如微软官方的EF Core
,比如SqlSugar
,比如Dapper
这些,不过EF Core感觉比较重,而且我做这个的时候,还没学怎么单独使用。
然后我找了个国人做的轻量级ORM,Chloe
,看文档使用很简单,于是就试试,模型代码:
[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; }
}
这个orm需要在模型类上加上属性,定义主键和表名什么的。EF Core这种就不用,完全按照约定来的,这点不如EF Core方便。
而且它不能自动生成表,我只好手动创建表,差评。
接下来常规操作,创建DBContext
,大部分ORM都差不多:
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;
}
}
对了,要先配置连接:
public static class SQLiteContextFactory
{
public static SQLiteContext GetContext()
{
string connString = "Data Source=CatSpider.db";
return new SQLiteContext(new SQLiteConnectionFactory(connString));
}
}
使用很简单:
var context = SQLiteContextFactory.GetContext();
obj = context.Insert(obj);
更多操作看文档去,本文就不展开了
提供HTTP接口
基本功能实现了,之前考虑到和其他语言或者模块的互操作,觉得可以用HTTP接口来交互,(虽然现在觉得不是最佳方案)
这个很简单,只要找一个轻量级的服务器框架就行了,我找到一个叫Nancy
的,听起来像人名,结果居然是Web框架。
使用很简单,直接启动:
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();
}
定义接口
这个框架有个Module
的概念,就和Controller差不多吧,定义很简单,我放测试代码上来,业务代码暂时不放出来:
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 }));
}
}
日志记录
最后说一下日志,我这里用了nlog这个轻量级日志引擎。
首先要配置,NLog.config
,设置生成时自动复制到目标文件夹:
<?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>
官方推荐每个类用一个logger实例:
private static Logger logger = LogManager.GetCurrentClassLogger();
使用:
logger.Debug($"列表:{obj}");
很方便。
大概就这,有空继续写其他的~
欢迎交流
交流问题请在微信公众号后台留言,每一条信息我都会回复哈~ - 微信公众号:画星星高手 - 打代码直播间:https://live.bilibili.com/11883038 - 知乎:https://www.zhihu.com/people/dealiaxy - 专栏:https://zhuanlan.zhihu.com/deali - 简书:https://www.jianshu.com/u/965b95853b9f