Home > Archives > 2008-05

2008-05

Java: 文字列の先頭・末尾の文字を削除するstrip()メソッド

テキストマイニングを行う際、文書を単語集合に区切ったのはいいけれど、単語の先頭・末尾に以下のような文字が入っている場合は辞書に格納する際に削除したいですね。

Hello!
page."
"Hi,

単語の前後に複数の記号((ピリオドやクォーテーション、カンマやコロンなど))が入り混じった文字列から、それらを刈り取る方法を採り上げます。0からゴリゴリ文字の条件判定文を書いたり、正規表現でマッチしたときに刈り取りしたりするのも良いですが、オープンソースで今回のケースにマッチしたクラスメソッドがありますので、その使い方と仕組みを簡単に紹介します。

StringUtils#strip(String str, String stripChars)

ソース・バイナリ共にApache CommonsLang Downloadsのページからダウンロードできます。

StringUtils#strip(String str, String stripChars)メソッド引数は、
+第一引数:刈り取り対象文字列(テキスト)
+第二引数:刈り取る文字
例えば、

str = "Hi,]";
stripChars = "],";

result = strip(str, stripChars);

の時のresultの中の文字列はHiとなります。

stripメソッドは内部でstripStartとstripEndメソッドを実行しています。前者は文字列の先頭部分の刈り取りを、後者は末尾部分の刈り取りを行っています。
以下に、stripEndのソースを引用します。

  public static String stripEnd(String str, String stripChars) {
    int end;
    if (str == null || (end = str.length()) == 0) {
      return str;
    }
    if (stripChars == null) {
      while ((end != 0) && Character.isWhitespace(str.charAt(end - 1))) {
        end--;
      }
    } else if (stripChars.length() == 0) {
      return str;
    } else {
      while ((end != 0) && (stripChars.indexOf(str.charAt(end - 1)) != -1)) {
        end--;
      }
    }
    return str.substring(0, end);
  }

ここで重要な処理部分はソース最後の、

      while ((end != 0) && (stripChars.indexOf(str.charAt(end - 1)) != -1)) {
        end--;
      }
    }
    return str.substring(0, end);

の部分です。
刈り取り対象文字列の末尾から順に文字を調べていきますが、その文字を指すパラメータにendを、その初期値はテキストの文字列長でループごとにデクリメントされていきます。そのendが指す文字がstripCharsに含まれているか否かを判定しているところが、

stripChars.indexOf(str.charAt(end - 1)) != -1

ですね。最終的にsubstringメソッドで部分文字列を取得することで刈り取っています。

これ書いた人上手いなー。indexOfやcharAtの使い道の幅が広がった感じがします。

検索エンジンを実装 (5)OR演算

前回がAND演算でしたので今回はOR演算ついて紹介します。今記事で紹介している演算アルゴリズムよりも高効率なものは存在するようですが、今回は割愛します。

OR演算処理の概要

OR演算結果 by yukun

上の図から、ある2つの語の転置インデックスリストをA, Bとします。ここで、リスト要素をそれぞれa, b(整数)とし演算結果を格納するリストをCとするとき、OR演算は主に以下の処理内容を繰り返します。

  1. if a < b then 要素aをCの末尾に追加し、aにリストAの次の要素を代入
  2. if a = b then 要素aをCの末尾に追加し、A, Bが指す次の要素をa, bに代入
  3. if a > b then 要素bをCの末尾に追加し、bにリストBの次の要素を代入

ソースコード

今回はOR演算処理を行う部分(メソッド)のみを示します。後で示す実行結果は、前回ブログラムをベースにintersect(postsSet)の箇所を今回のものに置き換えたものです。

import java.util.ArrayList;
import java.util.Collections;
/**
 * 検索エンジンのOR演算
 */
public class BooleanRetrieval {
  /**
   * OR演算処理
   * @param postsSet 全ての検索語の転置インデックスリスト
   * @return 演算後の転置インデックスリスト
   */
  public static ArrayList<integer> union(ArrayList<arrayList<integer>> postsSet) {
    ArrayList<integer> result; // 最終演算結果
    if (postsSet == null) return null;
    int len = postsSet.size();
    if (len == 0) return null;
    else if (len == 1) return postsSet.get(0);
    result = postsSet.get(0);
    for (int i = 1; i < len; i++) {
      result = union(result, postsSet.get(i));
    }
    return result;
  }

