tSeiya's blog

行動結果のアウトプット場

JavaによるHTML文章の解析手法

一般的な検索システムの構成は以下のようになっている。
f:id:excer:20110408230140p:image

この内、インデクサの中は「HTML文章の解析」→「テキストの分析」→「画像の分析」→「検索用データ生成」となっている。

今回は「HTML文章の解析」を行う。



そもそもHTML文章とは?

ウェブページを記述するためのマークアップ言語(=HTML)で記述された文章で、基本的な構造は

<html>
     <head>
          <title>タイトル</title>
     </head>
     <body>
          <a href="URI">あいうえお</a> 
   </body>
</html>

のように階層構造になっている。



どのようにプログラムを読み込むか

  • 2つのアプローチ

DOM(Document Object Model)
XMLを読み込む別のAPIであるSAXと異なり、XMLデータをツリー構造として扱う事ができる。ただし、通常の場合対象のXML文書を全て読み込んでからの扱いを前提とするため動作速度が遅かったり、メモリーの使用量が大きくなる欠点もある。
via Wikipedia(一部引用)

SAX(Simple API for XML)
XML文書を木構造として扱うDOMと異なり、一連のイベントとして表現するイベント駆動型のAPIである。したがって、アプリケーションソフトウェアが積極的にAPIにアクセスするDOMに対し、SAXではアプリケーションソフトウェアがイベントが来るのを待ち受ける受動的な動作が大部分を占める。
via Wikipedia(一部引用)


ここでは、HTML文章の読み込みにDOMを使う。DOMでは、文章を一度に読み込み、メモリ上にツリーを構築(DOMツリー)する。上記のhtmlをDOMツリーで表すと以下のようになる。
f:id:excer:20110408230141p:image
DOMツリーを構成しているそれぞれの四角はノードと呼ばれる。要素ノード(<>のタグで囲まれたもの)、属性ノード(hrefやaltなど)などがある。



DOMを使う

JavaでDOMを使うために、以下の二つを揃えます*1

ここからダウンロードしたものを使っても上手くいかなかったので、無視してください

揃えたら、ディレクトリを一つ作って、そこに置いておきます。ここでは、「class」というディレクトリにおいたことにします。


作ったプログラムが置いてあるディレクトリにいたとして、コンパイルを行うときは、

