myfinderの技術や周辺的活動のblog

2008年12月29日月曜日

人工無脳(chatbot)を実装してみる -第六回-

今回は、今までとは打って変わって形態素解析とマルコフ連鎖を利用して文章を自動生成してみます。

形態素解析にマルコフ連鎖と並ぶと、難しそうで思わず敬遠しちゃいそうですが、データ構造を先に見てしまえばきっとすぐ分ると思います。
長い文章のマルコフ連鎖は巨大なデータになってしまうので、ここでは
「私のお墓の前で泣かないでください」
を例にとって解説します。

まずは形態素解析です。
前回までに作成した形態素解析のためのクラスで解析メソッドに掛けると、下記のような出力が得られます。

[
["私", "名詞-代名詞-一般"],
["の", "助詞-連体化"],
["お", "接頭詞-名詞接続"],
["墓", "名詞-一般"],
["の", "助詞-連体化"],
["前", "名詞-副詞可能"],
["で", "助詞-格助詞-一般"],
["泣か", "動詞-自立"],
["ない", "助動詞"],
["で", "助詞-接続助詞"],
["ください", "動詞-非自立"]
]

こんな感じですね。
今までは、
1:名詞を抜き出して、入力を回答として記憶するパターン
2:名詞部分を置換用の文字列に置き換え、テンプレートとして記憶するパターン
という2種類の使い方をしていました。
今回は、「単語と単語のつながり」に着目して、文書を自動生成するための処理を組み立てます。

今回の文書のつながり方は、形態素解析によって次のようになっているということが分りました。
「私 -> の -> お -> 墓 -> の -> 前 -> で -> 泣か -> ない -> で -> ください」
この文のつながり情報を持ったデータ構造は下記のように表せます。

{
"泣か"=>
{"ない"=>["で"]},
"で"=>
{"泣か"=>["ない"], "ください"=>["%END%"]},
"前"=>
{"で"=>["泣か"]},
"ない"=>
{"で"=>["ください"]},
"墓"=>
{"の"=>["前"]},
"お"=>
{"墓"=>["の"]},
"私"=>
{"の"=>["お"]},
"の"=>
{"前"=>["で"],
"お"=>["墓"]}
}

これは、2単語の接頭+接尾で文章のつながりを表したものです。
このデータ構造によって、接頭2単語に続く接尾をランダムに選択し、接頭の2単語目と接尾を接頭2単語として再びランダムに選択し・・・ということを、ENDマークが出るまで繰り返すだけで文章を生成できるようになりました。
このままでは元の文章が生成されるだけなので、さらに別の文章もこのデータ構造に取り込んでいくと、分岐ができるようになり、(意味が通るかは別として)新たな文章が生成できるようになります。

今回は2単語+接尾でつないでいますが、1単語+接尾だけでつなぐと日本語として意味不明な文章が生成されがちなので、最低でも2単語以上でつないだ方がより自然な文章が作れるようです。
(「あたし彼女」のような文を作るときは1単語+接尾の方が雰囲気ある文章が作れそうですが)

2008年12月27日土曜日

人工無脳(chatbot)を実装してみる -第五回-

第五回は、第四回でやったことの逆をやります。
つまり、キーワード以外の部分をテンプレートとして学習させ、ユーザの発言に応じてテンプレートから回答するというものです。

これを実現するには、ユーザの入力を例によって形態素解析して、名詞部分を特定することが必要です。
この処理はすでに前回実装している、下記のようなものでいいかなと。

def analyze(text)
# MeCabのparseメソッドで解析
parsed_text = parse(text)
parsed_data = []
parsed_text.split(/\n/).map do |line|
parsed_data.push(line.split(/\t/))
end
return parsed_data
end

このメソッドで、例えば「俺ら東京さ行くだ」を解析すると下記のような結果が得られます。

[
["俺", "名詞-代名詞-一般"]
["ら", "名詞-接尾-一般"]
["東京", "名詞-固有名詞-地域-一般"]
["さ", "名詞-接尾-特殊"]
["行く", "動詞-自立"]
["だ", "助動詞"]
]

いい感じのデータ構造です、これならeachで簡単に名詞を置き換えることができます。

parts.each do |word, part|
if @morph.keyword?(part)
word = '%word%'
end
template += word
end

keyword?は前回実装した、下記のようなメソッドです。

def keyword?(part)
return /名詞-(一般|固有名詞|サ変接続|形容動詞語幹)/ =~ part
end

「名詞」でかつ他の品詞分類があるもので「一般」「固有名詞」「サ変接続」「形容動詞語幹」がキーワードとして判定されます。
なのでこの場合「俺ら」と「東京」が「%word%」に置換された文字列が得られるのです。

ここで得られた文字列をテンプレートとしてファイルやDBに保存して、ユーザの入力と突き合わせ、名詞の数が同じものだった場合に置き換えて出力するなどすれば、回答に利用できます。
ただ、名詞を抜いただけのテンプレートなので、場合によってはおかしな返答を返してしまうこともあります。

例えば
「私のお墓の前で泣かないでください」
をこの処理に掛けると
「%word%のお%word%の前で泣かないでください」
というテンプレートが出来るのですが、
ユーザが
「貴方の名前は?」
という入力を与え、このテンプレートとマッチしてしまうと
「貴方のお名前の前で泣かないでください」
という、意味不明な回答が出てきたりしてしまいます。

