C#でモダンにスクレイピングするならAngleSharp


C#やVB.NETでWebページをスクレイピングする方法をWeb検索するとHtml Agility Packが見つかることが多いですが、APIはXHTMLやXPATHといったXML技術をベースにしているので、今これを使うのは少々やぼったい印象があります。

じゃあ何がいいのか?ということですが、私はAngleSharpを強くお勧めします。

AngleSharpの利点

AngleSharpのどこがいいのか?は、改めて別の記事を書くつもりですが、ここでは簡単にまとめます。

  • HTMLだけでなく、SVG、MathML、CSSもパース可能です。
  • HTMLをパースするとW3CのWeb標準に従ったDOMが構築されます。HTML5ベースのため、閉じる必要のないタグ(<br><img>など)や閉じタグを省略可能なタグ(<li><dt><dd><tr><td>など)も正しく理解します。
  • Selectors APIが使えます。document.QuerySelector()document.QuerySelectorAll()という、jQueryのように便利でかつWeb標準に従った機構でDOM要素の検索が可能です。:beforeのような疑似要素や:nth-child()のような擬似クラスだってもちろん使えます。XPathは不要です。
  • Browsing contextを実装します。
  • JavaScriptのスクリプティングエンジンを統合可能です。パーサーの初期化時にJavaScriptを有効化すれば、<script>タグに記述されたJavaScriptによるDOM操作の結果を取得することもできます。
  • LINQサポート(当然ですが)。
  • PCL(profile 259)として実装されているので、.NET FrameworkでもMonoでも.NET CoreでもXamarinでも利用できます。
  • パフォーマンス測定の結果は、HTML Agility Packよりおおむね高速です。

一言でいえば、「HTML5などの新しいWeb標準に沿ったモダンなHTML環境」といえるでしょう。

コード例1

C#でHtml Agility Packを使ってウェブサイトのタイトルを取得する - 酢ろぐ! のコードをAngleSharpを使ったものに書き換えてみましょう。

// タイトルを取得したいサイトのURL
var urlstring = "http://blog.ch3cooh.jp/";

// 指定したサイトのHTMLをストリームで取得する
var doc = default(IHtmlDocument);
using (var client = new HttpClient())
using (var stream = await client.GetStreamAsync(new Uri(urlstring)))
{
    // AngleSharp.Html.Parser.HtmlParserオブジェクトにHTMLをパースさせる
    var parser = new HtmlParser();
    doc = await parser.ParseAsync(stream);
}

// HTMLからtitleタグの値(サイトのタイトルとして表示される部分)を取得する
var title = document.Title;

Debug.WriteLine(title);

一番最初に登場する<title>要素がTitleプロパティの値として取得されます。

コード例2

C#でHtml Agility Packを使ってYahoo!ファイナンスの現在の株価を取得する - 酢ろぐ! のコードをAngleSharpを使ったものに書き換えてみましょう。

// 株価を取得したいサイトのURL
var code = "7984.T";
var urlstring = $"http://stocks.finance.yahoo.co.jp/stocks/detail/?code={code}";

// 指定したサイトのHTMLをストリームで取得する
var doc = default(IHtmlDocument);
using (var client = new HttpClient())
using (var stream = await client.GetStreamAsync(new Uri(urlstring)))
{
    // AngleSharp.Html.Parser.HtmlParserオブジェクトにHTMLをパースさせる
    var parser = new HtmlParser();
    doc = await parser.ParseAsync(stream);
}

// クエリーセレクタを指定し株価部分を取得する
var priceElement = doc.QuerySelector("#main td[class=stoksPrice]");

// 取得した株価がstring型なのでint型にパースする
int.TryParse(priceNode.TextContent, NumberStyles.AllowThousands, null, out var price);

Debug.WriteLine("コクヨ(7984.T)の株価: {0}円", price);

オリジナルのコードは要素の階層構造をもっと厳密に見ていますが、せっかくクエリーセレクタが使えるので、少し緩くしています。

コード例3

C#でHtml Agility Packを使って豊橋技科大の休講情報を取得する - 酢ろぐ! のコードをAngleSharpを使ったものに書き換えてみましょう。

// 休講情報を取得したいサイトのURL
var urlstring = "https://www.ead.tut.ac.jp/board/main.aspx";

// 指定したサイトのHTMLをストリームで取得する
var doc = default(IHtmlDocument);
using (var client = new HttpClient())
using (var stream = await client.GetStreamAsync(new Uri(urlstring)))
{
    // AngleSharp.Html.Parser.HtmlParserオブジェクトにHTMLをパースさせる
    var parser = new HtmlParser();
    doc = await parser.ParseAsync(stream);
}

// クエリーセレクタを指定して休講情報テーブル部分を取得する
var items = doc.QuerySelectorAll("#grvCancel > tr")
    .Skip(1)
    .Select(item =>
    {
        // td単位で複数のデータを取得する
        var data = item.GetElementsByTagName("td");

        // 休講日
        var date = data[1].TextContent;

        // 時限
        var period = data[2].TextContent;

        // 授業の名前
        var subject = data[3].TextContent;

        return new { Date = date, Period = period, Subject = subject };
    });

// 取得した休講情報を出力する
items.ToList().ForEach(item =>
{
    Debug.WriteLine("${item.Date}({item.Period}) {item.Subject}");
});

元コードで使っていたElementAt()メソッドはAngleSharpでも使えますが、InnerTextはAngleSharpでは使えません。

コード例4

C#でHtml Agility Packを使って秀和システムの新刊情報を取得する - 酢ろぐ! のコードをAngleSharpを使ったものに書き換えてみましょう。

// 新刊情報を取得したいサイトのURL
var urlstring = "http://www.shuwasystem.co.jp/newbook.html";

// 指定したサイトのHTMLをストリームで取得する
var doc = default(IHtmlDocument);
using (var client = new HttpClient())
using (var stream = await client.GetStreamAsync(new Uri(urlstring)))
{
    // AngleSharp.Html.Parser.HtmlParserオブジェクトにHTMLをパースさせる
    var parser = new HtmlParser();
    doc = await parser.ParseAsync(stream);
}

// 最初のsinkanがコンピュータの関連書籍
var priceElement = doc.GetElementById("sinkan");

// 必要な情報を読み取る
var listItems = priceElement.GetElementsByTagName("dl")
    .Select(n =>
    {
        // 書籍のタイトルを取得する
        var title = n.QuerySelector("dt")
            .TextContent.Trim();

        // 書籍のISBNを取得する
        var isbn = n.QuerySelector("dd > p > strong")
           .TextContent.Trim();

        return new { Title = title, Isbn = isbn };
    });


// 結果を出力する
listItems.ToList().ForEach(item => 
{
    Debug.WriteLine($"{item.Title} ({item.Isbn})");
});