$javac -cp .:../class/* JavaProgram.java

実行するときは

$java -cp .:../class/* JavaProgram

のように行います*2


プログラムでは始めに、importで読み込みます。

 import org.cyberneko.html.parsers.DOMParser; //HTML読み込み用パッケージ
 import org.w3c.dom.*; //DOMツリー用パッケージ

与えられたウェブページを読み込んで、タイトルを表示する。

まず、DOMツリーを解釈するために、パーサ(構文解析プログラム)を準備する

	DOMParser parser = new DOMParser();
	parser.setFeature("http://xml.org/sax/features/namespaces", false);

setFeatureの方は、パーサの名前空間認識をオフにしています。(オンにすると一部文章の読み込みに失敗するらしい。良く分からないので、とりあえず書いときます(;・∀・))



読み込むhtmlファイルを決定します。引数があれば、第一引数をurlStrにセットし、なければあらかじめ用意しておいた"test.html"をセットします。

	String urlStr = args.length>0 ? args[0] : "test.html";
	System.out.println("SOURCE URL: " + urlStr); //urlStrを表示

「条件 ? 式1 : 式2」で、条件がtrueなら式1の値を、そうでなければ式2の値を取ります。



DOMツリーを生成し、parser内のドキュメントノードをdocumentにセット。”title”をタグ名にもつ要素を全て取得します。

	parser.parse(urlStr);
	Document document =parser.getDocument();
	NodeList nodeList = document.getElementsByTagName("title");

各要素のテキスト内容を表示します。

	for(int i=0; i < nodeList.getLength(); i++){
	    Element element = (Element)nodeList.item(i);
	    System.out.println(element.getTextContext());
	}
これで、<title>あいうえお</title>となっていた場合は、「あいうえお」というテキストが表示されます。

ページに含まれるアンカーテキスト(テキストの内容)とリンク先を表示させたい場合は、”a”タグをもつ要素と、href属性の値(URL)を取得します。

	NodeList nodeList = document.getElementsByTagName("a");

	for(int i=0; i < nodeList.getLength(); i++){
	    Element element = (Element)nodeList.item(i);
	    System.out.println(element.getTextContent() + "|" + element.getAttribute("href"));
	}
<a href="http://d.hatena.ne.jp/excer/">_level5+=Coding</a>となっていた場合は、
『_level5+=Coding|http://d.hatena.ne.jp/excer/』のように表示させます。

このままだと、アンカーテキストに空白文字が入るおそれがあるので、空白文字を削除することにします。

//テキスト内容に含まれる改行を削除(=長さ0の文字列に置換)
	    String textContent = element.getTextContent().replaceAll("\\s", "");
	    System.out.println(textContent + "|" + element.getAttribute("href"));

\\sは、正規表現で空白を意味します。*3


このコードは elementのメソッドgetTextContent()を実行して、その結果に対して、それに属するメソッドreplaceAll()を実行しています。つまり...

	    String textContent = element.getTextContent().replaceAll("\\s", "");

   String textA = element.getTextContent();
   String textContent = textA.replaceAll("\\s", "")

は等価です。



HTMLを読み込んだ後、その中に含まれる画像ファイル名、及び、その画像の周辺テキストを取得してファイルに書き込む

入力ファイルが"../data"にあるものを、出力ファイルは、ディレクトリ”../result”にして、
(入力データがtestData.htmlの場合、)”testData.image_and_text”という名前で生成させる。

	String resultFileName = urlStr.replaceAll("../data", "../result").replaceAlll("html$", "image_and_text");

上記のコードの”$”は一番最後を意味する正規表現



imgタグを持つ要素を全て取得し、画像ファイル名を出力する。ただし、画像ファイル名がない、あるいは、先頭が"http:"あるいは"file:"で始まる場合は、何も出力しないようにする。

	// "img"をタグ名にもつ要素を全て取得する
	NodeList nodeList = document.getElementsByTagName("img");
	
	// 各要素の画像ファイル名と周辺テキストを出力ファイルに出力する
	for(int i=0; i < nodeList.getLength(); i++){
	    Element element = (Element)nodeList.item(i);
	    
	    // 画像ファイル名をsrcNameにセットする
	    String srcName = element.getAttribute("src");
	    // 画像ファイル名がない、あるいは、先頭が"http:"あるいは"file:"で始まる場合は、何も出力せず、次のループへ
	    if(srcName.length()<=0 || srcName.startsWith("http:") || srcName.startsWith("file:")) {
		continue;
	    }
	}

画像ファイル名の戦闘に"../data"を付けて、周辺テキスト(imgタグのalt属性のテキスト、及び、imgタグの直接の親ノードが含むテキスト内容)を取得する。該当するテキストがない場合は、”NONE”を出力する。

	    // 画像ファイル名の先頭に"../data"をつける
	    srcName = "../data/" + srcName;

	    // altテキストをtextAltにセットする。空白は置き換えで削除
	    String textAlt = element.getAttribute("alt").replaceAll("\\s", "");
	    // 該当する周辺テキストがない場合は"NONE"をセットする	    
	    textAlt = textAlt.length()<=0 ? "NONE" : textAlt;
	    
	    // 親ノードのテキストをtextParにセットする
	    String textPar =  element.getParentNode().getTextContent().replaceAll("\\s", "");
	    // 該当する周辺テキストがない場合は"NONE"をセットする
	    textPar = textPar.length()<=0 ? "NONE" : textPar;

出力ファイルの形式を以下のようにする。
画像ファイル名 alt属性のテキスト 親ノードのテキスト

	    // 画像ファイル名、altテキスト、親ノードのテキストを指定の形式でoutTextにセット
	    String outText = srcName + "\t" + textAlt + "\t" + textPar + "\n";


ファイルへ出力する→FileOutputStreamを利用

import java.io.*;

	//指定したファイル名でFileOoutputStreamを生成
	FileOutputStream fos = new FileOutputStream(resultFileName);

	//適切な文字コードを指定して書き込めるようにする
	OutputStreamWriter osw = new OutputStreamWriter(fos , "UTF-8");

	//バッファを使って効率よく書き込めるようにする
	BufferedWriter bw = new BufferedWriter(osw);

	//書き込んでいる部分
	bw.write(outText);

	//使い終わったら、最後に開いた順にクローズしていく
	bw.close();
	osw.close();
	fos.close();

まとめ

import org.cyberneko.html.parsers.DOMParser;
import org.w3c.dom.*;
import java.io.*;

public class Practice1_01 {
    public static void main(String args[]) throws Exception{
	// DOMを解釈するパーサを準備
	DOMParser parser = new DOMParser();
	parser.setFeature("http://xml.org/sax/features/namespaces", false);
	
	// 引数があれば第1引数をurlStrにセット、なければ"test.html"をセット
	String urlStr = args.length>0 ? args[0] : "test.html";
	System.out.println("SOURCE URL: " + urlStr);
	
	// 出力ファイル名を生成
	//$は一番最後のという正規表現
	String resultFileName = urlStr.replaceAll("../data", "../result").replaceAll("html$", "image_and_text");
	System.out.println(resultFileName);
	
	// 出力ファイルをオープン
	FileOutputStream fos = new FileOutputStream(resultFileName);
	OutputStreamWriter osw = new OutputStreamWriter(fos , "UTF-8");
	BufferedWriter bw = new BufferedWriter(osw);
	
	// urlStrのファイルをパーサで読み込み、parserにDOMツリーを生成
	parser.parse(urlStr);
	// parser内のDOMツリーのドキュメントノードをdocumentにセットする
	Document document = parser.getDocument();
	// "img"をタグ名にもつ要素を全て取得する
	NodeList nodeList = document.getElementsByTagName("img");
	
	// 各要素の画像ファイル名と周辺テキストを出力ファイルに出力する
	for(int i=0; i < nodeList.getLength(); i++){
	    Element element = (Element)nodeList.item(i);
	    
	    // 画像ファイル名をsrcNameにセットする
	    String srcName = element.getAttribute("src");
	    // 画像ファイル名がない、あるいは、先頭が"http:"あるいは"file:"で始まる場合は、何も出力せず、次のループへ
	    if(srcName.length()<=0 || srcName.startsWith("http:") || srcName.startsWith("file:")) {
		continue;
	    }
	    // 画像ファイル名の先頭に"../data"をつける
	    srcName = "../data/" + srcName;

	    // altテキストをtextAltにセットする。空白は置き換えで削除
	    String textAlt = element.getAttribute("alt").replaceAll("\\s", "");
	    // 該当する周辺テキストがない場合は"NONE"をセットする	    
	    textAlt = textAlt.length()<=0 ? "NONE" : textAlt;
	    
	    // 親ノードのテキストをtextParにセットする
	    String textPar =  element.getParentNode().getTextContent().replaceAll("\\s", "");
	    // 該当する周辺テキストがない場合は"NONE"をセットする
	    textPar = textPar.length()<=0 ? "NONE" : textPar;
	    
	    // 画像ファイル名、altテキスト、親ノードのテキストを指定の形式でoutTextにセット
	    String outText = srcName + "\t" + textAlt + "\t" + textPar + "\n";

	    // outTextを出力ファイルに出力
	    bw.write(outText);
	}
	// 出力が完了したら出力ファイルを閉じる
	bw.close();
	osw.close();
	fos.close();
    }
}

参考:第1回 正しいHTMLとドキュメントツリーを理解しよう via @IT自分戦略研究所

*1:もしかしたら、MeCab.jarというのが必要かもしれない

*2:「-cp」はクラスパスです。「:」は複数のパスをつなげます。「*」はjarファイル(クラスなどをまとめたパッケージ)を読み込むために付けます。

*3:\は環境によって、「円マーク」であったり、「バックスラッシュ」であったりします。