1. 程式人生 > >.NET輕鬆寫部落格園爬蟲

.NET輕鬆寫部落格園爬蟲

.NET輕鬆寫部落格園爬蟲

爬蟲,是一種按照一定的規則,自動地抓取網站的程式或者指令碼。.NET寫爬蟲非常簡單,並能輕鬆優化效能。今天我將分享一段簡短的程式碼,爬出部落格園前200頁精華內容,然後通過微小的改動,將程式碼升級為多執行緒爬蟲,讓爬蟲速度提升數倍;最後將對爬到了內容進行一些有趣的分析。

本文演示的程式碼,可以從這裡下載:https://github.com/sdcb/blog-data/tree/master/2019/20190826-cnblogs-crawler-home

​我的演示程式碼通過LINQPad執行,可以在這裡找到最新的LINQPad下載連結:https://www.linqpad.net/Download.aspx

這些程式碼同樣可以執行在Visual Studio中。其中.Dump()方法可以在Visual Studio中搜索並安裝NuGet包即可相容:

Install-Package LINQPad

爬蟲的三要素

經過我“多年”的爬蟲騷操作的經驗,我認為爬蟲無非就是:

  1. 下載網站資料;
  2. 解析/儲存網站資料;
  3. 分析資料與下個頁面之間的關係,以便繼續下載下個頁面資料;

下面我將通過程式碼演示這三點。

下載網站資料

換作以前,有WebRequest/WebClient/RestSharp之類的選擇,但如今已經都被HttpClient取代了,HttpClient同時內置於.NET Framework 4.5/netstandard 1.1

及以後的版本,不用安裝第三方包。

程式碼使用也非常簡單:

var client = new HttpClient();
string response = await client.DownloadStringAsync("https://www.cnblogs.com");

其中response就是從部落格園下載的html字串。

解析網站資料

.NET解析html有多個包可供選擇,如HtmlAgilityPackCsQuery等。但AngleSharp由於其簡單好用、功能強大,已經也成為解析html的不錯之選。

AngleSharp是開源專案,Github地址是:https://github.com/AngleSharp/AngleSharp。

近期還加入了.NET Foundation(.NET基金會),官網地址是:https://anglesharp.github.io 。

使用AngleSharp解析html示例程式碼(LINQPad中,按Ctrl+Shift+P快速安裝NuGet):

Install-Package AngleSharp
Install-Package Newtonsoft.Json

使用程式碼如下:

var parser = new HtmlParser();
IHtmlDocument dom = parser.ParseDocument(@"<ul>
    <li>
        <a href=""cnblogs.com"">部落格園</a>
        <a href=""baidu.com"">百度</a>
        <a href=""google.com"">谷歌</a>
    </li>
<ul>");
var data = dom.QuerySelectorAll("ul li a").Select(x => new
{
    Link = x.GetAttribute("href"), 
    Title = x.TextContent
}).Dump();

執行效果:

Link Title
cnblogs.com 部落格園
baidu.com 百度
google.com 谷歌

然後這些資料可以通過JSON序列化,儲存到桌面上:

File.WriteAllText(@"C:\Users\sdfly\Desktop\cnblogs.json", JsonConvert.SerializeObject(data));

在解析網頁資料時,可能還需要靈活運用正則表示式,來抓取沒那麼直觀的資訊。

頁面與頁面之間的關係

我們找到部落格園的分頁器,開啟F12開發者工具,用滑鼠定位到分頁器:

如圖,注意到,每一個頁面按鈕,都對應了一個不同的連結地址,如第2頁,對應的的連結是:/sitehome/p/2,第3頁,對應的是:/sitehome/p/3

部落格園首頁內容一共有200頁,因此只需將在每一頁拼接一個$"/sitehome/p/{頁面數碼}"即可。

程式碼與優化

根據上面的知識,可以輕鬆將部落格園首頁200頁資料爬出來:

var http = new HttpClient();
var parser = new HtmlParser();

for (var page = 1; page <= 200; ++page)
{
    string pageData = await http.GetStringAsync($"https://www.cnblogs.com/sitehome/p/{page}");
    IHtmlDocument doc = await parser.ParseDocumentAsync(pageData);
    doc.QuerySelectorAll(".post_item").Select(tag => new
    {
        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(),
    }).Dump(page);
}

執行結果如下:

多執行緒優化

這個爬蟲將200頁資料全部爬完,根據我的網速,需要76秒,工作管理員顯示如下(下載速度只有每秒1.7 Mbps

.NET/C#中,只需對此程式碼的for迴圈修改為LINQ,然後而加以使用Parallel LINQ,即可將程式碼並行化:

Enumerable.Range(1, 200)  // for迴圈轉換為LINQ
    .AsParallel()         // 將LINQ並行化
    .AsOrdered()          // 按順序儲存結果(注意並非按順序執行)
    .SelectMany(page =>
    {
        return Task.Run(async() => // 非非同步程式碼使用async/await,需要包一層Task
        {
            string pageData = await http.GetStringAsync($"https://www.cnblogs.com/sitehome/p/{page}".Dump());
            IHtmlDocument doc = await parser.ParseDocumentAsync(pageData);
            return doc.QuerySelectorAll(".post_item").Select(tag => new 
            {
                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(), 
            });
        }).GetAwaiter().GetResult(); // 等待Task執行完畢
    })

通過這個非常簡單的優化,在我的電腦上,即可將執行時間降低為14.915秒,速度快了5倍!同時工作管理員顯示網路下載流量為:

資料簡單分析

現在我們得到了部落格園首頁部落格簡要資料,我將其儲存到桌面的一個json檔案中(大家也可以試著儲存為其它格式,如資料庫中)。當然少不了分析一番。使用LINQPad,可以很輕鬆地分析這些資料,並生成圖表。

分析基礎

void Main()
{
    var data = JsonConvert.DeserializeObject<List<CnblogsItem>>(
        File.ReadAllText(@"C:\Users\sdfly\Desktop\cnblogs.json"));
}

class CnblogsItem
{
    public string TItle { get; set; }
    public int Page { get; set; }
    public string UserName { get; set; }
    public DateTime PublishTime { get; set; }
    public int CommentCount { get; set; }
    public int ViewCount { get; set; }
    public string BriefContent { get; set; }
}

我建立了一個CnblogsItem的類,用來反序列號桌面上json檔案的資料。返序列化完成後,這些資料儲存在data變數中。

什麼時間發文章瀏覽量最高?

Util.Chart(data
        .GroupBy(x => x.PublishTime.Hour)
        .Select(x => new { Hour = x.Key, ViewCount = 1.0 * x.Sum(v => v.ViewCount) })
        .OrderByDescending(x => x.Hour), 
    x => x.Hour, 
    y => y.ViewCount).Dump();

結果:

可見,每天上午9點發文章瀏覽量最高,凌晨3-4點發文章瀏覽量最低(誰會在晚上3-4點爬起來看東西呢?)

星期幾發的文章多?

Util.Chart(data
        .GroupBy(x => x.PublishTime.DayOfWeek)
        .Select(x => new { WeekDay = x.Key, ArticleCount = x.Count() })
        .OrderBy(x => x.WeekDay),
    x => x.WeekDay.ToString(),
    y => y.ArticleCount).Dump();

結果:

可見:星期一、二、三的文章最多,星期四、五逐漸冷淡,星期六、星期日最少。——但星期六比星期日又多一點。(是因為996造成的嗎?)

哪位大佬發文最多(取前9名)?

Util.Chart(data
        .GroupBy(x => x.UserName)
        .Select(x => new { UserName = x.Key, ArticleCount = x.Count() })
        .OrderByDescending(x => x.ArticleCount)
        .Take(9), 
    x => x.UserName, 
    y => y.ArticleCount).Dump();

結果:

可見,大佬周國通竟然在前200頁部落格中,獨佔54篇,我點開了他的部落格(https://www.cnblogs.com/tylerzhou/)看了一下,竟然都有相當的質量——絕無放水可言,深入講了許多.NET的測試系列教程,確實是大佬!

結語

實際應用的爬蟲可能不像部落格園這麼簡單,爬蟲如果深入,可以遇到很多很多非常有意思的情況。

今天謹希望通過這個簡單的部落格園爬蟲,讓大家多多享受寫.NET/C#程式碼的樂趣