Skip to main content

Python: リストの初期化・出力・代入・要素数

· 2 min read
Yu Sasaki
Enterprise Security Manager / Advisor

ソースコード

#!/usr/bin/python
# coding: UTF-8
# リストの初期化
numbers = [1, 4, 7, 12, 23]
strings = ['Jon', 'Mery', 'Sun', 'Ren']
empty = [] # 空リスト
inilist = [0] * 4 # 0で初期化された要素が4つのリスト
# 型は?
print type(numbers)
print type(strings)
print type(empty)
print
# リストの全ての要素を表示
print numbers
print strings
print empty
print inilist
print
# for文で全要素を表示
for i in range(len(numbers)): # lenでリストの要素数を求める, rangeにループ回数(回数分の要素のリストが戻り値), inはリストをとる
print '%d, ' % (numbers[i]), # print文の末尾に「,」を付けると改行しない
print '\n'
# リストの要素の1つを表示
print numbers[0] # 要素は0から始まる
print numbers[3]
print strings[-1] # 最末尾の要素
print strings[-3]
print
# リスト要素に代入
numbers[2] = 1000
strings[1] = 'Micky'
# 代入の確認
print numbers[2]
print strings[1]

実行結果

<type 'list'>
<type 'list'>
<type 'list'>
[1, 4, 7, 12, 23]
['Jon', 'Mery', 'Sun', 'Ren']
[]
[0, 0, 0, 0]
1, 4, 7, 12, 23,
1
12
Ren
Mery
1000
Micky

リファレンス

チュートリアル

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

· 3 min read
Yu Sasaki
Enterprise Security Manager / Advisor

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

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演算

· 4 min read
Yu Sasaki
Enterprise Security Manager / Advisor

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

OR演算処理の概要

OR演算結果

上の図から、ある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;
}
}
単語 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!検索 サイトエクスプローラー を利用するブックマークレット

· One min read
Yu Sasaki
Enterprise Security Manager / Advisor

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

別ウィンドウで開く

下のリンクをお気に入りにドラック&ドロップして下さい。 サイトエクスプローラー(別) IEの場合、その際のホップアップは許可してください。ホップアップ確認がわずらわしい場合は下の「同じウィンドウで開く」バージョンを利用してください。

同じウィンドウで開く

[サイトエクスプローラー(同)](javascript:(function(){ document.location.href='http://siteexplorer.search.yahoo.co.jp/advsearch?p='+escape\(document.location.href\) })();) コード

javascript:(function(){
document.location.href=
'http://siteexplorer.search.yahoo.co.jp/advsearch?p='+escape(document.location.href)
})();

使い方

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

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

· 7 min read
Yu Sasaki
Enterprise Security Manager / Advisor

AND演算処理の概要

AND演算結果 上の図から、ある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: https://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/proper/commons-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翻訳を利用

· 3 min read
Yu Sasaki
Enterprise Security Manager / Advisor

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翻訳ロボの実行結果例](./naiTrans-e1273383658134.png) コンストラクタでの翻訳言語設定が逆でも、文字列が英語or日本語に統一されていれば、Excite翻訳側がそれに合わせて適当な言語で翻訳するみたいです。 最後に、

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

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

java.lang.OutOfMemoryErrorが発生する原因とその解決法の一例

· One min read
Yu Sasaki
Enterprise Security Manager / Advisor

JVMがGCを行えるように、開放するインスタンスへの参照を切っていたのだけれど、なぜか例外が投げられ続けていました。色々調べてみたら、java.io.ObjectOutputStream#writeObject(Object obj)の部分で、書き出されたobjの状態が保持され続けるので、いつまでたってもGCが始まらなかったのが原因でした。 解決法は、java.io.ObjectOutputStream#reset()メソッドでストリームが保持している状態を無効にすることで、不要インスタンスをGCの対象に入れることでした。

検索エンジンを実装 (3)文書内の検索語を特定

· 6 min read
Yu Sasaki
Enterprise Security Manager / Advisor

今回実装したことは、

  • IndexRecordクラスにフィールド更新用のメソッドやハッシュフィールドを追加(今後改善の必要大)。
  • 検索語を含んでいるファイルをピックアップする(色々と無駄な部分あり)。

辺りです。

後述に現在の問題点とその解決案を考えてみましたが、先ずはソースコードと実行結果(デバッグプリント)を示します。

追記:こちらに→ 検索エンジンを実装 (4)AND演算 完成版を書きましたので、そちらをご覧ください。↓以下、黒歴史(>_<)↓

IndexRecord.java