なので、この手のテンプレートはメンテナンスが必要だったりします。
が、これで、今までのユーザの入力をオウム返しするだけのものから、名詞だけとはいえ自分でフレーズを作って返す機能ができたわけです。

おわり。

2008年12月23日火曜日

人工無脳(chatbot)を実装してみる -第四回-

第四回は、ユーザの入力を形態素解析してキーワードを抽出し、キーワードと入力をパターン辞書に学習させる機能を追加します。
例によって「恋するプログラム」の例に倣ってやっています。

今回形態素を解析するのに、Rubyバインディングの存在するMeCabを利用しました。
下記はMeCabをラップするRubyのクラスです。

require 'MeCab'

class Morph

# 初期化
def initialize
@mecab = MeCab::Tagger.new("-Osimple")
end

def parse(text)
@mecab.parse(text)
end

def parse_nbest(n, text)
@mecab.parse_nbest(n, text)
end

def analyze(text)
parsed_text = parse(text)

# EOS部分がnilとなって返るのを防ぐ
parsed_data = []
parsed_text.split(/\n/).map do |line|
if line != "EOS"
parsed_data.push(line.split(/\t/))
end
end
return parsed_data
end

private :parse, :parse_nbest

def keyword?(part)
return /名詞-(一般|固有名詞|サ変接続|形容動詞語幹)/ =~ part
end
end

これで、Morphのインスタンスを取得すれば、形態素解析やキーワード判定が行なえるようになります。
入力を形態素解析するのは、無脳クラスのdialogueメソッドが適当と考えられるので、そこに下記のように追記します。

parts = @morph.analyze(input)

analyzeメソッドは、MeCabのparseを呼び出すラッパメソッドです。
戻り値には、表層系と品詞分類の配列の配列(二次元配列)が返ります。

ここまでで形態素の解析は出来ました、引き続きこれをパターン辞書に学習させる実装を行ないます。
学習させるということで、今回形態素解析した結果を引き渡す先はDictionaryクラスのstudyメソッドが適当と思われますので、そこに解析結果を引き渡します。

@dictionary.study(input, parts)

これまでのstudyメソッドは、単純にランダム辞書に入力を書き込んでいただけでしたが、パターン辞書への登録も受け付けられるように改修します。

def study(input, parts)
study_random(input)
study_pattern(input, parts)
end

study_randomは以前のままで、今回新規にstudy_patternというメソッドを追加しました。
このメソッドに、ユーザの入力と形態素解析結果を引き渡して、パターン辞書に学習させます。

def study_pattern(input, parts)
parts.each do |word, part|
next unless @morph.keyword?(part)
duped = @pattern.find{|pattern_item| pattern_item.pattern == word}
if duped
duped.add_phrase(input)
else
@pattern.push(PatternItem.new(word, input))
end
end
end

形態素解析の結果について処理を行なっています。
keyword?メソッドで、品詞情報が「名詞-(一般|固有名詞|サ変接続|形容動詞語幹)」という正規表現にマッチするかどうかを判断し、マッチすればキーワード登録の対象として次の処理に渡します。
単純に「名詞」だけを対象にすると、あまりに多くのワードを引っ掛けてしまうため、名詞として処理する対象を絞るため、このようにしています。
また、そのまま登録すると、キーワードが重複してしまう可能性があるため、重複チェックを行なっています。
重複チェックし、すでに存在するキーワードであれば、そのキーワードに入力を追記し、存在しなければ新規キーワードとして追加するのです。

最後に、学習したパターン辞書を保存する処理を追加します。

open('dicts/pattern.txt', 'w') do |file|
@pattern.each do |pattern_item|
file.puts(pattern_item.make_line)
end
end

ランダム辞書は単純にファイルの最後に追記するだけでしたが、パターン辞書はパターン辞書フォーマットに合わせた文字列に変換する必要があります。
そこで、PatternItemクラスに変換メソッドを用意しました。

def make_line
# 必要機嫌値##応答例
pattern = @modify.to_s + "##" + @pattern
# pattern\t必要機嫌値##応答例
phrases = @phrases.map { |phrase|
phrase["need"].to_s + "##" + phrase["phrase"]
}
return pattern + "\t" + phrases.join("|")
end

パターン辞書フォーマットは「必要機嫌値##パターン\t必要機嫌値##応答例(|応答例)」といった形式でした。
今回はそれに沿った形で文字列をreturnする実装としています。

ここまでで、名詞を抽出してパターン辞書に登録する処理を実装することができました。
が、MeCabも万能ではないので、キーワードとして不適切なものが登録されてしまう可能性は多いにあり得ます。

例えば「攻殻機動隊」をMeCabで形態素解析すると

攻殻機動隊
攻 名詞,固有名詞,人名,名,*,*,攻,オサム,オサム
殻 名詞,一般,*,*,*,*,殻,カラ,カラ
機動 名詞,一般,*,*,*,*,機動,キドウ,キドー
隊 名詞,接尾,一般,*,*,*,隊,タイ,タイ

となってしまい、現在の仕様のままでは、全く関係ない語である「攻」「殻」「機動」が下記のようにそれぞれ別のキーワードとして登録されてしまいます。

