Javaの理論と実践: XQueryによるscreen-scraping

XQueryでHTML抽出と変換を手軽に

XQueryは、XML文書から情報を抽出するためのW3C標準であり、現在まで14のワーキング・ドラフトが作られています。XQueryの主な関心事は、半構造化文書データ(semi-structured document data)の巨大なデータベースに対するクエリーに関するものですが、XQueryはもっと平凡な使い方に対しても、驚くほど効果的なのです。今回のJavaの理論と実践では、コラムニストのBrian Goetzが、XQueryをHTMLのscreen-scrapingエンジンとして効果的に使う方法を解説します。

Brian Goetz (brian@quiotix.com), Principal Consultant, Quiotix

Brian Goetz は18 年間以上に渡って、専門的ソフトウェア開発者として働いています。彼はカリフォルニア州ロスアルトスにあるソフトウェア開発コンサルティング会社、Quiotixの主席コンサルタントであり、またいくつかのJCP Expert Groupの一員でもあります。2005年の末にはAddison-Wesleyから、Brianによる著、Java Concurrency In Practiceが出版される予定です。Brian著による有力業界紙に掲載済みおよび掲載予定の記事のリストを参照してください。



2005年 3月 22日

先月のことですが、Java™技術の権威者、Sam Pullaraが私に、彼が携わっている最新のJava対応携帯電話機、Nokia 6630を見せてくれました。これには、埋め込みのJVM、GRPS、Bluetoothなど、最新技術が満載されています。しかしこのモデルも、スマートフォンを苦しめる毎度の問題、つまり画面サイズの制約と無縁ではありません。一部のWebサイトでは電話ベースのブラウザーをサポートしており、また埋め込みのブラウザーは、小さな画面でも効果的にページを描画しようとしていますが、典型的なWebページを携帯電話機の画面で見ようとすると、象を車の後部座席に押し込むようなことになります(これには誰もが不満です。あなたも車も、そして象も)。Samは、お好みのWebサイトからのデータを小さな画面に合わせて再フォーマットするための、単純で優雅なscreen-scraping手法を構築したのです。

賢明な手法

HTML文書からデータを抽出する手法としては、いくつかのものがあります。私はSamの採用した手法に感心しました。つまりXQueryを、screen-scraping(ページから必要なデータを抽出する)ツールとして、かつスタイルシート(スクロールしなくてもうまくページに収まるように、データを再フォーマットする)として使うのです。わずかなインフラと非常に単純なXQuery表現を使うことによって、無数のデータ・ソースから、必要なデータ(つまり交通情報や天気、金融関係データ)を抽出し、携帯電話機にすっきりと表示することが可能になります。

HTMLページをscreen-scrapeすることが、ある特定な問題に対して妥当と思われる状況に至った経験は私にも何度もありますが、screen-scrape用のJavaベースのツールキットはほとんどありませんでした。HTMLの構文解析ツールは数多くありますが、一般的に言って、抽象化機能が不十分です(そのためscreen-scrapingコードが汚くなります)。また、規格を無視したHTMLの氾濫のために制限を受けており、時間と共に構造が変化する、動的生成ページへの対応もお粗末です。

低品質のHTMLと、豊富なXML処理ツール・セットの隙間を埋めるためには、まずHTMLをXMLに変換する必要があります。このためにいくつかのツールが役に立ちます。JTidyツールキットはうまく仕事をこなし、簡単に使えます。JTidyは、典型的な品質の(つまり低品質の)HTMLを読み込み、なにがしかきれいなもの(いくつかのオプションを選択できます)を出力するように作られています。またDOMインターフェースも用意されており、HTML文書をトラバースしてXMLパーサーに渡すことができます。リスト1のコードは、InputStreamからHTML文書を読み込み、この文書のDOM表現を生成するものです。

リスト1. JTidy で、HTMLをXML互換のDOMに変換する
Tidy tidy = new Tidy(); 
tidy.setQuiet(true);
tidy.setShowWarnings(false);
Document tidyDOM = tidy.parseDOM(inputStream, null);

この単純な変換で、ほとんどどんなWebページも、XML文書として処理することができ、お好みのデータ抽出用XMLツール(SAXであれXSLであれXPath、どれでも)を適用することができます。XSL はXML文書から情報を抽出し、表示用に変換するために作られていることから、XSLを選ぶのが自明かもしれませんが、XSLは初めての人には非常に学ぶのが難しく、単純なXSL変換でも極めて複雑になります。XPathは、抽出部分(XSLもXQueryも、内容選択のために使います)に関しての有力候補です。XPathを使って必要なデータを引き出すことは簡単であり、その後で自分でHTMLをフォーマットできるのですが、XQueryを使った方が、もっと簡単なのです。