  public static ArrayList<integer> union(ArrayList<integer> p1, ArrayList<integer> p2) {
    ArrayList<integer> answer = new ArrayList<integer>(); // 2語の演算結果
    int len1 = p1.size();
    int len2 = p2.size();
    int i=0, j=0;
    while (i<len1 && j<len2) {
      int diff = p1.get(i) - p2.get(j);
      if (diff == 0) {
        answer.add(p1.get(i));
        i++; j++;
      } else if (diff < 0) {
        answer.add(p1.get(i));
        i++;
      } else {
        answer.add(p2.get(j));
        j++;
      }
    }
    while (i<len1) { answer.add(p1.get(i++)); }
    while (j<len2) { answer.add(p2.get(j++)); }
    return answer;
  }
}

OR演算の実行結果

単語          freq, docID
15           : 1, [2]
5            : 1, [2]
After        : 1, [1]
As           : 1, [1]
I            : 3, [0, 1, 2]
<中略>
should       : 2, [0, 1]
so           : 2, [1, 2]
some         : 1, [2]
standard     : 1, [0]
such         : 2, [1, 2]
than         : 1, [2]
that         : 1, [2]
the          : 3, [0, 1, 2]
think        : 3, [0, 1, 2]
this         : 1, [2]
time         : 2, [0, 2]
to           : 2, [0, 1]
touch        : 1, [1]
tremendously : 1, [1]
uncertainty  : 1, [1]
use          : 1, [2]
vigorous     : 1, [1]
wanna        : 1, [1]
well         : 1, [1]
what         : 1, [0]
when         : 1, [1]
where        : 1, [0]
why          : 1, [1]
with         : 1, [1]
検索語: the when
結果 :文書ID [0, 1, 2]に存在します。
検索語: should so
結果 :文書ID [0, 1, 2]に存在します。
検索語: this touch
結果 :文書ID [1, 2]に存在します。
検索語: use than
結果 :文書ID [2]に存在します。
検索語: where why
結果 :文書ID [0, 1]に存在します。
検索語: quit

おー、なんだかもりあがってきましたねー。

話は変わりますが、最近よく実感していることの一つに、ソフトウェアの仕様が変更されると場合によっては設計を根本から変える必要があること。

そして、手がけているソフトウェアが今後アップグレードを繰り返していくと予測できるなら、現時点で取り組んでいる設計に費やす時間はほどほどに、ということです。OO分析設計を用いて保守性・拡張性を高めるのもいいけれど、その設計上での拡張が今後の仕様変更に耐えうるとは限りません。

勿論、最初の設計が肝心であることには変わりないですけれど。それでも留意。

Yahoo!検索 サイトエクスプローラー を利用するブックマークレット

任意のWebページ上でワンクリックでサイトエクスプローラー - Yahoo!検索のインデックス検索が出来るブックマークレットを作ってみました。

別ウィンドウで開く

下のリンクをお気に入りにドラック&ドロップして下さい。

サイトエクスプローラー(別)

IEの場合、その際のホップアップは許可してください。ホップアップ確認がわずらわしい場合は下の「同じウィンドウで開く」バージョンを利用してください。

同じウィンドウで開く

サイトエクスプローラー(同)

使い方

任意のサイトで上述で作成したお気に入りをクリックすると、そのサイトのインデックス検索が行われます。

検索エンジンを実装 (4)AND演算

AND演算処理の概要

AND演算結果 by yukun

上の図から、ある2つの語の転置インデックスリストをA, Bとします。ここで、要素をそれぞれa, b(整数)とし演算結果を格納するリストをCとするとき、AND演算は主に以下の処理内容を繰り返します。

  1. if a < b then aの次の要素をaに代入
  2. if a = b then 要素aをCの末尾に追加しA, Bが指す要素を一つ進める