0##攻 0##攻殻機動隊のDVD
0##殻 0##攻殻機動隊のDVD
0##機動 0##攻殻機動隊のDVD


これらは、パターン辞書のメンテナンスや、MeCab辞書のメンテナンスで補うしかないのが現状です。
でもでも、これでさらに会話のバリエーションが充実する&使われれば使われるほどボキャブラリが増えていくようになってきました。

今回はここまで。

LeopardのMeCabで独自に出力フォーマットを設定する

CodeReposで粛々と書いているプログラムで、MeCabを使ったのだけども、そのときに出力フォーマットを独自定義にする方法を知ったので記録。

LeopardでMeCabをビルドしてインストールした場合、
解析結果の出力フォーマットは「/usr/local/lib/mecab/dic/ipadic/dicrc」に記述されている。
このファイルに、以下のように記述を追加することで、-Ohogeのように指定ができる。

node-format-KEY = STR # 形態素
unk-format-KEY = STR # 未知語形態素
eos-format-KEY = STR # EOS(解析結果フッタ)
bos-format-KEY = STR # BOS(解析結果ヘッダ)

例えば下記のように設定してみる。
とりあえずヘッダもフッタも未知語も設定しない。

node-format-myfinder = %m\t%F-[0,1,2,3]\n # 「表層文字列\tハイフン区切りの品詞情報」

これをコードで呼び出すときには、下記のようにする。

@mecab = MeCab::Tagger.new("-Omyfinder")

こうして呼び出したMeCabオブジェクトで解析をすると、出力は下記のようになる。

# コード
text = "すもももももももものうち"
puts @mecab.parse(text)
# 出力結果
すもも 名詞-一般
も 助詞-係助詞
もも 名詞-一般
も 助詞-係助詞
もも 名詞-一般
の 助詞-連体化
うち 名詞-非自立-副詞可能

# コード
text = "浜岡原発:廃炉・新設計画決定 地元に戸惑いも 菊川市長ら、中電と面会拒否 /静岡"
puts @mecab.parse(text)
# 出力結果
浜岡原発 名詞-固有名詞-一般
: 記号-一般
廃 名詞-サ変接続
炉 名詞-接尾-一般
・ 記号-一般
新設 名詞-サ変接続
計画 名詞-サ変接続
決定 名詞-サ変接続
地元 名詞-一般
に 助詞-格助詞-一般
戸惑い 名詞-一般
も 助詞-係助詞
菊川 名詞-固有名詞-人名-姓
市長 名詞-一般
ら 名詞-接尾-一般
、 記号-読点
中 名詞-固有名詞-地域-国
電 名詞-接尾-一般
と 助詞-格助詞-一般
面会 名詞-サ変接続
拒否 名詞-サ変接続
/ 記号-一般
静岡 名詞-固有名詞-地域-一般

こうやっておけば、BOSをいちいちカットする処理を書かなくてもよくなるし、「名詞かつ固有名詞」や「名詞かつサ変接続」といったワード判定が簡単に実装できる。

おわり。

2008年12月17日水曜日

intel DG45IDでNICが認識しない問題

ちょっとサーバセットアップの用件があり、intel DG45IDでPCを組んでCentOS5をインストールしたところ、NICが認識しなかった。
またかよCentOS...と思いつつ解決策を探ったところ、下記の方法でばっちり解決できた。

1.ドライバのダウンロード

http://sourceforge.net/projects/e1000 に、DG45IGで使えるe1000eのドライバがあります。
ファイルは「e1000e-0.5.8.2.tar.gz」という名前です。
こいつをダウンロードします。

2.USBメモリとかで当該PCにtarアーカイブをコピーして展開

どこでもいいので展開して、srcディレクトリに移動
cd /tmp/e1000e-0.5.8.2/src/
移動したらモジュールをコンパイル&インストール
make install
インストールできたら、下記の場所にバイナリファイルができているはずです
/lib/modules//kernel/drivers/net/e1000e/e1000e.[k]o
できていることを確認したら
modprobe e1000e
で、モジュールを有効にしましょう。
有効にしたら、PCをrebootすればOKです。

走り書き的に書いたので後で体裁を整えます。

情報ソースはこちらです。

2008年12月15日月曜日

なかなかいいソフトみつけました

今日はごく一部の方にはとてもお役に立ちそうなソフトをご紹介。

こちらで配布されている、Java RegexTesterというソフトがいい感じです。
最近お仕事で、正規表現と戯れることが増えてきたところで見つけたのでまさにわたりに船って感じで早速活用させてもらっています。

正規表現ってプログラム走らせて実際の結果をprintとかするパターンが多いかも知れませんが、先にこのツールで正規表現を探ってから実装するのがよいです。

おわり。

2008年12月14日日曜日

人工無脳(chatbot)を実装してみる -第三回-

第三回は、ユーザの入力を学習する機能を持たせてみます。

辞書を司るDictionaryクラスに、学習と保存のメソッドを追加しました。

def study(input)
# 入力がランダム辞書に含まれていれば追加しない
return if @random.include?(input)
@random.push(input)
end

def save
open('dicts/random.txt', 'w') do |f|
f.puts(@random)
end
end

