酢ろぐ!

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

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

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

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

ただ、以下の様な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を作成します。

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にスクレイピングした情報を表示してみましょう。簡単なヘルパークラスを作ります。

// Book.cs 

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

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

<ListBox Margin="0,75,0,0" Name="listBox1" Width="460">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal" Margin="0,0,0,17" >
                <Image Height="100" Width="100" Margin="12,0,20,0" Source="{Binding IllustUrl}" />
                <TextBlock Text="{Binding Title}" TextWrapping="Wrap" FontSize="24" Width="320" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

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

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