XQuery: (呆れるほど)簡単な紹介

XQueryは、XMLデータベース(潜在的には非常に巨大である可能性があります)から、データを抽出するために作られました。入力データセットは必ずしもXML文書である必要はありませんが、XML文書である可能性もあります。あるいは、インデックスを付けられ、XMLデータベースの中に保存された文書の集合であるかも知れず、さらには、リレーショナル・データベース中のテーブル・セットかも知れません。SQLと同様、XQueryには、複数のデータセットからデータを抽出し、要約し、集約し、連結するためのファンクションがあります。

JSPやASP、あるいはVelocityなどのプレゼンテーション・テンプレート言語と同様、XQuery は、プレゼンテーション領域と計算領域という2つの領域からの要素を組み合わせ、単一の複合構文にします。その結果、どんなXML文書も既に(自分自身への評価を行う)有効なXQuery表現となっています。また、「for」や「let」などの言語ステートメントもあり、XML要素と混在させることができます。

リスト2は、サンプルのXML文書、bib.xmlです。これは参考文献(bibliography)を表現しています。XQueryで何ができるかを示すために、XQuery表現をいくつか、簡単にお見せしましょう。その後で、screen-scrapingの例に移ります。構文変換やXQueryのユース・ケースを説明するには何百ページも必要ですが、これについては詳細な参考資料や例を参考文献に挙げてありますので、そちらを見てください。

リスト2. XML参考文献の例
<bib>
    <book year="1994">
        <title>TCP/IP Illustrated</title>
        <author><last>Stevens</last><first>W.</first></author>
        <publisher>Addison-Wesley</publisher>
        <price> 65.95</price>
    </book>
    . . .  more books . . . 
</bib>

リスト3は、1991年以降にAddison-Wesley社から出版された全書籍を選択し、その表題を抽出し、その表題を箇条書き(<ul>)リストにフォーマットするXQuery表現を示しています。「プレゼンテーション・モード」(<ul> タグや<li> タグなど、出力に直接渡されるデータ)から「コード・モード」へのモード切り換えは、丸括弧で示されています。「コード・モード」から「プレゼンテーション・モード」への暗黙的モード切り換えは、return文の直後に起こります。

リスト3. クエリー基準に従って本の表題を選択するXQuery表現
<ul>
{
  for $b in doc("bib.xml")/bib/book
  where $b/publisher = "Addison-Wesley" and $b/@year > 1991
  return
    <li>{ data($b/title) }</li>
}
</ul>

このクエリー構文は、「for」(しばしば「Flower表現*」と呼ばれます)で開始し、文書からXMLノードのシーケンスを選択します。この場合では、XPath表現を使ってbib.xml文書からの一連の<book>ノードを選択し、さらにこうしたノードをフィルターに通し、指定したクエリー基準(出版社がAddison-Wesleyで、出版年が1991)に一致するものを取り出します。こうしたノードそれぞれに対して、return文の中で表現を計算します。ここでは、マークアップ(<li>タグ)とコード(各<book> ノードの<title>要素の内容を抽出する)の混合です。
* Flowerはfor-let-where-order-returnの略。XQueryで「どこから選択するか」を表す表現。

この簡単な例は、XQueryの持つ、いくつかの面を説明しています。つまり、一つの文書の中にプレゼンテーションとコードを混合できること、XPathを使うこと、置換($b 参照)を使うこと、単純ではないクエリー表現、XQueryファンクション(data())、また、出力文書の構造は入力文書の構造と一致する必要がないという事実、などです。つまり、非常に簡潔な、さほど困難なく読めるクエリーにもかかわらず、大きな処理能力があるということです。

リスト4は、もっと単純なXQuery表現を示しています。これは、参考文献の中に含まれている個別の出版社の数を、単一の<count> 要素の中に出力するものです。前の例と同様、XPath表現を使って一連のノードを選択し、XQueryファンクションを適用して個別の値を選択し、ノード数をカウントしています。そして数(bib.xml文書の中にある個別の出版社の数)に対して評価しています。