果てしなくシンプルです。
studyを呼び出せば、Dictionaryクラスのインスタンス変数@randomにユーザの入力がストアされ、saveを呼び出すと、ストアされた内容をファイルに書き出します。
学習も保存も、基本的には外部からのアクションによって呼び出されるものになるので、Munohクラスから呼び出されるべきです。

#〜略〜
def dialogue(input)
#〜略〜
response = @responder.response(input, @emotion.mood)

# 入力を学習させる
# 応答を決める前に学習させると、オウム返しな返答をしてしまうので
# 先に返答を決めてから学習させる
@dictionary.study(input)

return response
end
#〜略〜
# Dictionaryクラスのメソッドへのつなぎ
# Munohクラスを利用する側からは、Munohクラスのオブジェクトを通してコールすべき
def save
@dictionary.save
end
#〜略〜

という風になる感じです。

オブジェクトが破棄されるときに自動的に書き出してくれたりするともっといいんだろうなと思いつつ、今日はここまで。

NetBeans6.5とJava 6でコンソール文字化け

先ほどleopardのNetBeansを6.5にアップグレードして、ついでにJavaを6に変更したところ、コンソールに出力される日本語が文字化けした。

こちら情報など、ググってたどれるところに書いてあることを試したものの解決せず途方に暮れていたのですが、下記の方法で解決できたので記録しておきます。

「プロジェクトプロパティ」を開き、「実行」メニューの「VMオプション」に「-Dfile.encoding=UTF-8」を追加します。

これだけで、文字化けが解消されました。
この症状を見るに、netbeans.confや「ソース/バイナリ形式」「Javaプラットフォーム」指定は関係ないようです。
そちらの設定はいろいろなパターンを試したのですがウマくいかなかったです。

最後に。
NetBeans関係の情報をBlogに書いてくださっている方々、手がかりをいただいたので感謝感謝です。

おわり

2008年12月13日土曜日

チェックデジット的な何か

超走り書き

/*
1の位を9倍、10の位を8倍、100の位を7倍、1000の位を6倍....
した後、それを11で割った余りの0〜10をそれぞれ「ABCHKMRUXYZ」で表したもの
*/
// 元の値
String num = "1234";
StringBuffer number = new StringBuffer(num);
// チェックデジット
String[] checkDigits = {"A","B","C","H","K","M","R","U","X","Y","Z",};
// かける数
int multNumber = 9;
// 掛けた数を保持する配列
int sumNumbers = 0;
// 各数にかけ算
for (int i = 0; i < number.length(); ++i) {
sumNumbers += number.charAt(i) * multNumber;
}
// その結果を11で割った余りを求める
System.out.println("CheckDigit is : " + checkDigits[sumNumbers % 11]);

人工無脳(chatbot)を実装してみる -第二回-

さて、今回は感情モデルの実装です。
実装したコードを中心に書いていきます。

・感情クラスの実装(抜粋)

class Emotion
MOOD_MIN = -15
MOOD_MAX = 15
MOOD_RECOVERY = 0.5

def initialize(dictionary)
@dictionary = dictionary
@mood = 0
end

def update(input)
# ユーザの入力と、パターン辞書をマッチングし、機嫌値を変動させる
@dictionary.pattern.each do |pattern_item|
if pattern_item.match(input)
adjust_mood(pattern_item.modify)
break
end
end
# 略
end

def adjust_mood(val)
# 略
end

attr_reader :mood
end

・回答パターンを管理するクラス(抜粋)

