オルタナティブ・ブログ > ビジネスをデザインするブログ >

事業開発ほどクリエイティブな行為は他に無いと思いこんでいる人間の日常

.NET 用HTMLパーサ「HTML Agility Pack」 はなかなかイケテる。

»

Bot 作成の技術メモ。

いよいよMix11の開幕が迫ってきました。お誘いを受けるも行けずじまい。

個人的にはSilverlight をHTML5に変換するツールを出してほしい・・・。というかExpression Blend をHTML5オーサリングに対応させてほしい今日この頃。

さて、そんなわけで世の中最新のWeb技術で盛り上がっておりますが、Bot や検索エンジンなどを作成しようと思うと、古いスタイルのHTMLから特定の情報を抽出しなければならないことが多々あります。ちゃんとしたXHTMLやシンプルなHTMLだとLinq to XML で処理してもいいのですが、なかなか実務ではそうは問屋がおろしません。

そんな時、重宝するのがHTML Agility Pack (HAP) です。

HAPは、.NETでHTMLをパースするというかXPathでHTMLをいじるための便利なツールです。最新版ではLinqによる操作にも対応し、便利さが向上したようです。また、HAP自体ももちろん便利なのですが、デモも兼ねた補助ツールであるHAPExplorer と組み合わせて使うとさらに便利です。

というわけで、簡単に使用方法をメモしておきます。

まず、なにはなくとも、HAPとHAPExplorerをダウンロードしてきます。

さて、コーディング!といきたいところですが、まずはHAPExplorer の使用方法から見てみます。

HAPExplorer を使う

今回は、デモとして、東京都が公開している放射線データから最新のデータを抽出する機能を実装してみたいと思います。

なお、開発の際は、負荷防止も含めミラーサイトを利用するようにしましょう。

となっています。

001

このサイトでは1時間毎に情報が更新され、最新情報は、常にテーブルの一番上に表示される構造になっているようです。

目的の機能を実装するためには、一番上、かつ、右端の平均値(赤丸)の箇所を取得できればいいのですが、この位置にある要素を取得するにはどうすればいいでしょうか?

(執筆時点では、0.896となっています)

HAP では、XPathで位置を指定することで簡単に各種要素を抽出可能です。

が、

そもそも要素のXPath を調べること自体が大変です。

そこで、HAPExplorer を利用します。

HAPExplorer を起動し、唯一存在するFileメニューからOpen URLを選択し、開いたダイアログボックスに解析対象のURLを入力しOpenをクリックします。

002

しばらく待つとHTMLが読み込まれます。左下のウインドウを利用して、目的のHTMLノードを探します。

タグ要素をドリルダウンしていくと、

003

右下のXPathの欄に/html[1]/body[1]/center[1]/table[2]/tr[3]/td[4] と表示されるところで、0.0896という数字に行きつきます。どうやら、ここが目的とする要素の位置のようです。

追記:

4月12日の8時くらいにHTMLのフォーマットが変わってしましましたようです。考え方は同じなので適宜変更お願いします。

どうやら、新しいPathは、

/html[1]/body[1]/div[1]/table[3]/tr[3]/td[4]

みたいです。

このXPathは、コーディング中に使用するのでメモしておきます。

余談ですが、このページはたまたまUTF-8なので問題なく表示されていますが、HTMLのエンコードがUTF-8以外の場合、日本語が文字化けしてしまいます・・・。それが唯一の欠点でしょうか・・・。

ひとまずHAPExplorer はこの辺にして、次はVSでコードを書きます。

HTML Agility Pack を利用する

VSを起動し、新規プロジェクトを作成します。ここではWindowsApplication を利用します。そしてFormに1つのTextBoxとButton を配置しました。TextBox はMultiLineをTrue にしてあります。

まずは、HtmlAgilityPack.1.4.0フォルダ中のHtmlAgilityPack.dll を参照に追加します。Azureの場合はCopy Trueを忘れずに。

コードはButtonのクリックイベントに記述することにします。

で、コードですが、全く難しくありません。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using System.IO;
using System.Net;

//using HtmlAgilityPack;

namespace HAPTest
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            
//HTMLを読み込む
            string source_url = "
http://ftp.jaist.ac.jp/pub/emergency/monitoring.tokyo-eiken.go.jp/report/report_table.do.html";

            WebClient wc = new WebClient();
            Stream st = wc.OpenRead(source_url);
            Encoding enc = Encoding.GetEncoding("utf-8");
            StreamReader sr = new StreamReader(st, enc);
            string html = sr.ReadToEnd();
            sr.Close();
            st.Close();

            //UTF-8ならこれ1行でもOK
            //string _html = wc.DownloadString(source_url);

            //HTMLを解析する
            HtmlAgilityPack.HtmlDocument doc = new HtmlAgilityPack.HtmlDocument();
            doc.LoadHtml(html);

            //XPathで取得するNodeを指定
            HtmlAgilityPack.HtmlNodeCollection _nodes = doc.DocumentNode.SelectNodes("
/html[1]/body[1]/center[1]/table[2]/tr[3]/td[4]");

            //これ別にいらないが・・・
            var nodes = _nodes;

            //表示
            string value = "";
            foreach (var node in nodes)
            {
                value = node.InnerText + "\r\n";
                textBox1.Text += value.ToString();
            }
        }
    }
}

めんどいので、全部コピペ。

ポイントはとりあえず赤字のところです。先ほどHAPExplorer で特定したXPathを使用します。また、HtmlAgilityPack をusingで取り込むと、Windows.Form.* 中のHtmlDocument クラスなどと名前がぶつかり警告が出てしまうので、using せず、フルパスで指定しています。

タグの中身はInnerTextとして取得できます。

これで「F5」実行すると、

004

と、値がさらっと取得できました。

とりあえず、これで完成です。

当然ですが、この手法はHTMLの構造が動的に変わるようなHTMLには適用できません・・・。その際は、ノード全体を取り込んだ上でLinqで抽出するか、一度DBに書き込んでから料理するなどすることになります。

では、ノード全体を取得するためにはどうしたらいいでしょうか。

例えば、指定するXPathを、

/html[1]/body[1]/center[1]/table[2]/tr/td[4]

などとすることで、平均値(列)全体を取得することができます([3]などと、要素を指定しているIndexを消します)。

すると、

005

という感じで、平均値列すべてが取得できます。

一番上のタイトル名的なものは邪魔なので、取り除きたいですね。

その際は、


var nodes = _nodes;

となっている箇所を、

var nodes = _nodes.Skip(1);

などとします。

すると、

006

となり、ゴミが取れました。

あとは必要な行なり、列なり取得してLinqで料理するだけです。

先日紹介したLinq to Twitter とHTML Agility Pack とAzure (WorkerRole)を利用すると、ほんと30分くらいでBot とかが作れてしまいます。

Comment(0)