import java.util.ArrayList;
import java.util.TreeMap;
public class IndexRecord {
// 総出現回数
Integer count;
// 出現したファイルID
ArrayList<integer> file_ids;
// ファイル内の出現位置(ファイルの先頭からのオフセット)
ArrayList<integer> word_poses;
// ファイルIDごとに出現数をカウント<ファイルid, 出現数>
TreeMap<integer, Integer> idcntMap;
private IndexRecord() {}
public IndexRecord(int id, int pos) {
count = 1;
file_ids = new ArrayList<integer>();
file_ids.add(id);
word_poses = new ArrayList<integer>();
word_poses.add(pos);
idcntMap = new TreeMap<integer, Integer>();
idcntMap.put(id, 1);
}
public void renewal(int id, int pos) {
count++;
file_ids.add(id);
word_poses.add(pos);
if(idcntMap.containsKey(id)){
Integer idcnt = idcntMap.get(id);
idcnt++;
idcntMap.put(id, idcnt);
} else {
idcntMap.put(id, 1);
}
}
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(count);
for(int i = 0; i < file_ids.size(); i++){
sb.append(" (" + file_ids.get(i) + ", " + word_poses.get(i) + ")");
}
sb.append(" "+ idcntMap);
return sb.toString();
}
}

Make2Gram.java

import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.TreeMap;
public class Make2Gram{
public static final boolean DEBUG = true; // デバッグ用フラグ
public static void main(String[] args) throws IOException{
if(args.length == 0){
System.out.println("引数にディレクトリ名を指定してください");
System.exit(1);
}
int N = 2; // bigram
// http://sattontanabe.blog86.fc2.com/blog-entry-55.html
// Java 再帰的にファイルを検索 / Chat&Messenger
// のクラスFileSearchを使用しています
FileSearch search = new FileSearch();
File[] files = search.listFiles(args[0], null); // 全てのファイルを取得
// ファイルIDとパスの対応表
TreeMap<integer, File> fileMap = new TreeMap<integer, File>();
ArrayList<string> docs = new ArrayList<string>();
for(int i=0; i < files.length; i++){
fileMap.put(i, files[i].getAbsoluteFile());
BufferedReader br = new BufferedReader(new FileReader(files[i]));
StringBuilder sb = new StringBuilder();
String line;
while((line = br.readLine()) != null)
sb.append(line);
br.close(); // ファイル内容(RAW)を格納
String text = sb.toString(); // ファイルの内容 改行抜き
docs.add(text);
}
//テキストの部分文字列とそのIndexRecordクラスを関連付けるMap
//TreeMapなのでMapのキーにした部分文字列でソートされる
TreeMap<string, IndexRecord> gramMap = new TreeMap<string, IndexRecord>();
for(int i = 0; i < fileMap.size(); i++){
// ファイルごとの処理
String text = docs.get(i);
for(int j = 0; j < text.length() - N; j++){
//テキストからN文字取り出す
String gram = text.substring(j, j + N);
if(gramMap.containsKey(gram)){
//gramMapに登録されてる文字列ならカウント等を増やす
IndexRecord ir = gramMap.get(gram);
ir.renewal(i, j);
gramMap.put(gram, ir);
}else{
//gramMapに登録されていない文字列なら登録
gramMap.put(gram, new IndexRecord(i, j));
}
}
}
//for(String part : gramMap.keySet())
//System.out.printf("%s : %sn", part, gramMap.get(part));
String input = "N文字"; // 検索語(e.g.)
if(DEBUG) System.out.println("input #=> "+ input);
String[] swords = new String[(input.length()+1)/2];
boolean odd = false; // 文字列長の偶奇判定
if (input.length() < 2){
System.out.println("2文字未満の処理は未実装");
System.exit(1);
}
// 検索文字列をN文字単位に分割
for(int i = 0, j = 0; i < input.length()-N; i += N, j++){
swords[j] = input.substring(i, i+N);
}
if ((input.length() & 1) == 1){ // 文字列長が奇数
swords[swords.length-1] = input.substring(input.length()-N, input.length());
odd = true;
}
if(DEBUG){
System.out.print("swords #=> ");
for(String part : swords) System.out.print(part +" ");
System.out.println();
} // [N文, 文字](e.g.)
TreeMap<integer, Integer> id_per_cnt = new TreeMap<integer, Integer>();
// N文字単位のIndexRecordを格納する配列
IndexRecord[] ng_records = new IndexRecord[swords.length];
// 2文字ごとにgramMapと照合
for(int i = 0; i < swords.length; i++){
if(!gramMap.containsKey(swords[i])) {
System.out.println(" 検索語:【"+ input +"】はありません。");
System.exit(1);
}
IndexRecord ir = gramMap.get(swords[i]);
ng_records[i] = ir;
TreeMap<integer, Integer> _idcntMap = ir.idcntMap;
for(Integer id : _idcntMap.keySet()){
if(id_per_cnt.containsKey(id)){
int cnt = id_per_cnt.get(id);
cnt += _idcntMap.get(id);
id_per_cnt.put(id, cnt);
} else {
id_per_cnt.put(id, _idcntMap.get(id));
}
}
if(DEBUG) System.out.println(" "+ swords[i] +":"+ ng_records[i]);
}
if(DEBUG) System.out.println("id_per_cnt #=> "+ id_per_cnt);
for(Integer id : id_per_cnt.keySet()){
// ↓以下の部分の評価は中途段階。出現位置を考慮に入れた判定に変更予定。
// それに伴い、IndexRecordのデータ構造は要変更。
if(id_per_cnt.get(id) / swords.length > 0){
if(DEBUG) System.out.println(" 検索語はファイルid["+ id +"]中に存在する可能性あり。");
String target_doc = docs.get(id);
int pos = target_doc.indexOf(input);
String mch = target_doc.substring(pos, pos + input.length());
System.out.println(" 照合文字列 : "+ mch);
}
}
}
}