class PatternItem
SEPARATOR = /^((-?\d+)##)?(.*)$/

def initialize(pattern, phrases)
SEPARATOR =~ pattern
@modify, @pattern = $2.to_i, $3
@phrases = []
phrases.split('|').each do |phrase|
SEPARATOR =~ phrase
@phrases.push({'need'=>$2.to_i, 'phrase'=>$3})
end
end

def match(input)
return input.match(@pattern);
end

def choice(mood)
choices = []
@phrases.each do |phrase|
choices.push(phrase['phrase']) if suitable?(phrase['need'], mood)
end
return (choices.empty?)? nil : Utils.select_random(choices)
end

def suitable?(need, mood)
return true if need == 0
if need > 0
return mood > need
else
return mood < need
end
end

attr_reader :modify, :pattern, :phrases
end

この実装に合わせて辞書の形式を「{感情変動値}##パターン\t{必要感情値}##返答1|{必要感情値}##返答2」
という形式に変更しました。
これによって、感情値に合わせた回答を返す処理を実現できます。
また、自動的に感情値を調整する仕組みを入れることで、感情値が0(ニュートラルな状態)に向かって調整されるようになります。

Macのバックスラッシュ入力

Macで正規表現を書くとき、「¥」を単純に入力しただけでは当たりません。
ちゃんと「alt(option)+¥」で「\」と入力してあげないと、ASCIIの「0x5C」にならないのです。

これをなぜか失念していて2時間ほど無駄な時間を過ごしたので、備忘のため書いておきます。


多分Mac以外の人が見たらどっちも「¥」に見えるんだろうなぁ。。。

2008年12月10日水曜日

LeopardのNetBeans6.1でSVN不具合

MacでNetBeansを使い始めて、Subversionにコミットしようとしたら

Can't convert string from native encoding to 'UTF-8':

というエラーが出て悩まされた。
ググって出てきたページに解決策が書いてあり、非常に助かった。
記事を書いてくれた方ありがとうございます!

人工無脳(chatbot)を実装してみる -第一回-

某書籍を見てまったりと実装してみる。
今日は基本的なコードだけアップします。
(基本的には本ママ)

* Bot本体

require 'Responder'

class Munoh
def initialize(name)
@name = name
# 「ってなに?」と聞き返すレスポンダ
@resp_what = WhatResponder.new("what")
# 固定的な回答をランダムに行なうレスポンダ
@resp_rand = RandomResponder.new("rand")
# デフォルトはランダム
@responder = @resp_rand
end

def dialogue(input)
# 質問が入力された場合に、1/2の確率でレスポンダを選択
@responder = rand(2) == 0 ? @resp_what : @resp_rand
return @responder.response(input)
end

def responder_name
return @responder.name
end

def name
return @name
end
end

* Responder

# レスポンスの基底クラス
class Responder
def initialize(name)
@name = name
end

# 基底クラスには特に実装なし
def response(input)
return ''
end

def name
return @name
end
end

# 「ってなに?」と聞き返すだけの簡単なお仕事をするクラス
class WhatResponder < Responder
def response(input)
return "#{input}ってなに?"
end
end

# ランダムに回答を返すだけの簡単なお仕事をするクラス
class RandomResponder < Responder
def initialize(name)
super
@response = ['さむいね', 'あついよ', '自販機からギザ10出て来た!']
end

def response(input)
# 実際に返答する回答を選択して返す
return @response[rand(@response.size)]
end
end

* これらをキックするmainプログラム

require 'Munoh'

def prompt(munoh)
return munoh.name + ':' + munoh.responder_name + '> '
end

puts 'ChatBot System protorype : むのうたん'
proto = Munoh.new('むのうたん')
while true
print '>'
input = gets
input.chomp!
break if input == ''

response = proto.dialogue(input)
puts prompt(proto) + response
end


ここからちょっとずつ成長させていきます。
あと、書いているコードはCodeReposにUPしてますので、最新版はそっちをご覧ください。

2008年12月8日月曜日

Tomcat6のコネクションプール設定

Tomcat6でコネクションプールを設定したのでその記録。

* META-INF/context.xmlの設定



driverClassName="com.mysql.jdbc.Driver"
maxActive="20" maxIdle="10"
maxWait="-1" name="jdbc/mysql"
username="username" password="password"
type="javax.sql.DataSource"
url="jdbc:mysql://host/dbname">



以上のように設定すると、プログラム中でJNDIを使って取得できるようになる。
コードには、以下のようなJNDI参照のための記述を追加する。

@Resource(name="jdbc/mysql")
private DataResource jdbcMysql;

try {
con = jdbcMysql.getConnection();
} finaly {
con.close();
}

こんな感じか。

finalなMap

アプリケーションの情報を書いたりするのに、public static finalなMapを作ったりする。

public static final Map<String, String> mapObj = new HashMap<String, String>();
static {
mapObj.put("foo", "bar");
mapObj.put("hoge", "fuga");
}

こんな感じで書くこともあるかもしれないが、実はこれだとmapObjがfinalにならない。
(putとかでデータを追記したり、改変したりできる)

これでは意図しないデータの改変などが起こりうるので、変更を許さないようにするには下記のように書かないといけない。


private static final Map<String, String> mapObj;
static {
Map<String, String> tmpMap = new HashMap<String, String>();
tmpMap.put("foo", "var");
tmpMap.put("hoge", "fuga");
mapObj = Collections.unmodifiableMap(tmpMap);
}


Collections.unmodifiableMapに、改変を許可しないMapオブジェクトを渡すと、改変できないMapオブジェクトが得られる。
(正確には、改変しようとすると例外をスローするオブジェクトが得られる)

こうやって書いておくことで、処理の途中でMapが改変されるのを防止できる。

2008年12月6日土曜日

Postfixのスパム対策

かの国からのスパムが頻繁にくるので、対策を行なった。
行なった対策は選択的SMTP拒絶方式というもの。
この対策は設定ファイルだけででき、多少の運用負担を受け入れられるのであれば効果的な方式なので導入することにしました。

リンク先に概要と詳細な説明があるので、今回は自分のサーバでやったことを書いておきます。

・/etc/postfix/main.cfへの設定追記

smtpd_client_restrictions =
permit_mynetworks,
check_client_access regexp:/etc/postfix/white_list,
check_client_access regexp:/etc/postfix/rejections

smtpd_helo_required = yes

smtpd_helo_restrictions =
permit_mynetworks,
reject_invalid_hostname,
check_helo_access regexp:/etc/postfix/helo_restrictions

smtpd_sender_restrictions =
permit_mynetworks,
reject_non_fqdn_sender,
reject_unknown_sender_domain

・/etc/postfix/white_listの作成

#
# To use this file, add following lines into the /etc/postfix/main.cf file:
#
# smtpd_client_restrictions =
# permit_mynetworks,
# check_client_access regexp:/etc/postfix/white_list
# check_client_access regexp:/etc/postfix/rejections
#
# where "white_list" is the name of this file.
#
# *** WHITE LIST ***
#
# When you find a legitimate mail relay server which is rejected by the
# rejection specification written in the /etc/postfix/rejections file, write
# down here a permission specification taking a leaf from the following
# examples.
#
#/^223-123-45-67\.example\.net$/ OK
#/^223\.123\.45\.67$/ OK
#
# Practical examples:
#
# mc1-s3.bay6.hotmail.com, etc.
/\.hotmail\.com$/ OK
#
# web10902.mail.bbt.yahoo.co.jp
/\.yahoo\.co\.jp$/ OK
#
# web35509.mail.mud.yahoo.com
/\.yahoo\.com$/ OK
#
# mail.google.com
/\.google\.com$/ OK
#
# n2.59-106-41-68.mixi.jp, etc.
/\.mixi\.jp$/ OK
#
# mmrts006p01c.softbank.ne.jp, etc.
# tgmsmtkn01sc1.softbank.ne.jp, etc.
/\.softbank\.ne\.jp$/ OK
#
# mmrts006p01c.softbank.ne.jp, etc.
# tgmsmtkn01sc1.softbank.ne.jp, etc.
/\.i\.softbank\.jp$/ OK
#
# imt1omta04-s0.ezweb.ne.jp, etc.
/\.ezweb\.ne\.jp$/ OK
#
# .docomo.ne.jp, etc.
/\.docomo\.ne\.jp$/ OK

・/etc/postfix/rejectionsの作成

#
# To use this file, add following lines into the /etc/postfix/main.cf file:
#
# smtpd_client_restrictions =
# permit_mynetworks,
# check_client_access regexp:/etc/postfix/white_list
# check_client_access regexp:/etc/postfix/rejections
#
# where "rejections" is the name of this file.
#
# *** BLACK LIST ***
#
# When you find a UCE sender's FQDN which is not rejected by the generic
# protection rules specified below, insert here a denial specification taking
# a leaf from the following practical examples. You should specify a subdomain
# name or a substring together with the domain name if possible so that you can
# avoid rejecting legitimate mail relay servers in the same domain.
#
#
# xxx-xxx-xxx-xxx.dynamic.hinet.net
/\.dynamic\.hinet\.net$/ 450 domain check, be patient
#
# *** GENERIC PROTECTION ***
#
# [rule 0]
/^unknown$/ 450 reverse lookup failure, be patient
#
# [rule 1]
# ex: evrtwa1-ar3-4-65-157-048.evrtwa1.dsl-verizon.net
# ex: a12a190.neo.rr.com
/^[^.]*[0-9][^0-9.]+[0-9]/ 450 S25R check, be patient
#
# [rule 2]
# ex: pcp04083532pcs.levtwn01.pa.comcast.net
/^[^.]*[0-9]{5}/ 450 S25R check, be patient
#
# [rule 3]
# ex: 398pkj.cm.chello.no
# ex: host.101.169.23.62.rev.coltfrance.com
/^([^.]+\.)?[0-9][^.]*\.[^.]+\..+\.[a-z]/ 450 S25R check, be patient
#
# [rule 4]
# ex: wbar9.chi1-4-11-085-222.dsl-verizon.net
/^[^.]*[0-9]\.[^.]*[0-9]-[0-9]/ 450 S25R check, be patient
#
# [rule 5]
# ex: d5.GtokyoFL27.vectant.ne.jp
/^[^.]*[0-9]\.[^.]*[0-9]\.[^.]+\..+\./ 450 S25R check, be patient
#
# [rule 6]
# ex: dhcp0339.vpm.resnet.group.upenn.edu
# ex: dialupM107.ptld.uswest.net
# ex: PPPbf708.tokyo-ip.dti.ne.jp
# ex: dsl411.rbh-brktel.pppoe.execulink.com
# ex: adsl-1415.camtel.net
# ex: xdsl-5790.lubin.dialog.net.pl
/^(dhcp|dialup|ppp|[achrsvx]?dsl)[^.]*[0-9]/ 450 S25R check, be patient

・/etc/postfix/helo_restrictionsの作成

# Illegal HELO command blocking specification
# Provided that your mail server's IP address is 223.12.34.56 and its
# acceptable domain name is "example.com", specify as follows:
#
#/^223\.12\.34\.56$/ REJECT
#/^(.+\.)?example\.com$/ REJECT


設定変更と追加が終わったら文法チェックを行なって、Postfixを再起動すればOK
必要に応じて、450を返したアクセスをチェックするシェルスクリプトを用いて、スパムとみなさないネットワークなどの追加メンテナンスを行なうこと。
メールサーバのログをtailしながら送信すると、リアルタイムにホスト名とかネットワークがとれるので、それでやるといいです。

SQLを生成するときに文字列連結しちゃいけない理由

プログラムでSQLを書くときに

String sql = "select *
from TABLE
where COLUMN_ID=" + input;

とか書いてしまうことが初学のときにあった。
これはチョーよくない。

これだとinputに「1 or 0=0」が入ってたりしたときに危険。
なんで危険なのかというと、SQLが

select * from TABLE where COLUMN_ID=1 or 0=0

となってしまう。
こうなると、「0=0」が常に真になるので、どんなクエリでもwhereの条件に当てはまってしまう。

このSQLではデータが全件返ってきてしまい、指定されたID以外のデータが表示されてしまう可能性がある。
こういうのを「SQLインジェクション」って呼ぶ。

じゃあどうやって回避するのかというと、JavaならPreparedStatementを使うのがよくあるやり方。

String sql = "select *
from TABLE
where COLUMN_ID=?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1, input);

