2013年03月03日

Webページの本文(記事)抽出エンジン - Boilerpipe Javaライブラリの特徴と使い方

普段何気なく眺めているWebページですが、プログラミングであるページのメイン・コンテンツ(記事)だけを抽出しようとすれば、パーサーには次に示すようなロジックの実装が必要になります。

  • HTMLの中からタグ、属性情報を利用して本文位置・要素(Element)を推測する
  • 本文と推測した要素内にある不自然な子要素(例えば広告コンテンツ)を削除する
  • 推測した本文要素から各種タグ情報を削除し、プレーンテキスト化する

現時点でこれらの条件を完璧に満たすフリー・ソフトウェアは見当たりませんが、一般的に主流にあるのはサイト(ドメイン)毎に本文位置を表すタグ・属性情報を正規表現で定義し、その要素の内側にある全てのテキストを本文と判断する手法のようです(Rule-based Scraping)。

今日は希薄な単語に共通な特徴を確率モデルを使って本文抽出にアプローチするBoilerpipeライブラリを使ってWebページのデータ化にチャレンジみます。

Boilerpipe のダウンロードと設定


Boilerpipeライブラリのダウンロードはこちら。
ダウンロードしたboilerpipe-x.x.x-bin.tar.gzに含まれるboilerpipe-1.2.0.jar、lib/nekohtml-1.9.13.jar、lib/xerces-2.9.1.jarをクラスパスに追加すれば準備はおしまい。

bp01


Boilerpipe の概要と使い方


Boilerpipeではまず、与えられたURLからテキストを含むタグ単位で分離し、これをTextBlockと呼ばれるテキスト集合に変換します。また、このTextBlockオブジェクトにはテキスト集合を構成するタグ、スペースなどでより細かく分解し、これを基準に算出した単語数、アンカーテキスト及び非アンカーテキストの密度なども含んでおり、ExtractorオブジェクトはTextBlockから本文抽出を行います。

fulltext

Extractorには幾つか基本となるクラスが用意されており、それぞれの内部では複数のFilterオブジェクトを組み合わせることでそのExtract特性を決定しています。

難しい話にも聞こえますが、標準で用意されている幾つかのExtractorを使えば非常に簡単に本文抽出を行うことが出来ますよ。

例えば、一般的なウェブ・ページの構造と相性の良いArticleExtractorを使う場合のコードは次の通り。

 public void extractText(String url) throws MalformedURLException,
   BoilerpipeProcessingException {
  //String txt = CommonExtractors.DEFAULT_EXTRACTOR.getText(new URL(url));
  String txt = CommonExtractors.ARTICLE_EXTRACTOR.getText(new URL(url));
  //String txt = CommonExtractors.LARGEST_CONTENT_EXTRACTOR.getText(new URL(url));
  //String txt = CommonExtractors.CANOLA_EXTRACTOR.getText(new URL(url));
  
  System.out.println(txt);
 }

ページに含まれる本文テキストだけが出力されたはずです。

ここにあげた4つのExtractorの何れかを使えばかなりの割合でWebページの本文テキスト抽出が行えます。

え、プログラミン無しで今直ぐ試してみたい?、ではこちらのオンライン・ページでお好みのURLを使って確認してみて下さい。
ちなみに、このページではOutput Modeも指定できます。これをプログラミンで実装する場合には次のようなコードになります。

 public void extractByArticle(String url)
   throws BoilerpipeProcessingException, IOException, SAXException {
  
  //HTMLHighlighter hh = HTMLHighlighter.newHighlightingInstance();
  HTMLHighlighter hh = HTMLHighlighter.newExtractingInstance();
  String txt = hh.process(new URL(url), CommonExtractors.ARTICLE_EXTRACTOR);
  
  System.out.println(txt);
 }

この例ではArticleExtractorを指定し、本文だけで構成されるHTMLコードを出力しています。


気象庁 - 過去気象データ Extractor, Filter を作ってみる


基本Extractorの特性・使い方が分かったところで、最後に Custom Extractor を作ってみます。

ターゲットとなるWebページは気象庁が提供する過去気象データ。ここはそのHTML構造・構成に特徴が少ない為、組込みのExtractorでは目的のデータ(テキスト)だけを抽出することが難しいページです。

まずは、ExtractorBaseを継承した独自のExtractorクラスを作成し、process()メソッドを継承します。

public class WetherDataExtractor extends ExtractorBase {

 private BoilerpipeFilter weatherFilter = null;
 
 public WetherDataExtractor(BoilerpipeFilter filter) {
  weatherFilter = filter;
 }
 