プログラムの主な処理内容

  1. 検索対象テキストを単語に分割。
  2. 単語を転置インデックスに登録。ここで、1単語あたりに格納する情報は、その単語の出現頻度とその文書ID。転置インデックスのデータ構造はTreeMapを使用しkeyに単語、valueはIndexRecordでputします。
  3. ユーザからの標準入力をパースしAND演算(Intersectメソッドで実現しています)。

以下に、ソースコードと実行結果を示します。

IndexRecord.java

1つのトークン(単語)に対するインデックス情報(docIDリストや出現頻度情報)

import java.util.ArrayList;

/*
 * 1つのトークン(単語)に対するインデックス情報
 */
public class IndexRecord {
  // 出現文書IDリスト(通常ソートの必要あり)
  private ArrayList<integer> posts;
  // 出現頻度(今回は同一doc内の頻度はカウントしない)
  private int freq;

  public IndexRecord(int id) {
    posts = new ArrayList<integer>();
    posts.add(id);
    freq = 1;
  }

  /** docIDをリストに追加(今回は同一docIDのtermはカウントしない) */
  public void addDocID(int id) {
    if (!existDocID(id)) {
      posts.add(id); // docIDの追加
      freq++; // 出現頻度のカウントアップ
    }
  }

  /** docIDが既にリスト中に存在するか否か */
  public boolean existDocID(int id) {
    return (posts.indexOf(id) != -1) ? true : false;
  }

  public String toString() {
    String str = freq + ", "+ posts;
    return str;
  }

  public ArrayList<integer> getPosts() {
    return posts;
  }
}

BooleanTest.java (AND演算のテストプログラム)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.StringTokenizer;
import java.util.TreeMap;

/**
 * 検索エンジンのAND演算処理
 * Web page: http://www.yukun.info/
 * license GPL
 */
public class BooleanTest {
  // 検索対象テキスト
  static String doc0 = "It is meaningless only to think my long further aims idly. "+
  "It is important to set my aims but at the same time I should confirm my present condition. "+
  "Unless I set the standard where I am in any level, I'll be puzzled about what I should do from now on. It's in my case.";
  static String doc1 = "Today, I enjoyed playing with friends daytime. "+
  "After enjoying, I got back to my daily life with an vigorous power. "+
  "I should think so, but why did I feel touch of uncertainty and regret? "+
  "I wanna enjoy myself and another tremendously during the day when I've played. "+
  "Well, As well as I commit play to quality, I'll choose such kinds of play.";
  static String doc2 = "I'll manage the limited time in a day. "+
  "I think that I divide the time into some intervals such as 5 minutes, "+
  "15 minutes and more than one hour and so on. I'll make use of this character of the interval.";

