Buho No.212 目次

人工無能入門

まったか


某日のログ

(04:10:24)[こんの] おー。
(04:10:29)[こんの] 眠いぞー。
(04:10:29)[とさか] 気合が足りんのう、こんの。
(04:10:40)[こんの] そんなことを言っても。
(04:10:50)[こんの] ねむいのはしょうがないのだ。
(04:10:51)[むのう] ねれー。:>
(04:10:51)[とさか] 気合が足りんのう、こんの。
(04:11:00)[こんの] 寝るぞー。

これは某italkサーバーのログです。 italkらしい和気藹々とした会話が展開されていますね。 でも、この場面で実際にログインしている人間は一人しかいません。 (もちろん、こんの君ですよ!!)  実は、あとの二人?(「とさか」と「むのう」)は、あらかじめ反応を決められた プログラムなのです。よく観察してみると、だいたい次のような法則で反応している ことが推測されます。
とさか:「眠い」「ねむい」「気合いが足りんのう、○○」
むのう:「ねむい」「ねれー:>

このようなプログラムのことを人工無能(無能)と呼びます。 すごく単純な仕組みですが、うまく工夫すればitalkに楽しいメンバーを 一人加えることができるかもしれません。 今回は、この人工無能の作り方について書いてみたいと思います。 言語はPerl(Perl5)を使いますが、別にPerlで書く必要性はありません。 ただ、Perlだと開発が楽ですし、正規表現(あとで説明します)を簡単に使うことが できるという利点があります。 1

人工無能の仕組み

人工無能のすることはたった2つです。1つは、italkサーバーに接続すること。 そして、接続したら、ログを1行ずつ読んでそれに対して反応を返すことです。 図のような流れになります。

    italkサーバに接続
        |
———————→↓←—————————
↑     ログを1行読む     ↑
|       ↓         |
|  キーワードが見つかったか——→|
|       ↓ YES         NO
|     反応を返す
|       ↓
|←———————

では、人工無能が実際にどのような処理をしているのかソースを見ながら 説明してことにします。

