Abeanibot introduced #2

前の記事で、

検索結果をローカルに保持してある sqlite のデータベースと照合する。新しいものがあればそれをツイートする

と書いた。ここのやり方は sqlite ならではのものになっているので一応メモしておく。

普通に考えると、番組 A が既にデータベースにあるかを照合し、その結果によって処理を振り分けるには

  • [cci]select * from program where name=:A[/cci] する
  • レコードセットが行を返さなかったら [cci]insert into program (name) values (:A)[/cci] する。そしてツイートする

と、1 番組につき 2 回クエリを発行しなければならない。これは無駄な感じがするので、例の sqlite 側の関数の実体を php で実装できる機能を使う。


$db = new SQLite3("program.db");
$db->createFunction("php_name", __NAMESPACE__ . "\\phpName");

$insertedNames = [];

function phpName ($name) {
global $insertedNames;
$insertedNames[$name] = 1;
return $name;
}

こんな感じにしといて、[cci]insert or ignore into program (name) values (php_name(:A))[/cci] とすると、番組 A がテーブルに存在していなかった時のみ php_name 関数が呼ばれ、そして行が追加される。php_name 関数自体は何も副作用をもたらさないのだがこの時 PHP 側に制御がいったん移るので、そこで適当なものに覚えておく。こうすると 1 番組につき 1 クエリで済むようになる。

ところで createFunction の第 2 引数には PHP 側の関数を指定する。PHP で関数オブジェクトを指し示す方法としては、関数名の文字列、[cci]array([$this object], [method name string])[/cci] のような配列、クラス名を前置した静的メソッド名文字列などが段階的に追加されてきた。加えて最近の PHP では function リテラルそのものも使えるようになっている(これは Closure クラスのインスタンスが実体だ)。

この段階的というのがミソで、sqlite3::createFunction は最も初歩的な関数名文字列しか対応していないっぽいのである(かろうじて、名前空間は認識するが)。このへんの「ううn…」さがまあ PHP らしさなのだが。

Abeanibot introduced

あべ☆アニ関連でちょっとした Twitter 上のボットを作った:

あべアニ ぼっと
https://twitter.com/Abeanibot

これは AbemaTV 上で新しく配信されるらしいアニメをつぶやく。例えばこんなふうに:

新しくというのは、そのままの意味だ。例えば、すでにのんのんびよりは何度も配信されているので、もしまた再度第 1 話から配信されることになったとしてもそれはツイートされない。一方もし将来まじぽかの配信が決定した場合はそれはツイートされる。

どのように対象の番組を抽出しているか。情報源としては番組表データと、検索結果のデータが考えられるが、今回あべアニぼっとでは検索結果を使用している。というのは番組表データは当日から 1 週間先のものまでしか得られないが、検索であれば更にそれを飛び越えた期間の番組データを得ることができるからだ。

そういうわけでバックエンドでは 1 時間ごとに PHP スクリプトを起動し、AbemaTV の検索 API に対して 2 つのクエリを投げる。なぜ 2 つかというとこの API には若干の癖があるようで、どうも検索結果が 100 件を超えないように調整されるような感じがするのである。AbemaTV のアニメ系チャンネルの場合、タイトルに含まれる話数は必ず [cci]#\d+(〜\d+)?[/cci] の形式になっているようなので、第 1 話から新しく配信されていそうな番組を検索するには [cci]#1[/cci] で検索する。しかしこれには #10 とかも含まれて、直近の番組で 100 件近くに達してしまう。なのでこのクエリだと 1 週間先、2 週間先の新番組を抽出できない可能性がある。

そこで次に、[cci]#1〜[/cci] でも検索する。これなら確実に第 1 話の番組だけを抽出できる。ただしこれの場合複数話連続している配信形態であることが前提になるので、第 1 話を単体で配信するパターンに対応できない。それをカバーするのは最初に投げたクエリである。

この 2 つの検索結果をマージすることで最終的な結果が得られる。ここまで読めば分かるように、この方法だと 1 週間先、2 週間先で、かつ第 1 話を単体で配信するパターンの番組は検索から漏れてしまう可能性があるのだが、まあ仕方がない。クエリに単語区切りを指定するような特別な演算子を含めることができれば解決するのだが、クエリがどのような文法になっているのか分からない。