リスト4. 個別の出版社の数をカウントするXQuery表現
<count>
{
  let $d := distinct-values(doc("bib.xml")/book/publisher)
  return count($d)
}
</count>

こうした例は、XQueryによって実行可能なクエリー・タイプの、ごく表面を引っ掻いたにすぎません。XQueryでどんなことができるか、そして皆さんが選んだフォーマットへとXML文書を変換するためにXQueryをどう使うべきかを皆さんに知ってもらうために、これらを紹介しました。XQueryの力の大部分は、巨大な文書ベースやその他のデータ・ソースへのクエリーを目的としたものですが、XQueryの非常に小さなサブセットを使うと、HTML文書をscreen-scrapeして様々なアプリケーション(携帯電話機など画面が制限されたデバイス上に必要なデータを表示する、など)に対して必要な部分を抽出したり、複数サイトからのデータを集約、表示するようなDIY(do-it-yourself)ポータルを作ったりできるようになります。


XQueryを使ったscreen-scraping

Webページをscreen-scrapeする上での困難は、Webページには自動識別できるような構造が無く、その構造も、内容が編集されるにつれ変更される可能性があり、別のリクエストによって別の動的コンテンツ(広告など)がページに入れ込まれる可能性もある、という点です。その結果、抽出すべきデータに対応するのはページのどの部分か、見当をつけざるを得ない場合が頻繁に出てきます。

株価

