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);
}

还可以利用IEnumerableAsParallel()方法将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