setString(1,input)の1というのが、sql中で最初に出てくる?にあたる。
0じゃないのが気になるところ。

ただ、like演算子のときに使う「%」とか「_」は自動的に対策されないので、replaceAllメソッドとかでエスケープする必要がある。

Java SE 6でのJDBCドライバ

Java6からはJDBCの記述が楽になった。
今までは

try {
Class.forName("org.apache.derby.jdbc.ClientDriver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

とか書かないといけなかったが、Java6からは書かなくてよくなった。

forNameを呼び出していたのは、各JDBCのStatic Initializerを呼ぶ必要があるから行なっていたのだが、Java6用のJDBCドライバからは、META-INF/servicesの中に「java.sql.Driver」というファイルを作成し、そこにJDBCクラスのドライバFQDNを書いておくと自動的に読み込まれるようになったそうで、明示的に書く必要はなくなったとのこと。

2008年12月1日月曜日

RewriteRuleに独自処理をかませたい

RewriteMapでPerlとかに渡せます。

例えば、リダイレクト先に渡すパラメタのエンコード文字列を大文字にしたい、とか。

RewriteCond %{HTTP_HOST} ^.*\.www\.myfinder\.jp$
RewriteCond %{REQUEST_URI} ^/contents/.*$
RewriteMap escape-map prg:/usr/local/bin/escape.pl
RewriteRule ^/contents/(.*)$ http://redirect.myfinder.jp/contents/${escape-map:$1} [NE,R=301,L]

escape.plの内容ですが、標準入力から受け取った値を処理して返すというのが基本です。
注意しなければならないのは、ここでエラーが発生してプログラムが止まると、Apacheごと止まる可能性があることです。
使う場合は細心の注意を払って組まないと大変な目に遭うかも。
また、プログラムを更新した場合はApacheの再起動が必要です。

#!/usr/bin/perl

$| = 1;

while (<>) {
chop;
foreach $ch (split //, $_) {
if ($ch =~ /^[a-zA-Z0-9-_.*]/){
print $ch
} else {
$ch =~ s/([^\w])/'%'.unpack('H2', $1)/eg;
$ch = uc $ch;
print $ch
}
}
print "\n";
}

注文してたのが届きました。

これでしばらくBlogネタには困るまい。

2008年11月30日日曜日

GUIのスレッド処理

スレッド処理中にGUIコンポーネントを操作する場合、外部スレッドから操作するとGUIが壊れたりして大変。
そんなときはjava.awt.EventQueueのinvokeLater()とかinvokeAndWait()とかを使うとGUIのスレッド内で処理できる。


java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new ThreadSample().setVisible(true);
}
});