  public static void main(String[] args) {
    ArrayList<string> docIDlist = new ArrayList<string>();
    // 文書を格納
    docIDlist.add(doc0);
    docIDlist.add(doc1);
    docIDlist.add(doc2);
    StringTokenizer st[] = new StringTokenizer[3];
    String stripChars = ".,:;?!"'[]{}()"; // 除外文字

    // 文字列を空白で区切るよう設定
    for (int i = 0; i < st.length; i++) {
      st[i] = new StringTokenizer(docIDlist.get(i), " ");
    }
    // 転置インデックス用のMap
    TreeMap<string, IndexRecord> termMap = new TreeMap<string , IndexRecord>();
    // 分割されたトークンを取得
    for (int i = 0; i < st.length; i++) {
      // ここでのパラメータiはdocIDを指すことと同じ
      while (st[i].hasMoreTokens()) {
        // 文字列トークンの先頭・末尾の文字をフィルタリング
        // org.apache.commons.lang.StringUtilsクラスを使用
        // http://commons.apache.org/lang/
        String term = StringUtils.strip(st[i].nextToken(), stripChars);
        //System.out.println("値 : " + term);
        if(termMap.containsKey(term)) {
          // 登録されているtermならdocIDの追加とカウントアップ
          IndexRecord ir = termMap.get(term);
          ir.addDocID(i);
          termMap.put(term, ir);
        } else {
          // termMapに登録されていないtermならdocIDと合わせて登録
          termMap.put(term, new IndexRecord(i));
        }

      }
    } // for loop ends

    // termMapのデバッグプリント
    System.out.println("単語          freq, docID");
    for (String part : termMap.keySet()) {
      System.out.printf("%-12s : %sn", part, termMap.get(part));
    }

    BufferedReader br =  new BufferedReader(new InputStreamReader(System.in));
    String words = "";
    while (true) {
      ArrayList<arrayList<integer>> postsSet = new ArrayList<arrayList<integer>>();
      System.out.print("検索語: ");
      try {
        // ユーザからの標準入力を受付
        words = br.readLine();
        if (words.equals("quit")) break;
        // 入力文字列をパース
        StringTokenizer parser = new StringTokenizer(words, " ");
        while (parser.hasMoreTokens()) {
          String term = StringUtils.strip(parser.nextToken(), stripChars);
          // termMapに登録されている単語か否か
          if (termMap.containsKey(term)) {
            postsSet.add(termMap.get(term).getPosts());
          } else {
            postsSet = null;
            break;
          }
        }

        // AND演算処理
        ArrayList<integer> result = intersect(postsSet);
        System.out.print("結果 :");
        if (result == null || result.size() == 0)
          System.out.println("文書中に存在しません。");
        else
          System.out.println("文書ID "+ result +"に存在します。");
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  } // main() ends

  // AND演算処理メソッド
  public static ArrayList<integer> intersect(ArrayList<arrayList<integer>> postsSet) {
    if (postsSet == null) return null;
    int len = postsSet.size();
    if (len == 0) return null;
    else if (len == 1) return postsSet.get(0);
    // postsSetを昇順にソート(演算回数の削減)
    Collections.sort(postsSet, new FreqComparator());
    ArrayList< Integer > result = postsSet.get(0);
    for (int i = 1; i < len; i++) {
      result = intersect(result, postsSet.get(i));
    }
    return result;
  }

  public static ArrayList<integer> intersect(ArrayList<integer> p1, ArrayList<integer> p2) {
    ArrayList<integer> answer = new ArrayList<integer>();
    int len1 = p1.size();
    int len2 = p2.size();
    for (int i=0, j=0; i< len1 && j < len2; ) {
      if (p1.get(i) == p2.get(j)) {
        answer.add(p1.get(i));
        i++; j++;
      } else if (p1.get(i) < p2.get(j)) {
        i++;
      } else {
        j++;
      }
    }
    return answer;
  }
}

FreqComparator.java (ArrayList要素のソート用)

import java.util.ArrayList;
import java.util.Comparator;

public class FreqComparator implements Comparator<object>{
  public int compare(Object o1, Object o2){
    return ((ArrayList<integer>) o1).size() - ((ArrayList<integer>) o2).size();
  }
}

実行結果

単語          freq, docID
15           : 1, [2]
5            : 1, [2]
After        : 1, [1]
As           : 1, [1]
I            : 3, [0, 1, 2]

<中略>

set          : 1, [0]
should       : 2, [0, 1]
so           : 2, [1, 2]
some         : 1, [2]
standard     : 1, [0]
such         : 2, [1, 2]
than         : 1, [2]
that         : 1, [2]
the          : 3, [0, 1, 2]
think        : 3, [0, 1, 2]
this         : 1, [2]
time         : 2, [0, 2]
to           : 2, [0, 1]
touch        : 1, [1]

<中略>

検索語: think that
結果 :文書ID [2]に存在します。
検索語: wanna play to
結果 :文書ID [1]に存在します。
検索語: the java
結果 :文書中に存在しません。
検索語: should so
結果 :文書ID [1]に存在します。
検索語: time to
結果 :文書ID [0]に存在します。
検索語: well only
結果 :文書中に存在しません。
検索語: the time
結果 :文書ID [0, 2]に存在します。
検索語: quit

おー、なんだか楽しくなってきましたね。

文字列の区切り方

今回は検索対象テキストを英文に絞ったため、テキスト中の空白文字で区切ることでトークンを抽出できました。対して、日本語テキストの場合は区切り記号等は無い為、n-gramか形態素辞書などを用いてトークンに区切ることで実現できます。日本語文の区切り方は色々ありますが、中でも簡単な方法は、文字種(英文字、記号、ひらがな、カタカナ、漢字)の違いを区切りの境界と捉える方法です。

余談ですが、ブラウザやエディタ等で文字の上でダブルクリックするとカーソル下の文字列が選択状態になりますが、その範囲を決定する際に上述の方法が応用されているようです。ソフトによってはトリプルクリックするとカーソル下の行全体が選択状態になります(使うと編集が楽です)。

JavaプログラムからExcite翻訳を利用

POSTメソッドを用いてWebページのフォームにリクエストを送信し、そのレスポンスを取得するプログラム例として、エキサイト 翻訳を利用してみます。

送信クエリの1つは翻訳言語設定、2つ目は翻訳対象文字列でレスポンスのWebページから翻訳された文字列を抽出します。

ソースコード

import java.net.*;
import java.util.regex.*;
import java.io.*;

/**
 * Excite翻訳(http://www.excite.co.jp/world/)を利用するクラス
 */
public class ExciteTrans {
  private String direction; // 翻訳する言語設定 "ENJA" or "JAEN"
  /** テスト用main() */
  public static void main(String[] args) {
    ExciteTrans et = new ExciteTrans();
    System.out.println(et.getTransText("Hello!")); // 翻訳対象テキスト
  }
  /** コンストラクタ */
  public ExciteTrans() { direction ="ENJA"; }
  public ExciteTrans(String str) {
    if (str.equals("JAEN") || str.equals("ENJA"))
      direction = str;
    else
      direction ="JAEN";
  }
  /**
   * テキストを翻訳
   * @param before 翻訳前のテキスト
   * @return 翻訳後のテキスト
   */
  public String getTransText(String before) {
    String afterText = null; // 翻訳されたテキスト
    try {
      // URLクラスのインスタンスを生成
      URL exciteURL =
        new URL("http://www.excite.co.jp/world/english/");
      // 接続します
      URLConnection con = exciteURL.openConnection();
      // 出力を行うように設定
      con.setDoOutput(true);
      // 出力ストリームを取得
      PrintWriter out = new PrintWriter(con.getOutputStream());
      // クエリー文の生成・送信
      out.print("before={"+ before +"}&wb_lp={"+ direction +"}");
      out.close();
      // 入力ストリームを取得
      BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
      // 一行ずつ読み込む
      String aline;
      // 抽出用の正規表現
      String regex = "<textarea [^>].*after.*>(.*)";
      Pattern pattern = Pattern.compile(regex);
      while ((aline = in.readLine()) != null) {
        Matcher mc = pattern.matcher(aline);
        if(mc.matches()) {
          afterText = mc.group(1);
        }
      }
      in.close(); // 入力ストリームを閉じる
    } catch (IOException e) {
      e.printStackTrace();
    }
    return afterText;
  }
}

実行結果

こんにちは!

Java MSN Messenger Library (JML)で翻訳ロボを作る

Java MSN Messenger Library (JML)とは Windows Live Messenger 上の通信プロトコルMSNP8-MSNP12をサポートするライブラリです。主に、メンバーの会話に自動応答するチャットボット(Chat Bot)や人工無脳などを作る際に用いられます。

今回は、ライブラリに付属しているサンプルプログラムのEchoMessengerクラスにこの翻訳クラスをかませて、メンバーの発言を翻訳し、その文字列色をランダムで選択した色で応答するチャットボットを作成してみました。

実行結果は下図になります。

JML翻訳ロボの実行結果例 by yukun

コンストラクタでの翻訳言語設定が逆でも、文字列が英語or日本語に統一されていれば、Excite翻訳側がそれに合わせて適当な言語で翻訳するみたいです。

最後に、

礼儀正しくクローリングする際は

  • robots.txtやメタタグを守るように
  • 相手サーバに負荷をかけすぎないように
  • 相手Webサイトポリシー(利用規約)を守るように
  • 頻繁に使いたいモノなら、同機能のWebサービスを使うこと(利用規約内で)

うーん、今回のようなプログラムの利用はあまりよくないようでね。

Page 1 of 212

Home > Archives > 2008-05

バックナンバー
最近のコメント
最近のトラックバック
メタ情報

Return to page top