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

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

じゃあ何がいいのか？ということですが、私は[AngleSharp](https://anglesharp.github.io/)を強くお勧めします。

[![](https://raw.githubusercontent.com/AngleSharp/AngleSharp/master/header.png)](https://anglesharp.github.io/)

## AngleSharpの利点

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

- HTMLだけでなく、SVG、MathML、CSSもパース可能です。
- HTMLをパースすると[W3CのWeb標準](https://www.w3.org/TR/html51/syntax.html#parsing-html-documents)に従ったDOMが構築されます。HTML5ベースのため、閉じる必要のないタグ（`<br>`や`<img>`など）や閉じタグを省略可能なタグ（`<li>`、`<dt>`や`<dd>`、`<tr>`や`<td>`など）も正しく理解します。
- [Selectors API](http://standards.mitsue.co.jp/resources/w3c/TR/selectors-api/)が使えます。`document.QuerySelector()`や`document.QuerySelectorAll()`という、jQueryのように便利でかつWeb標準に従った機構でDOM要素の検索が可能です。`:before`のような疑似要素や`:nth-child()`のような擬似クラスだってもちろん使えます。XPathは不要です。
- [Browsing context](https://developer.mozilla.org/en-US/docs/Glossary/Browsing_context)を実装します。
- [JavaScriptのスクリプティングエンジンを統合可能です](https://www.nuget.org/packages/AngleSharp.Scripting.JavaScript/)。パーサーの初期化時にJavaScriptを有効化すれば、`<script>`タグに記述されたJavaScriptによるDOM操作の結果を取得することもできます。
- LINQサポート（当然ですが）。
- PCL（profile 259）として実装されているので、.NET FrameworkでもMonoでも.NET CoreでもXamarinでも利用できます。
- [パフォーマンス測定](https://github.com/AngleSharp/AngleSharp/wiki/Performance)の結果は、HTML Agility Packよりおおむね高速です。

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

## コード例1

[C#でHtml Agility Packを使ってウェブサイトのタイトルを取得する - 酢ろぐ！](http://blog.ch3cooh.jp/entry/20141201/1417397400) のコードをAngleSharpを使ったものに書き換えてみましょう。

```csharp
// タイトルを取得したいサイトの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!ファイナンスの現在の株価を取得する - 酢ろぐ！](http://blog.ch3cooh.jp/entry/20141202/1417483800) のコードをAngleSharpを使ったものに書き換えてみましょう。

```csharp
// 株価を取得したいサイトの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を使って豊橋技科大の休講情報を取得する - 酢ろぐ！](http://blog.ch3cooh.jp/entry/20141203/1417539847) のコードをAngleSharpを使ったものに書き換えてみましょう。

```csharp
// 休講情報を取得したいサイトの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を使って秀和システムの新刊情報を取得する - 酢ろぐ！](http://blog.ch3cooh.jp/entry/20141204/1417656600) のコードをAngleSharpを使ったものに書き換えてみましょう。

```csharp
// 新刊情報を取得したいサイトの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})");
});
```