とか

java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
output.append("パネルとかフィールドにテキスト追加");
}
});

とかやる。

2008年11月29日土曜日

forでListを使うときのナントカ

1. いまいちなやり方

for (int i = 0; i < data.size(); ++i) {
String str = (String)data.get(i);
output.append(str + "\n");
}

2. イテレータを使ったやり方

for (Iterator i = data.iterator(); i.hasNext();) {
String str = (String)i.next();
output.append(str + "\n");
}

3. 拡張for文で記述量をさらに圧縮

for (String str : data) {
output.append(str + "\n");
}

ソースも読まずにJavaでRubyのinclude?みたいなのを書いてみる。

単に含まれているかどうか調べるならindexOfでいいんだけど。


public boolean include (String bodyText, String searchWord){

char btext[] = bodyText.toCharArray();
char bsearchWord[] = searchWord.toCharArray();

int n = 0;

while (n < btext.length - 1) {
boolean match = false;
int idx = 0;
while (idx < bsearchWord.length - 1) {
if (bsearchWord[idx] != btext[n + idx]) {
match = false;
break;
}
++idx;
match = true;
}
if (match == true) {
return true;
}
++n;
}
return false;
}

オブジェクトのコピーその2

オブジェクトをコピーするときはその「深さ」についてちゃんと考えないといかんようで、その顕著な例として、多次元配列がわかりやすそう。

今回は2次元配列で

int[][] arr = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};

int[][] copy = arr.clone();

という感じでオブジェクトをコピーすると、配列の外側だけがコピーされて、内側の配列への参照は同じものになる。

元データと、その変化シミュレーションみたいな表示をやりたいときにcloneを使ってしまうと、コピー後のオブジェクトの内側配列に対する操作がコピー前のオブジェクトの内側配列にも反映される。
同じものを参照しているから当然と言えば当然なんだけど、このへん忘れてcloneして喜んでしまいそうなので注意したい。

2次元配列をコピーするときは、多分こうするのがいいと思う。

int[][] arr = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};

int[][] copy = new int[arr.length][]; // とりあえず外側の配列の数合わせて宣言。
for (int i : arr) {
copy[i] = arr[i].clone(); // 改めて内側の配列をコピー
}

という感じで書いておけば、コピー元とコピー先で違うものになるので、妙な挙動はなくなる。

