Edited at

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

More than 1 year has passed since last update.

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.Parser.Html.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.Parser.Html.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.Parser.Html.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.Parser.Html.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})");
});