 @Override
 public boolean process(TextDocument doc)
   throws BoilerpipeProcessingException {

  return TerminatingBlocksFinder.INSTANCE.process(doc)
    | new DocumentTitleMatchClassifier(doc.getTitle()).process(doc)
    | NumWordsRulesClassifier.INSTANCE.process(doc)
    | ExpandTitleToContentFilter.INSTANCE.process(doc)
    | weatherFilter.process(doc);
 }

}

ここではArticleExtractorと同様のFilterと気象データ用のweatherFilterを使って本文抽出を行なっていますが、と全く同じもので、まだ気象データを抽出することは出来ません。

続いて、process()メソッドで受け取るTextDocumentオブジェクトから本文を抽出するフィルター(BoilerpipeFilterインタフェースを実装したクラス)を作成しましょう。ここでは過去気象データのうち、10分ごと日ごとのページから時間(日付)、気温、湿度、降雨量を抜き出し、新たなTextBlockオブジェクトを作成するJapanMAフィルター(テキストはCSV形式)を作成します。

public class JapanMA implements BoilerpipeFilter {

 public static final int MINUTELY_10 = 600;
 public static final int DAILY = 86400;

 @Override
 public boolean process(TextDocument doc)
   throws BoilerpipeProcessingException {

  boolean hasChanged = false;

  // detect step type
  int stepType = -1;
  if (doc.getTitle().indexOf("10分ごと") != -1) {
   stepType = MINUTELY_10;
  } else if (doc.getTitle().indexOf("日ごと") != -1) {
   stepType = DAILY;
  }

  // filter contents
  List<TextBlock> textBlocks = doc.getTextBlocks();
  TextBlock temp, humi, rain;
  List<TextBlock> weatherBlocks = new ArrayList<TextBlock>();
  for (TextBlock tb : textBlocks) {
   TextBlock weatherBlock = null;
   switch (stepType) {
   case MINUTELY_10:
    if (tb.getTagLevel() > 6 && tb.getText().indexOf(":") != -1) {
     rain = textBlocks.get(textBlocks.indexOf(tb) + 3);
     temp = textBlocks.get(textBlocks.indexOf(tb) + 4);
     humi = textBlocks.get(textBlocks.indexOf(tb) + 5);
     weatherBlock = new TextBlock(tb.getText() + ","
       + temp.getText() + "," + humi.getText() + ","
       + rain.getText());
    }
    break;
    
   case DAILY:    
    if (tb.getNumWordsInAnchorText() > 0 && isNumber(tb.getText())) {
     rain = textBlocks.get(textBlocks.indexOf(tb) + 3);
     temp = textBlocks.get(textBlocks.indexOf(tb) + 6);
     humi = textBlocks.get(textBlocks.indexOf(tb) + 9);
     weatherBlock = new TextBlock(tb.getText() + ","
       + temp.getText() + "," + humi.getText() + ","
       + rain.getText());
    }
    break;
    
   default:
    break;
   }

   if (weatherBlock != null) {
    weatherBlock.setIsContent(true);
    weatherBlocks.add(weatherBlock);
   }
  }
  
  if (weatherBlocks.size() > 0) {
   textBlocks.addAll(weatherBlocks);
   hasChanged = true;
  }

  return hasChanged;
 }

 private boolean isNumber(String str) {
  try {
   Integer.parseInt(str);
   return  true;
  } catch (NumberFormatException nme) {
  }

  return false;
 }

}

時間情報を含むTextBlockを特定し、その位置から意図するデータを静的に抜き出し、最後に新たなTextBlockを生成、setIsContent()にtrueを設定後、本来のTextBlockリストに追加しています。

これを先ほど用意したWetherDataExtractorのコンストラクタで設定すれば、独自Extractorの完成です。

String txt = new WetherDataExtractor(new JapanMA()).getText(new URL(url));		
System.out.println(txt);

WetherDataExtractorは各Filterと共有したTextBlockリストの中かisContent()がtrueであるTextBlockをマージして、getText()メソッド経由で呼び出し元に返します。

実際に使ってみると、10分、1日ごとの過去気象データのページであれば目的のデータだけがCSV形式で出力されるはずです。

1,6.5,45,--
2,10.0,41,--
3,5.6,33,--
 * snip *
29,7.1,36,--
30,8.1,36,--

今回作ったフィルターはTextBlockの持つ確率データを全く利用していないので(手抜き・・)、サンプル・コードとして適当とは言い辛いのですが、工夫すればもっと簡単に抜き出すことが出来るかもしれませんね。

それでは。

 
統計学が最強の学問である
西内 啓
ダイヤモンド社
売り上げランキング: 31


Posted by netbuffalo at 18:01│Comments(0)TrackBack(0)プログラミング | Java


この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/netbuffalo/4391414

コメントする

名前
URL
 
  絵文字