同じオブジェクトを共有したいのか、別々に取り扱いたいのかをはっきりさせるのが大事ってことかなと。

オブジェクトのコピー

単純なオブジェクトのコピーならcloneを使った方がいいのかもしれないけど、ちゃんとコピーしたい場合はコピーコンストラクタとかを作るのがよいようで。

class SampleClass {
SampleClass(int height, int width) {
}

SampleClass(SampleClass sc) {
this(sc.getHeight(), sc.getWidth());
}
}

とか書いて

SampleClass sc1 = new SampleClass(100, 50);
SampleClass sc2 = new SampleClass(sc1);

という風にコピーするのがよさげ。

参照型の性質 - 参照とnull -

例えば


try {
Obj obj = null;
obj.method();
} catch (NullPointerException ex) {
System.out.println("nullpo");
ex.printStackTrace();
}


という感じでNullPointerExceptionを誘発してみるようなプログラムはまずないと思うのだけど、プログラムの外(ファイルとか)から値を受け取る処理を書くときは


if (obj != null) {
obj.method();
}


とかして呼び出すようにしておくように習慣づけたい。

あと、参照型と文字列を「+」演算子で連結するときには、参照型は文字列に変換される。
そのときは自動的にtoString()が呼ばれている。

final static Hoge HOGE = "hoge"の注意点

Javaの話ですが。
忘れがちなのでメモ。

final static Hoge HOGE = "hoge"

とか書いて、宣言したのとは別のソースでその定数を使っている場合、バイトコードに値が入るので、定数が変わった場合はそのソースも再コンパイルが必要です。

以上。

Movable Type 4

ちょっと所用でMT4をセットアップすることがあったのだが、MT4って重厚長大な感じでいまいち。。。

2008年11月23日日曜日

myfinderが運用しているインフラについて

いろいろと技術ネタをまとめていくにあたっての前提となるインフラについて、書いておきます。

myfinderは自宅サーバとして、8ノードからなるシステムを構築しています。



様々な冗長構成を試すためにそれぞれの要素を2重化(DNSとかメールとか管理サーバは突っ込まないで><)して、LVSやmod_proxy_balancerをheartbeatなんかで多重化したりということを試しています。
LB-AP-DB間は2重化する設計なので、どの要素も両方ダウンしない限り基本的にはサービスが継続可能なように構成しています。

DBの多重化なんかも試しています。

これから書いていくネタは基本的にこのインフラをベースにやったことになります。

iptablesについて再度読書。

IPVS(LVS)の設定をするにあたり、一部iptablesの設定をいじることがあり再入門をしてみた。
その際に読んだRedHatLinux Firewallsという本があるのだが、これがなかなかの良書。

03年に出た本なので、出版から5年ほど経っているものの、ipchains以外の内容はあまり腐っていない。
iptablesはいまも現役なので、RedHat系のLinuxを使っていて、iptablesについて知りたい方向けかなと思う。

amazonのレビューにもある通り、初級者→中級者にレベルアップする本という位置づけと考えるといいかも。

やった内容なんかはRedMineにまとめているので、近いうちに公開していきます。

2008年11月20日木曜日

ライトニングトークに行ってきた

CyxxxxLightningTalkに行ってきました。

発表資料もUPしてあります。

2回連続サーバ運用ネタだったので、次からはしばらく違う方向でいくのもいいかも。

SolrのAnalyzerとTokenizer設定を変える

RedMine更新しました。

Solrのschema.xmlのTokenizerを変えれば日本語検索できます。

あとでまた追記。

2008年11月19日水曜日

SolrとかHadoopとかのこと。

興味があってまとめ始めたので、おいらのRedMineの当該プロジェクトを公開にしてみます。


http://repos.myfinder.jp/wiki/hadoop-and-lucene


Wikiに書き込みたい!という場合は適当にご連絡ください。

2008年11月18日火曜日

RedMineのURL処理にちょっと手を入れた。

RedMineデフォルトのURL処理だと、URLと文字列がくっついている場合にそのまま全体をAタグでかこってしまう。
単にURLを貼付けるときに、スペースを入れるように運用で工夫すればいいという話でもあるのだけども、気持ち悪いのでソースに手を入れてみた。
バージョンは0.7-stable

URLの文字列処理は「lib/redmine/wiki_formatting.rb」の124-154行目にある。
そこのコードの正規表現を揉むのもいいんだけど、下記のように書いてしまった方がシンプルで、RedMineのWiki用途くらいであればそこそこ満足いく精度で置換してくれる。


def inline_auto_link(text)
URI.extract(text) do |uri|
text.gsub!(url,"<a class='external' href='#{url}'>#{url}</a>")
end
end


やったことの記憶を頼りに再現してみただけなので、後でコードは直すかも直しました。

関係ないけど「Blogger Syntax Highlighter」がいい感じ。
いろんな言語に対応してて、使い方が簡単。

CodeRepos

CodeReposのコミッタになってみた。

http://coderepos.org/share/wiki/Committers/myfinder

タイミング的に肉が食えると聞いてちょっと作ろうかなと思うものがいくつかあったりなかったりして。
とはいえ自分のRedMineに置いておいても、なんて思うものについてコミットしていく予定。

Bloggerってどうなんだろと思いつつ。

bloggerに引っ越してみる。