.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 を使う
今回は、デモとして、東京都が公開している放射線データから最新のデータを抽出する機能を実装してみたいと思います。
なお、開発の際は、負荷防止も含めミラーサイトを利用するようにしましょう。
となっています。
このサイトでは1時間毎に情報が更新され、最新情報は、常にテーブルの一番上に表示される構造になっているようです。
目的の機能を実装するためには、一番上、かつ、右端の平均値(赤丸)の箇所を取得できればいいのですが、この位置にある要素を取得するにはどうすればいいでしょうか?
(執筆時点では、0.896となっています)
HAP では、XPathで位置を指定することで簡単に各種要素を抽出可能です。
が、
そもそも要素のXPath を調べること自体が大変です。
そこで、HAPExplorer を利用します。
HAPExplorer を起動し、唯一存在するFileメニューからOpen URLを選択し、開いたダイアログボックスに解析対象のURLを入力しOpenをクリックします。
しばらく待つとHTMLが読み込まれます。左下のウインドウを利用して、目的のHTMLノードを探します。
タグ要素をドリルダウンしていくと、
右下の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」実行すると、
と、値がさらっと取得できました。
とりあえず、これで完成です。
当然ですが、この手法はHTMLの構造が動的に変わるようなHTMLには適用できません・・・。その際は、ノード全体を取り込んだ上でLinqで抽出するか、一度DBに書き込んでから料理するなどすることになります。
では、ノード全体を取得するためにはどうしたらいいでしょうか。
例えば、指定するXPathを、
/html[1]/body[1]/center[1]/table[2]/tr/td[4]
などとすることで、平均値(列)全体を取得することができます([3]などと、要素を指定しているIndexを消します)。
すると、
という感じで、平均値列すべてが取得できます。
一番上のタイトル名的なものは邪魔なので、取り除きたいですね。
その際は、
var nodes = _nodes;
となっている箇所を、
var nodes = _nodes.Skip(1);
などとします。
すると、
となり、ゴミが取れました。
あとは必要な行なり、列なり取得してLinqで料理するだけです。
先日紹介したLinq to Twitter とHTML Agility Pack とAzure (WorkerRole)を利用すると、ほんと30分くらいでBot とかが作れてしまいます。