いずれにしても得た検索結果をローカルに保持してある sqlite のデータベースと照合する。新しいものがあればそれをツイートするという流れだ。

ところで Twitter ボットを作ったのは自由帳ボットに続いて 2 つめなのだが、どちらも純粋にツイートするだけの機能しかない。巷の高機能なボットはフォロー返しや、メンションに対するある程度の反応を行うものもある。これらのボットにもそういう機能が必要だろうか? 要調査。

Enable mecab on xrea server

ぼちぼちあべ☆アニに検索機能を追加したい。この機能は:

  • 当日から1週間先までの範囲内で
  • タイトル、説明に含まれる任意の文字列を全文検索する

という仕様にしておこう。まずサーバ側で必要なものを考えるととりあえずは全文検索機能を備えたデータベースだ。以前タテログを作った時は MySQL を使った。しかし xrea サーバ上の MySQL というのは、当然だがルートユーザの権限が必要なチューニングは一切できないのがネックだ(実は裏技がなくもないのだが、それをここでは公開できない)。タテログの場合も全文検索用のメモリの割り当てをもうちょっと増やしたいのだが、いかんともしがたい。そういう訳で今回は SQLite を使ってみたい。

次に必要なのは、日本語を形態素解析するライブラリだ。タテログの場合は大掛かりな機構を使わず、2-gram で済ませたのだがやはりこのへんはきっちりやりたい。フリーの形態素解析器といえばなんといっても Mecab とか Chasen とかだ。これらを xrea 上で使えると嬉しい。さらに欲を言えば、これらを子プロセスとして呼び出すのではなく PHP エクステンションとして扱えるとなお良い。その場合のバインディングは php-mecab を使うことになる。

さて、実は xrea サーバにはすでに mecab はインストールされている。ただし、バージョンは 0.93 と若干古い。また、PHP バインディングは用意されていない。なので今回はこれは使わず、自前で mecab と php-mecab を xrea サーバ上でビルドすることにしよう。幸い xrea サーバには ssh でログインでき、また gcc 等々のビルド環境及び PHP エクステンションをビルドするための phpize ツールなどもあらかじめインストールされている。

Mecab と辞書のビルド

  • 作業用ディレクトリを予め掘っておく。今回は ~/devel (/virtual/akahuku/devel) とした
  • Mecab 公式からソースを落としてくる

  • $ tar zxf mecab-0.996.tar.gz
    $ cd mecab-0.996
    $ ./configure --prefix=$HOME --enable-utf8-only --with-charset=utf8
    $ make
    $ make check
    $ make install
  • 同様に辞書を落としてくる: とりあえず今回は IPA 辞書を落とした

  • $ tar zxf mecab-ipadic-2.7.0-20070801.tar.gz
    $ cd mecab-ipadic-2.7.0-20070801
    $ ./configure --prefix=$HOME --with-mecab-config=/virtual/akahuku/bin/mecab-config --with-charset=utf8
    $ make
    $ make install

php-mecab のビルド
PHP エクステンションをビルドするには、前述の phpize を使う。これ自体はちょっとしたシェルスクリプトで、php のインクルードファイル等々を走査しつつ configure スクリプトを生成する。ここで困ったことが 1 つある。xrea のサーバは複数のバージョンの PHP が同時にインストールされていて、コントロールパネルからどれを使用するか選択できるのだが、どれを選択しても /usr/local/include/php 以下の内容は最も古い php5.3 のもののままなのである。従って他のバージョンの PHP 向けに普通に make すると php 本体とエクステンションのバージョン不整合が起こり認識されない。ちなみに appsweets.net で使用している PHP は 5.5.35 だ。そこで:

  • PHP 公式から該当 PHP のソースを落として、展開して、configure *だけ* やっておく
  • php-mecab のソースディレクトリ(今回は /virtual/akahuku/devel/php-mecab/mecab/)で

    $ php55ize
    $ ./configure --prefix=$HOME --php-config=/usr/bin/php55-config --with-mecab=/virtual/akahuku/bin/mecab-config
  • 生成された Makefile を vi で開き、/usr/local/include/php を含んでいるマクロの該当部分をすべて自前の PHP のソースディレクトリ(今回は /virtual/akahuku/devel/php-5.5.35)に置き換える

  • $ make
    $ make test
  • make install はしない(というか権限の関係上できない)。modules ディレクトリに出力された mecab.so をそのまま使う

