酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

Windows Phone 7でSgmlReaderを使ってHTMLをスクレイピングする

WebAPIが提供されていないサイトのアプリを作っていると、スクレイピングしなければいけない事が多々あります。

SgmlReader for Silverlight/Windows Phone 7 - 酢ろぐ!で紹介したとおり、僕は neueccさん(@neuecc)がSilverlight/Windows Phone 7向けにポーティングしてくださったsgmlreader.slを使っています。

ただ、以下の様なHTMLの文書型宣言があると、HTMLパースに失敗してしまっていました。

|html| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> ||<

どうしてもパースを通したかったので、forkして修正したコードを「https://bitbucket.org/ch3cooh393/sgmlreader.sl/」へあげておきました。一応、SgmlReaderの最新版とのマージを取っておきましたが、あまりテストしていないので動かないHTMLがあったらごめんなさい。

**使い方

https://bitbucket.org/ch3cooh393/sgmlreader.sl/downloadsへアクセスしてビルドします。ビルドしたSgmlReader.WP7.dllSystem.Xml.Linq.dllを自分のプロジェクトへ参照追加します。

f:id:ch3cooh393:20141015161203p:plain

通信処理開始の開始のトリガーは適当にbutton1_Clickにハンドリングしておきます。このメソッドでは米国Amazonのランキング情報を取得しに行っています。

具体的には、WebClientクラスのOpenReadAsyncメソッドを使用して非同期でHTMLを取得しにいきます。読み込みが終わるとwc_OpenReadCompletedが呼ばれるので、StreamをSgmlReaderへ渡して、新しいXDocumentを作成します。

|cs| using System.Xml.Linq; using Sgml; using System.IO;

private void button1_Click(object sender, RoutedEventArgs e)
{
    var wc = new WebClient();
    wc.OpenReadCompleted += wc_OpenReadCompleted;
    wc.OpenReadAsync(new Uri("http://www.amazon.com/gp/bestsellers/books"));
}

void wc_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
    if (e.Error != null)
    {
        return;
    }

    using (var reader = new StreamReader(e.Result, System.Text.Encoding.UTF8))
    using (var sgmlReader = new SgmlReader { InputStream = reader })
    {
        sgmlReader.DocType = "HTML";
        sgmlReader.CaseFolding = CaseFolding.ToLower;

        // HTMLを元にXDocumentを作成。
        var doc = XDocument.Load(sgmlReader);
    }
}

||<

デバッグ実行してVSのウォッチで正しくデータがパース出来ているか確認してみてください。

f:id:ch3cooh393:20141015160949p:plain

**もっと現実に即した使い方

これで終わりなのですが、実際に本当にパース出来ているのか判らないので、ListBoxにスクレイピングした情報を表示してみましょう。簡単なヘルパークラスを作ります。

|cs| // Book.cs

namespace SgmlReaderTest { public class Book { public string Title { get; set; } public string PhotoUrl { get; set; } } } ||<

次に表示するListBoxのItemTemplateを定義します。ImageとTextBlockが横並びに配置しました。

|xml| <ListBox.ItemTemplate> </ListBox.ItemTemplate> ||<

最後に欲しい要素を探してListBox.ItemSourceに設定します。

|cs| void wc_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e) { if (e.Error != null) { return; }

using (var reader = new StreamReader(e.Result, System.Text.Encoding.UTF8))
using (var sgmlReader = new SgmlReader { InputStream = reader })
{
    sgmlReader.DocType = "HTML";
    sgmlReader.CaseFolding = CaseFolding.ToLower;

    // HTMLを元にXDocumentを作成。
    var doc = XDocument.Load(sgmlReader);
    var q = doc.Descendants("div")
        .Where(ul =>ul.Attribute("class") != null
            && ul.Attribute("class").Value == "zg_item zg_sparseListItem")
        .Descendants("img")
        .Select(el =>
        {
            var obj = new Book();
            obj.PhotoUrl = el.Attribute("src").Value;
            obj.Title = el.Attribute("title").Value;
            return obj;
        });

    listBox1.ItemsSource = q.ToList();
}

} ||<

実行してみました。

f:id:ch3cooh393:20141015160859p:plain