忘れたときに備えた記録

トップ 最新 追記
2005|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|11|12|
2009|01|02|03|04|05|06|10|12|
2010|06|07|08|12|
2011|07|09|
2012|09|11|
2013|02|03|09|
2015|10|11|
2016|01|08|11|
2017|02|08|10|
2018|11|

2007-11-13(Tuesday)

$KCODEと正規表現と、正規表現リテラルの文字コード指定

以下のようなソースを、文字コードUTF-8で保存して、-Kの指定無しで実行する。

#!/usr/bin/ruby
module UTF8
   def self.c2u(c)
      [c].pack("U")
   end

   def self.utf_range(a, b)
      "#{c2u(a)}-#{c2u(b)}"
   end
   KATAKANA = utf_range(0x30a0, 0x30ff)
   KANJI = utf_range(0x4e00, 0x9faf)
   RE_KATAKANA = /[#{KATAKANA}]{2,}/uo
   RE_KANJI = /[#{KANJI}]{2,}/uo
end

def test(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/
   puts s.scan(re).join(",")
end

def test_ck(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/
   ok = $KCODE
   $KCODE="n"
   puts s.scan(re).join(",")
   $KCODE=ok
end

include UTF8
s = "漢字とカナ"
puts s[RE_KANJI]
puts s[RE_KATAKANA]
puts s.scan(/[#{KATAKANA}]{2,}|[#{KANJI}]{2,}/).join(",")
puts s.scan(/[#{KATAKANA}]{2,}|[#{KANJI}]{2,}/u).join(",")

puts "none"
test(s)
test_ck(s)

puts "u"
$KCODE="u"
test(s)
test_ck(s)

実行結果は次のとおり

漢字
カナ
漢字とカナ
漢字,カナ
none
漢字とカナ
漢字とカナ
u
漢字,カナ
漢字とカナ

何をするコードか

UTF8::KATAKANAとKANJIは、それぞれUTF8のカタカナと漢字にのみマッチする範囲を表す文字列を返す。 これを使って正規表現を作ったのが UTF8::RE_KATAKANAとRE_KANJI。これらはそれぞれ、2文字以上のカタカナ、あるいは2文字以上の漢字にマッチする。

これを組み合わせて、2文字以上のカタカナまたは2文字以上の漢字にマッチする正規表現を作りたい。

で、Regexp.unionを使わないで結合した場合の挙動を確かめているのが上に挙げたコード。 実際には文字コードオプションの有無やらなにやらでいろいろな組合せを作ったのだけど、長くなったので一部だけ抜粋しました。完全なサンプルは下の方にあります。

出力の後半の方("none"以下)で、漢字,カナと出力されていたら期待どおりの動作をしていると言う事なので、そうなる組合せを調べたわけ。

結果

まとめると次のような結果が得られた。

メソッド呼出元で$KCODE="n"の場合は、どうがんばっても(正規表現を使うときに$KCODE="u"に変更しても)結合した正規表現は期待どおりに動いてくれない。

特に興味深いのは test_kc_u(下のソース参照) の動作で、正規表現を作るときは$KCODE="n"だがオプションで文字コードを指定し、それを使うときには$KCODE="u"となっているが、それでも期待どおりには動かない。

では、どんな条件が揃ったら期待どおりに動くのかというと、

  1. 正規表現を作るときに$KCODE="u"で、使うときにも$KCODE="u" (これは当然)
  2. 正規表現を作るときに$KCODE="u"でオプション指定"u"も有り、使うときにも$KCODE="u" (これも当然)
  3. 正規表現を作るときに$KCODE="u"でオプション指定"u"も有るが、使うときには$KCODE="n"

だった。

最後の例は、 test_ck_u(s)での動作だ。

つまり、

  • 結合した正規表現オブジェクトを作る際に$KCODE="u"とすることがまず必要で
  • 使うときにも$KCODE="u"ならオプション指定"u"はいらないが
  • 使うときに$KCODE="n"なら、オブジェクトを作るときにオプション指定"u"が必要

ということか。

なお、正規表現の結合に Regexp.union を使う場合は、Regexp.unionで結合した正規表現オブジェクトを作るときに$KCODE="u"であれば、使うときには$KCODE="n"でも構わないようだ。これが一番簡単かな。

"o"オプションの意味

おまけで、各test*_oメソッドは、Regexp.unionを使わない結合でオプション"o"を指定したもの。最初に$KCODE="n"として各メソッドを呼び出していて、その後$KCODE="u"としてから各メソッドを再び呼び出しているが、実行結果は変わらない。

つまりメソッド中の正規表現リテラルは"o"オプションが付いたとき、メソッドの中で一意(メソッドを呼び出すたびに作りなおされる)ではなくて、グローバルに一意(C言語でいう関数の中のstatic変数のようなもの) ということらしい。

ソース

実験に使ったサンプルコードの完全版。

#!/usr/bin/ruby
module UTF8
   def self.c2u(c)
      [c].pack("U")
   end

   def self.utf_range(a, b)
      "#{c2u(a)}-#{c2u(b)}"
   end
   KATAKANA = utf_range(0x30a0, 0x30ff)
   KANJI = utf_range(0x4e00, 0x9faf)
   RE_KATAKANA = /[#{KATAKANA}]{2,}/uo
   RE_KANJI = /[#{KANJI}]{2,}/uo
end

def test(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/
   puts s.scan(re).join(",")
end

def test_ck(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/
   ok = $KCODE
   $KCODE="n"
   puts s.scan(re).join(",")
   $KCODE=ok
end

def test_kc(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/
   ok = $KCODE
   $KCODE="u"
   puts s.scan(re).join(",")
   $KCODE=ok
end

def test_u(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/u
   puts s.scan(re).join(",")
end

def test_ck_u(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/u
   ok = $KCODE
   $KCODE="n"
   puts s.scan(re).join(",")
   $KCODE=ok
end

def test_kc_u(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/u
   ok = $KCODE
   $KCODE="u"
   puts s.scan(re).join(",")
   $KCODE=ok
end

def test_o(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/o
   puts s.scan(re).join(",")
end

def test_ck_o(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/o
   ok = $KCODE
   $KCODE="n"
   puts s.scan(re).join(",")
   $KCODE=ok
end

def test_kc_o(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/o
   ok = $KCODE
   $KCODE="u"
   puts s.scan(re).join(",")
   $KCODE=ok
end

def test_uo(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/uo
   puts s.scan(re).join(",")
end

def test_ck_uo(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/uo
   ok = $KCODE
   $KCODE="n"
   puts s.scan(re).join(",")
   $KCODE=ok
end

def test_kc_uo(s)
#  re = Regexp.union(RE_KANJI, RE_KATAKANA)
   re = /#{RE_KANJI}|#{RE_KATAKANA}/uo
   ok = $KCODE
   $KCODE="u"
   puts s.scan(re).join(",")
   $KCODE=ok
end

include UTF8
s = "漢字とカナ"
puts s[RE_KANJI]
puts s[RE_KATAKANA]
puts s.scan(/[#{KATAKANA}]{2,}|[#{KANJI}]{2,}/).join(",")
puts s.scan(/[#{KATAKANA}]{2,}|[#{KANJI}]{2,}/u).join(",")

puts "none"
test(s)
test_ck(s)
test_kc(s)
test_u(s)
test_ck_u(s)
test_kc_u(s)
test_o(s)
test_ck_o(s)
test_kc_o(s)
test_uo(s)
test_ck_uo(s)
test_kc_uo(s)

puts "u"
$KCODE = "u"
test(s)
test_ck(s)
test_kc(s)
test_u(s)
test_ck_u(s)
test_kc_u(s)
test_o(s)
test_ck_o(s)
test_kc_o(s)
test_uo(s)
test_ck_uo(s)
test_kc_uo(s)

その実行結果

漢字
カナ
漢字とカナ
漢字,カナ
none
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
u
漢字,カナ
漢字とカナ
漢字,カナ
漢字,カナ
漢字,カナ
漢字,カナ
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
漢字とカナ
Tags: Ruby

2007-11-23(Friday)

複数のスレッドが同じファイルを"a"モードで開いた時の挙動

"a"モードで開くと、常に末尾に追加されるという動作を確認するためのスクリプトを作ったのでメモ。

#!/usr/bin/ruby
t1 = Thread.start do
   sleep(1)                  # t2がファイルを開くのを待ってから
   open("test", "a") do |f|  # ファイルを開き
      puts "t1_open"
      f.flock(File::LOCK_EX) # すぐにロックする
      puts "t1_lock"
      f.puts "t1"*10         # 書き込む
   end
end

t2 = Thread.start do
   open("test", "a") do |f| # (先に)ファイルを開いて
      puts "t2_open"
      sleep(2)              # t1がファイルをロックして書き込むのを待つ
      f.flock(File::LOCK_EX)
      puts "t2_lock"
      f.rewind
      f.puts "t2"*10        # t1の書き込むが終わってから書き込む
   end
end

t1.join
t2.join

これが例えばt2のopenモードが"w"だったりすると、ファイル先頭から"t2"*10だけが書き込まれたファイルになる。

Tags: Ruby

PStore#transaction が競合した時の挙動

こっちが本命で、PStore#transactionで同じファイルにアクセスしたときに、ロックがかかっていることを確かめるテストスクリプト。

#!/usr/bin/ruby
require "pstore"

t1 = Thread.new do
   PStore.new("data").transaction do |db|
      puts "t1: start sleep"
      db["data"] = 1
      sleep(1)
   end
   puts "t1: end"
end

t2 = Thread.new do
   puts "t2: before transaction"
   PStore.new("data").transaction(true) do |db|
      puts "t2: start sleep"
      db["data"]
      sleep(2)
   end
   puts "t2: end"
end

t1.join
sleep(1)
t3 = Thread.new do
   puts "t3: before transaction"
   PStore.new("data").transaction(true) do |db|
      db["data"]
   end
   puts "t3: end"
end
t3.join

t4 = Thread.new do
   puts "t4: before transaction"
   PStore.new("data").transaction do |db|
      puts "t4: in transaction"
      db["data"]=2
   end
   puts "t4: end"
end
t2.join
t4.join
t2: before transaction
t1: start sleep
t1: end
t2: start sleep
t3: before transaction
t3: end
t4: before transaction
t2: end
t4: in transaction
t4: end

transaction(false)がLOCK_EX、transaction(true)がLOCK_SHとして振る舞っていることが分かる。

Tags: Ruby

2007-11-24(Saturday)

Marshal.load とブロック

Marshal.loadには、引数として手続きオブジェクトを渡すことはできるけど、ブロック付きで呼び出すことはできないと分かった。

サンプルソース

#!/usr/bin/ruby
a = [1, 2, 3]

s = Marshal.dump(a)
puts "block"
Marshal.load(s) do |o|
   p o
end
puts "proc"
Marshal.load(s, proc{|o| p o})

結果

block
proc
1
2
3
[1, 2, 3]
Tags: Ruby

インスタンス変数が追加されたクラスのMarshalデータをloadしたとき

  • あるクラスCのオブジェクトをMarshal.dumpする
  • バージョンアップなどでクラスCにインスタンス変数が追加される
  • 古いバージョンのクラスCオブジェクトのMarshalデータをMarshal.loadする

と言ったときに、どういうことが起こるのか実験してみた。

スクリプト1 dump側

#!/usr/bin/ruby
class C
   def initialize(v)
      @v = v
   end
end

open("db", "w") do |f|
   Marshal.dump(C.new(:hoge), f)
end

スクリプト2 load側 (Cのインスタンス変数が増えている)

#!/usr/bin/ruby
require "yaml"
class C
   attr_reader :v, :v2
   def initialize(v)
      @v = v
      @v2 = [v, v]
   end
end

c = open("db") do |f|
   Marshal.load(f)
end

puts c.is_a?(C)
p c.v
p c.v2

実行結果

true
:hoge
nil

例外を投げたりせず、追加されたインスタンス変数の内容をnilとして、インスタンスを作成している。

Tags: Ruby

読み書き両用モードでのopenと、File#truncate

File#truncate(0)を実行した直後の書き込み位置がモードによって違うことに気づかず、結構難儀したのでメモ。

サンプル

#!/usr/bin/ruby
def test(log, mode)
   open(log, "a") do |f|
      f.puts "1st"
   end
   open(log, mode) do |f|
      puts "pos1", f.pos
      puts "read..", f.read, "end"
      f.truncate(0)
      puts "pos2", f.pos
      f.puts "2nd"
      puts "pos3", f.pos
   end
   puts "dump", IO.read(log).inspect
end

test("log_a", "a+")
test("log_r", "r+")

結果

pos1
0
read..
1st
end
pos2
4
pos3
4
dump
"2ndn"
pos1
0
read..
1st
end
pos2
4
pos3
8
dump
"0000000000002ndn"

"a+"では、truncate(0)直後もposの値は古いままだけど、書き込みはファイル先頭から行われる。

一方"r+"では、truncate(0)直後もposの位置は古いままで、書き込み時には、そこまでを000で埋めてから追記される。大抵の場合は、truncate(0)の後にrewindをするのが正しい記述だろう。

それと、posの値なんだけど、リファレンスマニュアルでは

"a+": "r+" と同じですが、オープン時にファイルがすでに存在していれば読み書き位置がファイルの末尾にセットさます。

とある。しかしこの実験では、"a+"で存在するファイルを開いても、読み込み位置はファイルの先頭(0)になっている。あれ?バグ?

Tags: Ruby

open(file, "a+")のpos

fopenのマニュアルだと

     a+      読み出しおよび追加 (ファイルの最後に書き込む) のために開く。ファイルが存在していない場合には新たに作成する。読み出しの初期ファイル位置はファイルの先頭であるが、書き込みは常にファイルの最後に追加される。

とあるから、Rubyのリファレンスマニュアルの方が間違ってるのかな。

Tags: ruby

2007-11-26(Monday)

Bayesフィルタ

唐突ですが、次のようなゲームを考えます。

  1. デカい箱に、印刷されたメールがどっさり入っている
    • メールは、1通につき1枚の紙に印刷されている
    • SPAMメールか、非SPAMメール(HAMメール)かが、一目で分かるようになっている(紙が色分けされているとか)。
  2. 箱から手探りで、メールを1通取り出す。
    • この時点で、メールがSPAMかHAMかが分かる
  3. メールから単語をでたらめに1つ選ぶ

さて、このゲームをこれから始めるとすると、箱に入っているメールに関して次のことが分かっていると、いくつかの確率を求めることができます。

これらから、次の確率が定まります

これらの情報を元に、次の問題を考えます。

最終的に選ばれた単語がwであるときに、箱から選んでいたメールがSPAMであった確率はいくらでしょうか?

つまり、$P(\lambda\in\Lambda_S|w)$を計算しようというわけです。計算できたら何が嬉しいかというと、例えば、「Viagraという単語を含むメールがSPAMである確率はいくらか」ということが計算できるわけです。

さて。まず、条件付き確率の定義から $$P(\lambda\in\Lambda_S|w) = \frac{P((\lambda\in\Lambda_S)\cap w)}{P(w)}$$ が成り立ちます。

ここで、$(\lambda\in\Lambda_S)$とは「取り出したメールがスパムである」という事象なので、各SPAMを$\lambda_i \in \Lambda_S (i=1, 2, 3, \dots, k)$(ただし、$k=|\Lambda_S|$)と書くことにすると、 $$(\lambda\in\Lambda_S) = (\lambda\in\{\lambda_i(i=1,2,3,\dots,k)\})$$ と書けるので、 $$P((\lambda\in\Lambda_S)\cap w) = P(\lambda_1\cap w) + P(\lambda_2\cap w) + \cdots + P(\lambda_k\cap w)$$ という和に分解できます。

一方、$P(w)$ですが、これも $$P(w) = P(w\cap \lambda\in\Lambda) = P(\lambda_1\cap w) + P(\lambda_2\cap w) + \cdots + P(\lambda_l\cap w)$$ と分解できます。

なお、ここで$l=|\Lambda|$(つまり全メール数)であり、箱の中の各メールを $\lambda_i(i = 1, 2, \dots, k, k+1, k+2, \dots, l)$ と書いています。

以上より、求めたかった確率が計算できます。なお、次の式では「メール$\lambda_i$に含まれる単語wの数」を個別に$w_{\lambda_i}$で表しています。

\begin{eqnarray}
P(\lambda\in\Lambda_S|w) &=& \frac{P((\lambda\in\Lambda_S)\cap w)}{P(w)} \\
&=& \frac{P((\lambda\in\Lambda_S)\cap w)}{P(w\cap \lambda\in\Lambda)}\\
&=& \frac{P(\lambda_1\cap w) + P(\lambda_2\cap w) + \cdots + P(\lambda_k\cap w)}{P(\lambda_1\cap w) + P(\lambda_2\cap w) + \cdots + P(\lambda_k\cap w) + P(\lambda_{k+1}\cap w) + \cdots + P(\lambda_l\cap w)} \\
&=&
\frac{\frac1{|\Lambda|}\frac{w_{\lambda_1}}{|\lambda_1|} +
\frac1{|\Lambda|}\frac{w_{\lambda_2}}{|\lambda_2|} +
\frac1{|\Lambda|}\frac{w_{\lambda_3}}{|\lambda_3|} +
\cdots +
\frac1{|\Lambda|}\frac{w_{\lambda_k}}{|\lambda_k|}}
{
\frac1{|\Lambda|}\frac{w_{\lambda_1}}{|\lambda_1|} +
\frac1{|\Lambda|}\frac{w_{\lambda_2}}{|\lambda_2|} +
\frac1{|\Lambda|}\frac{w_{\lambda_3}}{|\lambda_3|} +
\cdots +
\frac1{|\Lambda|}\frac{w_{\lambda_k}}{|\lambda_k|} +
\frac1{|\Lambda|}\frac{w_{\lambda_{k+1}}}{|\lambda_{k+1}|} +
\frac1{|\Lambda|}\frac{w_{\lambda_{k+2}}}{|\lambda_{k+2}|} +
\frac1{|\Lambda|}\frac{w_{\lambda_{k+3}}}{|\lambda_{k+3}|} +
\cdots +
\frac1{|\Lambda|}\frac{w_{\lambda_l}}{|\lambda_l|}
} \\
&=&
\frac{\frac{w_{\lambda_1}}{|\lambda_1|} +
\frac{w_{\lambda_2}}{|\lambda_2|} +
\frac{w_{\lambda_3}}{|\lambda_3|} +
\cdots +
\frac{w_{\lambda_k}}{|\lambda_k|}}
{
\frac{w_{\lambda_1}}{|\lambda_1|} +
\frac{w_{\lambda_2}}{|\lambda_2|} +
\frac{w_{\lambda_3}}{|\lambda_3|} +
\cdots +
\frac{w_{\lambda_k}}{|\lambda_k|} +
\frac{w_{\lambda_{k+1}}}{|\lambda_{k+1}|} +
\frac{w_{\lambda_{k+2}}}{|\lambda_{k+2}|} +
\frac{w_{\lambda_{k+3}}}{|\lambda_{k+3}|} +
\cdots +
\frac{w_{\lambda_l}}{|\lambda_l|}
}
\end{eqnarray}

つまり、 $$
\frac{\left(\begin{array}{c}\mbox{SPAMメール全部についての、}\\\mbox{メールごとの(単語wの数/メール全体の単語数)の総和}\end{array}\right)}{\left(\begin{array}{c}\mbox{SPAM,HAM区別無しで
メール全部についての、}\\\mbox{メールごとの(単語wの数/メール全体の単語数)の総和}\end{array}\right)}
$$
という計算になります。

また、通常の条件付き確率$P(w|\lambda)$の順番を逆にした確率$P(\lambda|w)$を求めているこの式変形が、Bayesの定理と呼ばれています。

で、このままだと保存する値が実数値$\frac{w_\lambda}{|\lambda|}$になってしまうので、HAMへのバイアスも兼ねて、代わりに $$
\frac{\frac{\mbox{全SPAM中の単語wの数}}{\mbox{SPAMメールの数}}}
{\frac{\mbox{全SPAM中の単語wの数}}{\mbox{SPAMメールの数}} +
\frac{\mbox{全HAM中の単語wの数}}{\mbox{HAMメールの数}}}
$$
を使って単語のSPAM率を求めましょうというのが、Paul Grahamが A Plan for Spam (和訳)で書いた式の意味なのではないでしょうか。

と言うようなことを2週間前に研究室のセミナーで紹介しまして、それに際して実験用にRubyで、SPAM率を求めるプログラムを作りました。

Bayesライブラリ

そんなわけで実験用にスパムメール判定プログラムを作ったら結構いい具合に動いたので、このようなBayesフィルタリングの

  • メッセージを与えたらトークンに分解したり
  • データベース(Hash)にメッセージ毎のトークンを追加したり
  • そこからトークン毎のスパム率を求めたり

といった処理をまとめ直したライブラリを作りました。とりあえずここに置いてあります。

メール用のBayesフィルタリングでは、すでに bsfilterという素晴らしいものが公開されているので、僕のライブラリでは汎用性を主眼に置いています。具体的には、tDiary用のスパムフィルタが作れたりします。

Tags: Ruby

tDiary用Bayesフィルタ

と言うわけで作りました、tDiary用Bayesフィルタ spambayes.rb。ここに置いてあります。

tDIaryのフィルタリングの一種でtdiary/filter/に置いて使う用のものですが、ツッコミをSPAMと判定したときや、設定画面から「実はHAMなので投稿を受け付ける」と言う操作が行えます(HAMと判定されたツッコミが実はSPAMだったときに、非表示にするという操作を設定画面で行うことまでは出来ないですが)。

あと、リンク元スパムのフィルタリング機能も提供していて、やはり設定画面で「SPAM/HAMと判定されたものをHAM/SPAMとしてデータベースに追加する」という事ができます。

この日記ですでに動かしているのですツッコミとか頂いてもすぐには登録されないかもしれません。

Tags: SpamBayes

2007-11-27(Tuesday)

SPAMかむひあ

一昨日あたりからこの日記でBayesフィルタの試用を始めているのだけど、以前までは日に5回前後来ていたツッコミスパムがなぜかその頃から妙に来なくなっていて、さっき、ようやく2通目のツッコミスパムが届いた。

よしよし、中でバグっていてツッコミを受け損なっているわけではないな。もっと来ないかなぁ。


2007-11-30(Friday)

NamedBrancheについて

hg こと mercurial では特に指定しない場合、branch名が'default'になる。このままで更新を続けて、ある時点で新しいブランチを作ると、ちょっとした不便が発生するという罠があることに気がついたのでまとめてみた。

まずは、次の作業を見てみる。行の '#'以降は、後で追記したコメントだ。

hiraku@hirakuro:~/tmp$ hg init tr           # truncリポジトリを作る
hiraku@hirakuro:~/tmp$ cd tr
hiraku@hirakuro:~/tmp/tr$ ls -al / > root
hiraku@hirakuro:~/tmp/tr$ hg add                   # ファイルを追加して
adding root
hiraku@hirakuro:~/tmp/tr$ hg ci -m InitialImport  # チェックイン
hiraku@hirakuro:~/tmp/tr$ cd ..
hiraku@hirakuro:~/tmp$ hg clone tr br              # clone で新しいブランチを作り
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
hiraku@hirakuro:~/tmp$ cd br
hiraku@hirakuro:~/tmp/br$ hg branch B              # そのブランチ名をBとする
marked working directory as branch B
hiraku@hirakuro:~/tmp/br$ ls -al /usr > usr
hiraku@hirakuro:~/tmp/br$ hg add                   # ブランチ B でファイルを追加して
adding usr
hiraku@hirakuro:~/tmp/br$ hg ci -m Add-usr         # チェックイン
hiraku@hirakuro:~/tmp/br$ cd ..
hiraku@hirakuro:~/tmp$ hg clone br local           # Bからcloneして、作業ディレクトリを新たに作る
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
hiraku@hirakuro:~/tmp$ cd local
hiraku@hirakuro:~/tmp/local$ hg id                 # Bからcloneしたリポジトリのidがtip(最新リビジョン)でない
a67c0ebc8c82
hiraku@hirakuro:~/tmp/local$ ls                    # Bで作ったファイルがない
root
hiraku@hirakuro:~/tmp/local$ hg log                # 実は、B からcloneした直後の作業領域は、Rev.0のもの
changeset:   1:c8c5500f5b06
branch:      B
tag:         tip
user:        hiraku
date:        Fri Nov 30 11:46:22 2007 +0900
summary:     Add-usr

changeset:   0:a67c0ebc8c82
user:        hiraku
date:        Fri Nov 30 11:45:45 2007 +0900
summary:     InitialImport

hiraku@hirakuro:~/tmp/local$ hg branches           # inactiveとか、分かってるのに〜
B                              1:c8c5500f5b06
default                        0:a67c0ebc8c82 (inactive)

ブランチBからcloneしても、それで新しく作られるローカルリポジトリの作業領域のリビジョンは、defaultブランチの最新リビジョン(この場合は Rev.0)になってしまう。

今度は、truncリポジトリを作るときにブランチ名を付けて試してみよう。

hiraku@hirakuro:~/tmp$ hg init tr              # リポジトリを作って
hiraku@hirakuro:~/tmp$ cd tr
hiraku@hirakuro:~/tmp/tr$ hg branch T          # のっけからブランチ名 T(trunc)をつける
marked working directory as branch T
hiraku@hirakuro:~/tmp/tr$ ls -al / > root
hiraku@hirakuro:~/tmp/tr$ hg add               # ファイルを追加して
adding root
hiraku@hirakuro:~/tmp/tr$ hg ci -m InitialImport # チェックイン
hiraku@hirakuro:~/tmp/tr$ hg log
changeset:   0:790e3d51f8ee
branch:      T                                    # ちゃんとブランチ名がついている
tag:         tip
user:        hiraku
date:        Fri Nov 30 12:05:19 2007 +0900
summary:     InitialImport

hiraku@hirakuro:~/tmp/tr$ cd ..
hiraku@hirakuro:~/tmp$ hg clone tr br          # clone でブランチリポジトリを作り
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
hiraku@hirakuro:~/tmp$ cd br
hiraku@hirakuro:~/tmp/br$ hg branch B          # ブランチ名Bをつける
marked working directory as branch B
hiraku@hirakuro:~/tmp/br$ ls -al /usr > usr
hiraku@hirakuro:~/tmp/br$ hg add               # ファイルを追加して
adding usr
hiraku@hirakuro:~/tmp/br$ hg ci -m Add-usr     # チェックイン
hiraku@hirakuro:~/tmp/br$ hg log
changeset:   1:6472406e6690
branch:      B
tag:         tip
user:        hiraku
date:        Fri Nov 30 12:05:57 2007 +0900
summary:     Add-usr

changeset:   0:790e3d51f8ee
branch:      T
user:        hiraku
date:        Fri Nov 30 12:05:19 2007 +0900
summary:     InitialImport

hiraku@hirakuro:~/tmp/br$ cd ..
hiraku@hirakuro:~/tmp$ hg clone br local     # cloneでBからローカルリポジトリを作る
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
hiraku@hirakuro:~/tmp$ cd local/
hiraku@hirakuro:~/tmp/local$ hg id           # 作業領域のリビジョンはtip
6472406e6690 (B) tip
hiraku@hirakuro:~/tmp/local$ ls              # ちゃんと、Bブランチの最新版のファイルが揃っている
root  usr

ブランチ名をdefaultのままにしていると、ブランチを切って平行して更新したり、それを外部からcloneで取得してもらう時に不便が生じる。

というわけで、公開中のhgリポジトリを全部作り直すことにしました。アクセスログを見る限りcloneで持っていった人は居ないはずですが、念のためお知らせです。

Tags: Mercurial

リポジトリの再構築

ブランチ名を付けた上でのリポジトリの作り直し、終了〜。つ、疲れた

Tags: 更新

インストールとスクリーンショット

こちらからダウンロードしたものを展開すると、次のようなファイルが展開されます。

spambayes-2007113000-2.1
|-- filter
|   `-- spambayes.rb
|-- lib
|   `-- bayes.rb
`-- plugin
    |-- en
    |   `-- spambayes.rb
    |-- ja
    |   `-- spambayes.rb
    `-- spambayes.rb

tDiaryをインストールしたディレクトリをTDIARYと書くと、

  • filter/spambayes.rb : TDIARY/tdiary/filter
  • lib/bayes.rb : TDIARY/misc/lib/bayes.rb
  • plugin以下のファイル : TDIARY/plugin

へコピーします。

設定画面

設定画面で、必要な項目にチェックを入れます。上から順に

  • コメントのSPAM検出にSpamBayesを使うか
  • リンク元のSPAM検出にSpamBayesを使うか
  • (デバッグ用の)エラーログをキャッシュディレクトリに出力するか
  • 閾値(SPAM率がこの値以上のものをSPAMとみなす)
  • コメントが届いたときに通知するメールアドレス(comment_mail-*プラグインが有効になっている必要があります)
  • スパム率を計算する方法
    • PLAIN : トークンのSPAM率を定理通りに求める
    • PaulGraham : 'A Plan for Spam'で紹介されている式で計算する

です。

ツッコミ一覧

フィルタリングが有効になった場合、ツッコミはこのページに追記されていきます。 計算方式がPaulGrahamの場合は必ずSPAMかHAMか判定しますが、Plainの場合は以下の場合に判定不能として処理します(PaulGrahamの方法ではこの様な場合はHAMと判定します)。

  • データベースが空の場合
  • SPAMデータベースにのみ入っているトークンとHAMデータベースにのみ入っているトークンの両方が含まれている場合

この場合は、Doubtという欄にコメントが登録されます(スクリーンショットは少し古い版で、Unknownと表記しています。以下同様)。

HAM/SPAMの場合は「SPAM/HAMとして処理する」を選ぶとデータベースが更新され、以降は同様のツッコミをSPAM/HAMとして認識するようになります。Doubtの場合も同様です。

HAM/SPAMの場合は他に「HAM/SPAMのまま」という選択肢があり、この場合は、データベースの更新は行いません。

いずれを選択しても、ツッコミのデータはキャッシュディレクトリの cache/bayes/corpus/以下にコピーされ、その後の「データベースの再構築」で使われます。

トークン表示その1

これは、あるSPAMツッコミのトークンの一覧です。データベースが不十分なため、かなりの単語が'Unknown'(最新版では'Doubt')となっています。

トークンその2

トークン一覧の続きです。いくつかのトークンがSPAM率=1.0と判定されるとともに、"vmware"という単語がSPAM率=0.0として表示されています。

これはまだデータベースへの反映を行っていないからで、このツッコミを「SPAMとして処理」すると、Doubtだった単語のSPAM率が1.0となり、"vmware"のSPAM率が少し上がるでしょう。

他に、tDiaryのツッコミでは無効なhrefというキーワードがバッチリSPAM率=1.0になっているのが分かります。いえい。

トークンその3

ツッコミを投稿してきた際の名前やメールアドレス、投稿元のホストアドレスも、トークンとして使われます。

特にホストアドレスはオクテット毎に分解して扱うので、特定のネットワークからツッコミがくるという場合にもうまく対応するはずです。

リンク元一覧

リンク元も、フィルタリングの対称とすることができます。

Googleなど一部の検索エンジンがリンク元の場合は、検索キーワードだけをトークン化して、ツッコミ本文と同じカテゴリのトークンに追加します。このため、ツッコミのほとんどがSPAMという日記(このページとか!!)でも、HAMデータベースが寂しいことにならずに済みます。

リンク元SPAM

いわゆる「リファラスパム」です。反転している部分の1つ下のリンク元は、実はSPAMではないのですが、データベースが不十分なせいで「.orgがリンク元だったらSPAM」という判定がなされてしまっています(もう更新しましたが)。

リンク元トークン

これがそのリンク元のトークン一覧。.orgだけがSPAM率1となっているせいで、SPAMと誤判定された様子が分かります。

リンク元URLも部分で区切ってあって、上の例のように「特定のトップレベルドメインからリファラスパムがどっさり!!」とかでも対応できるようになっています。

報告メール

ツッコミが投稿されると、このようなメールが届きます(メールアドレスを設定してある場合のみ)

メール中のリンクは上から順に

  • そのツッコミをSPAMとして処理する
  • HAMとして処理する
  • ツッコミされた日記

というページへのリンクです。SPAM/HAMとして処理するリンクは、リンク先でもう一度確認されます。

報告メールは

  • 件名 : HAM/SPAM/DOUBTで始まる
  • 差出人 : BayesFilter

となるので、procmailとかで処理することができます。

Tags: SpamBayes