実行結果(コマンドライン引数部分は省略)

input #=> N文字
swords #=> N文 文字
N文:2 (0, 1) (0, 42) {0=2}
文字:3 (0, 2) (0, 43) (1, 61) {0=2, 1=1}
id_per_cnt #=> {0=4, 1=1}
検索語はファイルid[0]中に存在する可能性あり。
照合文字列 : N文字

現在の問題点

IndexRecordクラス:ArrayList型ではフィールド(ファイルidと出現位置)の関係性を取りづらい。

解決案

IndexRecordクラスのフィールドにファイルidを主キーとして、その部分単語の全ての出現位置を求められるハッシュデータが必要かと考えました。

今回もうすうすは感じていましたが、データ構造を設計し間違えるとプログラム構造が煩雑になりやすいです。初めから仕様を明確にしておけばデータモデリングでミスることもなかったかな。

しばらくは、今後の実装機能の洗い出しとそれに対応できるクラス構造を考えてみようかな。また、N-gramに分割する処理部分は別クラスのインスタンスメソッドとしてまとめたほうが良いですね。並行してデザインパターンも復習しておこう。

検索エンジンを実装 (2)出現位置とその文書ID

· 4 min read
Yu Sasaki
Enterprise Security Manager / Advisor

id:d-kamiさんから改良版Make2Gram付きトラックバックを頂きました(連絡方法がわからんのでトラックバックで - マイペースなプログラミング日記)(はてなダイヤリーから移転前)。d-kamiさん、ありがとうございます。

上記のページにあるコードから、TreeMapやsubstringを用いたbigramの切り出し・カウント方法などを学ばせて頂きました。

さて、今回の実装その2は以下の機能を加えました。

  1. コマンドライン引数にディレクトリ名を指定して、そのディレクトリ以下のファイル全てを処理の対象とする。
  2. N-gram情報には文書IDと部分文字列の出現位置を格納するようにデータ構造の拡張。

検索エンジンを実装 (1)転置インデックス作成

· 2 min read
Yu Sasaki
Enterprise Security Manager / Advisor

今回はN-gramでテキストを分解します。N-gram法とは対象の文字列を一定のN文字単位で分解し、それの出現頻度を求める方法です。これによって、検索エンジンに使われる転置インデックスを作成したいと思います。転置インデックスの作成方法にはN-gramの他に形態素解析があります。両者の性能の長短は全文検索 - Wikipediaに詳しく載っています。

Javaソースコード(Make2gram.java)

さて、まずは文字列を2単語に切り分けるプログラムを作成しました。データ構造は単純にArrayListで、出現頻度も求めていません。

import java.io.*;
import java.util.*;
/**
* N-gram法
*/
public class Make2gram {
public static void main(String[] args) {
final short nsepa = 1; // 2gram
String line;
ArrayList<stringBuffer> filelist = new ArrayList<stringBuffer>();
ArrayList<stringBuffer> bigram = new ArrayList<stringBuffer>();
if (args.length < 1) { // コマンドライン引数の数
System.out.println("How to use: java Make2gram [filename]");
System.exit(1);
}
try {
BufferedReader br = new BufferedReader(new FileReader(args[0]));
while ((line = br.readLine()) != null) {
//System.out.println(line);
filelist.add(new StringBuffer(line)); // 入力ファイルは一行=一要素として格納
}
br.close();
}
catch (Exception e) {
System.err.println("[main] : " + e.toString());
}
for (Iterator it = filelist.iterator(); it.hasNext(); ) {
StringBuffer str = (StringBuffer) it.next();
int lm = str.length() - nsepa;
for (int i = 0; i < lm; i++) {
StringBuffer bi = new StringBuffer(4); // 4Byte(2文字)分の容量(Javaの内部文字コードはUnicode)
bi.append(str.charAt(i)); // append():文字列の末尾に追加
bi.append(str.charAt(i+1));
bigram.add(bi);
//System.out.print(str.charAt(i));
//System.out.println(str.charAt(i+1));
}
}
// 2-gramを表示
for (Iterator it = bigram.iterator(); it.hasNext(); ) {
System.out.println(it.next());
}
}
}

入力ファイル(text.txt)

検索された文書は「更新順」「ファイル名順」「文書のタイトル順」などにソートされる。
一般的な検索エンジンでは独自のランク付けルールも適用し「重要度」などと呼んでいるものもある。

実行結果

検索
索さ
され
れた
た文
文書
書は
は「
「更
更新
新順
順」
」「
…<省略>…