まず、italkサーバーへの接続です。これは処理が決まっているのでそのまま 使って下さい。(実は私もよくわかっていないのです。(^^; 2

$myhandle = "とさか";
$port = 12345;
$server = "backup.italk.ne.jp";

## サーバーへの接続処理 ##
$sockaddr = 'S n a4 x8';
($name, $alias, $proto) = getprotobyname('tcp');
($name, $alias, $type, $len, $hostaddr) = gethostbyname($server);
$sock = pack($sockaddr, 2, $port, $hostaddr);
socket(S, 2, 1, $proto) || die $!;
connect(S, $sock) || die $!;

select(S);
$| = 1; select(STDOUT);

print S $myhandle, "\n"; # 自分のハンドルを告げる

これでitalkサーバに接続できました。 ログを1行ずつ読むには、次のようにします。

while(<S>) {
    #print; # ログを標準出力に表示したいとき

            # ここに必要な処理を書く
}

これで終わりです。あとは、while節の中に、 どのようなキーワードに対して どう反応するべきかを記述していくだけです。 一般に、

if (/keyword/) {
    print S "xxxxxxxxx\n";
    next;
}

で、ログ中のキーワードkeywordに対して、 xxxxxxxxという反応を返すことができます。 末尾の \n(改行)を忘れると いつまでたっても発言が送られないので注意してください。 それから、printのあとのSは、 発言の送り先(italkサーバー)です。 忘れると標準出力の方に書き出されます。 逆に、デバッグなどで標準出力に メッセージを書き出す必要があるときは、 print "xxxxxxxx\n" として下さい。

さきほどの「むのう」のように、 キーワード「ねむい」に対して、「ねれー:D」と 反応するには次のようになります。

if (/ねむい/) {
    print S "ねれー:D\n";
    next;
}

正規表現を使う

無能を作るには、このような条件節をどんどん書き足していけばいいのですが、 もう少し厳格にキーワードを判断したいときがあります。

例えば、特定の人に対する反応だということを強調するため、 発言者のハンドル名を取得して使いたいというときがあります。 「とさか」は「気合いが足りんのう、○○」と答えています。

また、さきほどの例でいうと、誰かがいじわるして、 「ねむい」というハンドルを使ったときに問題が起こります。 そうすると、無能にはハンドルと発言の区別が付かないので 「ねむい」さんが発言する度にいちいち反応してしまい、 非常にうざったくなります。 「ねむい」というハンドルを使う人はなかなかいないと思いますが、 同じようなことがすべてのキーワードについて言えるので、 ハンドルと発言は区別しなければいけません。

こういうような処理をするときに、正規表現が役に立ちます。 正規表現は「ある規則に基づいて文字列(記号列)の集合を表す方法の1つ」です。 grepやawk、sedなどのUNIXの検索、置換コマンドでよく使われます。 最初は少しわかりづらいかと思いますが、 覚えると大変便利なのでこの際覚えておくと役に立ちますよ。

都合上、正規表現についての説明ははしょりますので、各自調べて下さい。

以下に主な正規表現だけまとめておきます。

cその文字自身(メタキャラクターを除く)
\mメタキャラクターmの、その文字自身
^行頭
$行末
./td>任意の1文字
[c1 c2 ... cn]集合c1からcnの中の任意の1文字
[c1-c2]c1からc2までの範囲の中の任意の1文字
r*正規表現rの0回以上の繰り返し
(\r\)正規表現のグループ化

^ $ . * ( [ ) ] \ などの特殊な意味を持った文字を メタキャラクターといいます)

そこで、今度はitalkのログがどういう構造になっているのか見てみましょう。

(04:10:50)[こんの] ねむいのはしょうがないのだ。

左から時刻、ハンドル、発言というようになっています。 時刻は () で、ハンドルは [] で囲まれており、 1文字分スペースが空いて発言の部分に 分かれています。ハンドルを取得するには、次のようにします。

## 発言のフィルター ##
if (/^\(..:..:..\)\[(.*)\] (.*$)/) {
    $handle = $1; $_ = $2;
    if ( $handle eq $myhandle ) {
        next; # 自分の発言は排除
    }
}

これで$handleにハンドル名が取得されます。 3 発言部分は$_にとっておきます。 そうすれば、キーワードの判断は発言部分に関してだけ行われます。 「とさか」の反応は次のようにして書けます。

## 発言のフィルター ##
if (/^\(..:..:..\)\[(.*)\] (.*$)/) {
    $handle = $1; $_ = $2;
    if ( $handle eq $myhandle ) {
        next; # 自分の発言は排除
    }

    if (/ねむたい/ | /ねむい/  | /眠/ ) {
        print S "気合がたりんのう、",$handle,"。\n";
        next;
    }
    next;
}

ところで、italkのログは普通の発言だけでなく、過去ログやユーザーのログイン& ログアウト、電報など様々な形があります。それらをいちいち調べて正規表現にあて はめるのも面倒かと思いますので、私の方でサンプルソースを用意しておきました。

プログラムリスト(pmuno.pl)

#!/usr/local/bin/perl
# italkの設定
$server = "backup.italk.ne.jp";
$port = 12345;
$myhandle = "無能君";

# サーバーへの接続処理
$sockaddr = 'S n a4 x8';
($name, $alias, $proto) = getprotobyname('tcp');
($name, $alias, $type, $len, $hostaddr) = gethostbyname($server);
$sock = pack($sockaddr, 2, $port, $hostaddr);
socket(S, 2, 1, $proto) || die $!;
connect(S, $sock) || die $!;

select(S);
$| = 1;
select(STDOUT);
print S $myhandle, "\n";

$bl = 0; # 過去ログ排除用フラグ
while (<S>) {
    # ログイン時に表示される過去ログの排除
    if (/^\#\# .*BACK LOG START/) { $bl = 1; }
    if (/^\#\# .*BACK LOG END/)   { $bl = 0; }
    if ($bl) { next; }

    # 発言のフィルター
    if (/^\(..:..:..\)\[(.*)\] (.*$)/) {
        $handle = $1; $_ = $2;
        if ( $handle eq $myhandle ) { next; } # 自分の発言は排除
        if (/こらー/) {
            print S "ごめんなさい、",$handle,"さん\n";
        }
        next;
    }

    # ログインに対する反応
    if (/^\(\[(.*)\@.*logged in/) {
        $handle = $1;
        if ( $handle eq $myhandle ) { next; } # 自分の発言は排除
        print S "こんにちわ、",$handle,"さん\n";
        next;
    }

    # ログアウトに対する反応
    if (/^\(\[(.*)\@.*logged out/) {
        $handle = $1;
        if ( $handle eq $myhandle ) { next; } # 自分の発言は排除
        print S "さようなら、",$handle,"さん\n";
        next;
    }

    # 電報に対する反応
    if (/^\#\< Message from.*\[(.*)\]/) {
        $handle = $1;
        print S $1,"さんが電報を送ってくれました\n"; # 電報の送り手を公開
        next;
    }

    if (/^\#\<\ (.*)/) {
        @denpo = split(/\s/,$1);
        $command = shift(@denpo);

        if ($command eq "date") { &shell("date"); next;}
        if ($command eq "uptime") { &shell("uptime"); next;}
        if ($command eq "quit") 
               { print S "終了します\n/q\n"; last;}
        if ($command eq "kill")
               { print S "強制終了します\n"; exit;}
    }
}

# 外部からコマンドを実行させるサブルーチン

sub shell { 
    open(EXEC, "@_ |");
    while() { 
        select(S); $|=1;
        print $_;
    }
}

プログラムをitalk上に常駐させるには、バックグラウンドで実行します。 (ただし、無能の動作が安心できるようになってからにして下さい。) 終了するには、killコマンドなどで直接終了させるか、 電報でquitと送ってください。 (フォアグラウンドで実行している場合にはC-cで中断できます。) 電報で、dateとやると、無能の実行されているホストから dateコマンドを実行しその結果を表示します。 同様にuptimeも行うことができます。 ここでは、この2つのコマンドにとどめましたが、 たとえば、電報の中身を直接実行させるようなことを許しますと、 セキュリティー上深刻な問題を引き起こしかねないので気を付けて下さい。

おわりに

ここまでの説明で、どうやったら無能が作れるのかわかってもらえれば幸いです。 多分、説明不足の点や間違ったところがあると思いますので、 疑問な点は私かこんの君に個人的に聞いてください。 特にこんの君はいろんな無能を作っているのでいろいろ参考になると思います。 今回使用したソースは、
moemoe4c:\home\masataka\muno\ と、
http://www.komaba.ecc.u-tokyo.ac.jp/~g610578/P/
に置いておきますので、自由に使ってかまいません。

お願い

無能の実験は、絶対に main.italk.ne.jp ではやらないで下さい。 プログラムのミスにより思わぬ事態が発生し、 多大な迷惑をかける恐れがあります。 そうでなくても、italkerの公共の場で個人的な実験をするのは如何かと思います。 (ごめんなさい。一番迷惑をかけていたのは私です。m(_ _)m) 代わりに無能実験用に backup.italk.ne.jp が用意されています。 mainに無能を上げる場合は十分に安全性を試してからにして下さい。 (mainにおける無能使用の正式な許可については、 italkで確認して下さい。)

謝辞

編集長の壱君、原稿の締切を一日延ばしてくれてありがとうございました。 こんの君、ログとソースの提供ありがとうございました。 お二人には原稿の中身についてもいろいろとアドバイスを頂き、とても感謝して います。迷惑かけっぱなしで本当すみませんでした。m(_ _)m

参考文献・参考ホームページ


1) もう少し厳密に言うと、人工無能は「ユーザーとしてitalkサーバーにログインし、発言などの動作を行うプログラムの総称」です。したがって、必ずしも上の例のようにつっこみを入れるだけが人工無能ではありません。無能はitalkの補助機能としても使われてきました。伝言君なんかはそのなごりです。
2) 編註 第211号のわたるさんの記事参照。CでもPerlでもやることは全く同じです。ただPerlの場合は,構造体の代わりにリストを使うようになっています(って4日前に生まれてはじめてPerlを触った編集長)。
3) 編註 入門書類を見ればすぐに理解できることですが,Perlでは,文字列の演算子が数値の演算子と区別されています。たとえば,文字列の比較では == は使わず,この例のように eq を使います。
4) 編註 305に置いてあるマシンの名前です。

今野 俊一 (こんの, knn) <toknn@ijk.com>, <knn@ebony.plala.or.jp>
東京大学 工学部 計数工学科(内定), TSG(理論科学グループ)