PHP への組み込み
xrea の場合、現在は PHP を各ユーザの ~/.fast-cgi-bin 以下の php.ini 及び起動スクリプトによって起動させている。php55.ini を vi で開き

extension="/virtual/akahuku/devel/php-mecab/mecab/modules/mecab.so"
mecab.default_dicdir="/virtual/akahuku/lib/mecab/dic/ipadic"

を追記。

あとは fcgi による php が再生成された時に phpinfo(); して mecab が組み込まれていることを確認。

* * *

という感じになる。これにより、

$mecab = new MeCab_Tagger("すもももももももものうち");
echo $mecab->parse($str);

というように PHP から直接 Mecab の機能を使えるようになる。ここで更にもうひとひねりある。PHP の SQLite には面白い機能がある。SQLite3#createFunction() で SQL のクエリ内で使用できる関数を PHP の関数で自由に定義できるのである。つまり、PHP 側で

function php_tokenize ($arg) {
return (new Mecab_Tagger(["-O" => "wakati"]))->parse($arg);
}
$db = new SQLite3("foo.sqlite");
$db->createFunction("tokenize", "php_tokenize");

などとすると、クエリで直接

insert into table (content, tokens) values ("すもももももももものうち", tokenize("すもももももももものうち"));

などと書ける。これは面白い。データベースが PHP と同じプロセスで動いているからできる芸当だ。文字列のマーシャリングとか問題ないのか若干心配がないこともないが…。

ところで createFunction()、createAggregate()、createCollation() を使う際には第一に気をつけるべきことがある。もしその PHP 側のコールバック関数内で例外が発生して PHP の実行が中断された場合、SQLite 側のトランザクションは正しくロールバックされず、ジャーナルファイルが作られたまま、そしてデータベースファイル自身はロックされたまんま(ロック元は、mod_php 環境であれば http サーバだ。fcgi 環境では知らない)になってしまう。つまりコールバック関数内では出来うる限りガチガチにエラーチェックする必要があるということだ。

などとさっきまでやっていたら、いつの間にかふたばが全滅していた。
serverstat
なにこの…なに?

PHP in cygwin

普段 Windows 上で作業しているといっても、実際は cygwin の端末の中でちくちくしているわけだ。

一方で、Windows 向けの PHP をインストールもしている。そうすると cygwin の中から PHP スクリプトを呼び出したくなる時もある。どうすればいいだろうか。

純 Windows 環境側の path 環境変数で、すでに php.exe にはパスを通してある。また cygwin は起動時にその path を継承するので、とりあえず cygwin 上で

$ php -f C:\path\to\scriptというのはできる。次に shebang を通して実行させたい。つまり #!/usr/bin/php と 1 行目に書いた php スクリプトを実行させたい。

うちの環境では、cygwin 上の php.exe の位置は [cci]/cygdrive/c/Program Files (x86)/php/current/php.exe[/cci] だ。そこでまず、コマンドプロンプトで > cd C:\cygwin\bin
> mklink php.exe "C:\Program Files (x86)\php\current\php.exe"
などとしてみた: 失敗。Windows では実行形式を指すシンボリックリンクを実行したとき、argv[0] が真の実行形式ファイルを差さないようだ。それはシンボリックファイルを指したままだ。だから、実行形式を指すシンボリックリンクを実行した場合、実行形式の存在するディレクトリを起点にして設定ファイルを読み込んだりする処理はことごとく失敗する。うちの環境の php の場合、php.exe の隣においてある php.ini が読み込まれない。

そういうわけで /usr/bin/php として

#!/bin/sh
exec "/cygdrive/c/Program Files (x86)/php/current/php.exe" "$@"

てな感じのシェルスクリプトを置くと動く。

また、php スクリプト側を

#!/usr/bin/env php


としてもいい。あんまりこの env のトリック好きじゃないけど、実質的にはいちばん楽かなー。