まず、現在のIBMの株価をYahoo! Financeページ(http://finance.yahoo.com/q?s=IBM)から抽出することから始めましょう。このページには、ニュースの見出しや広告、金融データなど、種々雑多なものが含まれています。しかし私が欲しいのは株価であり、これは表の中の、「Last Trade(最後の取引)」があるセルの隣のセルにあります。リスト5のクエリーは、テキストに「Last Trade」を含む<td> ノードをすべて選択し、その一つ一つ(私が期待しているのは一つだけです)に対して、次の<td> ノードの内容を含むテーブル行を出力します。この内容は、return文にあるdata() ファンクションによって抽出されます。そうしないと、<td> ノードのテキストだけではなく、全てのマークアップまで取得してしまいます。(このクエリーで唯一細工が要るのは、text()[1]の部分です。ここで何が行われているかというと、text() ファンクションが、<td> 要素内にある全テキスト・ノードの一致確認をしているのです。この場合は一致するのは一つだけですが、XQueryにはそれが分かりません。ですからXQueryに対してさらに、最初のテキスト・ノードでテキスト一致を試す前に、最初のテキスト・ノードを選択するように伝える必要があります。)そのページがテキスト「Last Trade」を中に持つテーブル・セルを含む限り、そしてその次のセルが株価を含む限り、クエリーをフェールさせることなく自由にページ構造を変更することができます。

リスト5. Yahoo! Financeから株価を抽出するXQuery表現
<table>
{
  for $d in //td
  where contains($d/text()[1], "Last Trade")
  return <tr><td> { data($d/following-sibling::td) } </td></tr>
}
</table>

天気

もう一つ、別のページを試してみましょう。Yahoo! Weatherページには、いくつかのポートレット・パネルがあります。ここから、リストアップされている都市の名前と天気、そしてアイコンを抽出します。(Yahoo! Weatherページ、http://weather.yahoo.comは、Yahoo! にログインしている場合には皆さんがMy Yahoo! で選択した都市の天気を表示し、ログインしていなければ、主な都市の天気を表示します。)リスト6に示すクエリーは、テキスト「New York, NY」を含むサブパネルを検索した後、中に「New York, NY」を含むテーブルまでナビゲートし、そのテーブルの全行を選択します。

リスト6. Yahoo! Weatherから天気情報を抽出するXQuery表現
<table>
{
  for $d in //td[contains(a/small/text(), "New York, NY")]
  for $row in $d/parent::tr/parent::table/tr
  where contains($d/a/small/text()[1], "New York")
  return <tr><td>{data($row/td[1])}</td> 
           <td>{data($row/td[2])}</td>              
           <td>{$row/td[3]//img}</td> </tr>
}
</table>

次に各行に対して、関係のある3つのデータ・カラム、つまり都市名、温度、アイコン、の抽出を行い、この情報のみを含む、単純化したテーブルを出力します。そうすると、関心のある都市の天気情報が、小さな画面に適した形でコンパクトに表示されます。その結果を下記に示します。

シカゴ(イリノイ州)49...63 FPartly Cloudy
ロンドン(イギリス)32...41 FFair
ニューヨーク(ニューヨーク州)36...44 FCloudy
サンフランシスコ(カリフォルニア州)52...67 FPartly Cloudy

このクエリーは、リスト5のクエリーほど堅牢ではありません。このクエリーでは、テキスト「New York, NY」がsmall要素の内部にあると想定しています。(small要素は、Yahoo! が次にページを再構成するときに簡単に変更される可能性のあるマークアップのようなものです。)また、「New York, NY」は、天気情報用のページで一ヶ所以上現れる可能性が多分にあります。しかし、こうした危険要素は、クエリーの開発にもっと時間をかければ、ずっと小さくすることができます。開発における選択肢の多くと同様、クエリーの複雑さと安定性は二律背反しがちなのです。

リスト5リスト6に示した例だけが、こうしたクエリーをキャストする方法ではありません。もっと複雑なXPath構文を使えば、リスト6にある2つのfor文を一つのXPath表現にまとめることができ、またFLOWER構文を使う代わりに、リスト5全体をXPath表現としてキャストすることができます。皆さんがXPathの権威者ならば、よりXPath指向な手法を使った方が容易だと思うでしょう。一方、SQL経験が豊富な人であれば、FLOWER構文の方が魅力的に見えるでしょう。

ツール

HTMLページに対してXQuery表現を実行するために必要なコードの量は、驚くほどわずかです。JTidyライブラリーを使えば、HTML文書をクリーンアップし、それをDOMオブジェクトとして表現することができます(リスト1を参照)。文書のDOMオブジェクトに対してクエリーをコンパイルし、実行するためにはSaxon XQueryエンジンを使いました。文書のDOM表現に対してXQuery表現をコンパイルし、実行するためには、6行のコードしか必要ありません。これをリスト7に示します。

リスト7. SaxonでXQuery表現をコンパイルし、実行するためのコード
Configuration c = new Configuration();
StaticQueryContext qp = new StaticQueryContext(c);
XQueryExpression xe = qp.compileQuery(query);
DynamicQueryContext dqc = new DynamicQueryContext(c);
dqc.setContextNode(new DocumentWrapper(tidyDOM, url, c));
List result = xe.evaluate(dqc);

クエリー評価の結果はDOMElementListであり、お好みの(いや、皆さんが一番嫌いでない)DOM操作手法を使って、クエリー結果を文書に変換することができます。

XQueryの実装には他にも、無料、商用を含めて数多くあります。どこを探すべきかを、参考文献に挙げましたので、そちらを見てください。


まとめ

XQueryは巨大な文書ベースをクエリーするように作られていますが、単純な文書の変換にも好適なツールとして使用できます。小さな画面用に複雑なページを単純化するために使うこともでき、また複数ページから要素を抽出して自家製ポータルに集約するために使うこともできます。あるいは、データをプログラム的に取得する方法が他にない場合に、単純にWebページからデータを抽出するために使うこともできます。XQueryを使うことによって、比較的容易な方法でHTMLページをscreen-scrapeし、必要なデータを得ることができるのです。

参考文献

  • Nicholas Chaseによるチュートリアル、Process XML using XML Query(developerWorks, 2002年9月)は、XQueryの使い方と構文について深く追求しています。
  • Sam Pullaraのblogでは、彼の携帯電話機について読むことができます。
  • SourceForgeにあるホームページからJTidyをダウンロードしてください。
  • Saxon XQueryとXSL実装について調べてください。
  • XQueryの正式仕様は、W3C siteからダウンロードすることができます。このページでは、一連のXQuery実装もホストしています。
  • XQuery tutorialにあるスライド・セットには、XQueryがどのように役立つか、どのように使うのかを示す好例が数多く含まれています。
  • Java技術についてさらに学ぶために、developerWorksのJavaゾーンをご覧ください。技術資料やハウ・ツー記事、教育資料、ダウンロード、製品情報、その他豊富な資料が用意されています。
  • XMLについてさらに学ぶために、developerWorksのXMLゾーンをご覧ください。Javaゾーンと同様、技術資料やハウ・ツー記事、教育資料、ダウンロード、製品情報、その他豊富な資料が用意されています。
  • New to Java technologyには、Javaプログラミングを始めるための助けとなる最新情報が用意されていますので、ご覧ください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=218552
ArticleTitle=Javaの理論と実践: XQueryによるscreen-scraping
publish-date=03222005