The TV program time table #3

1 クール(12〜13話)分の一挙放送、といった番組は番組表においても縦方向に非常に長い面積を占める。その枠の先頭部分にタイトルとサムネイルを描画するわけなのだが残りの部分は全くの空白なので、始まって数時間もすると何を放送しているのか分からないただの枠が番組表に残るだけになる。これを何とかしたい。

これを何とかするのは、近年よく広告に使われる sticky positioning だ。sticky というのは日本語で言えばネバネバだ。ページをスクロールした時にできるだけビュー内にとどまろうと粘っこい動作をするアレである。現実には広告によく使われるが、本来 sticky な動作をして嬉しいのは上下に長い表のヘッダ部分などである。あべアニで言えばそれは正にチャンネル名の部分で、既にそういう動作をするように仕掛けてある。同じことを、それぞれの番組枠にも適用すればいい。

ところでこの sticky positioning というのは CSS3 の規格でまだ策定中の段階であり、現時点では Firefox を除くブラウザにはまだ実装されていない。なので javascript による Polyfill を使うことになる。そういうわけであべアニでは StickyFill というライブラリを使っている。ところが残念なことに、このライブラリに対してそれぞれの番組枠を sticky にするように指示しても上手くいかないのであった。上手くいかない上にスクロールもかなり重くなる。

Polyfill でやるべきことは

  • window の scroll イベントをリスンして
  • sticky 動作の対象となっている要素を走査し、その領域内にビューの上端が含まれていたら要素を position:fixed にする。このとき、その親要素の領域内に収まることを優先しつつ、要素の内容がビューに収まるように位置を微調整する。それから、領域の周辺の要素の位置を動かさないためにダミーの置換要素を生成したりも必要ならする

ということにつきる。しかしやることは単純だが、重い。まず scroll イベントは相当な頻度で発生する。それから要素を走査する際にその配置情報を逐一取得するのも重い。StickyFill は汎用的に使えるようにするためにその辺を素直にやっているので重い。

そういうわけで仕方ないので番組枠の sticky 動作についてはページに最適化しつつ自前で書いた。たとえばいったん計算したら使いまわせるもの(margin や border のサイズ)は積極的にキャッシュしたり、そもそも sticky 動作させる必要のない番組枠(内容の高さが枠の高さ以上あるもの)は積極的に除外したりだ。それでもブラウザがスムーズスクロールする設定にされていると若干描画がバタつく。早くブラウザでネイティブサポートされて欲しい。

ついでに言うとネイティブサポートの際は stuck 状態かどうかを取れる CSS の擬似クラスを定義してくれるととても助かるのだが。そうすると stuck したときに限り box-shadow をかけるなどの小細工ができる。

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 つめなのだが、どちらも純粋にツイートするだけの機能しかない。巷の高機能なボットはフォロー返しや、メンションに対するある程度の反応を行うものもある。これらのボットにもそういう機能が必要だろうか? 要調査。

server status

先週ふたばのみならず、多くのサイトが DDoS 攻撃に晒されサービスを正常に提供できなくなる事件が起きた – 経過。その攻撃自体は今は沈静化している。また、ふたば側ではユーザーとサーバとの間に CloudFlare を挟むようになり、DDoS 耐性が増強された。

で。ふたばに関してはサーバの状態を観測するには公式の接続テストページか、拙作のふたば鯖☆偽監視所がある。今回はふたばの全サーバが攻撃されたので、公式の接続テストページは本来の役目を果たさない。従ってサーバの状態を確認したいユーザのアクセスが偽監視所に集中し、今まではだいたい日に 100 件とかそこらのアクセスだったのが、400000 アクセスとかに増えるようになった日が数日続いた。

偽監視所にアクセスして返される内容は gzip された静的な html であり、内容に含まれるグラフっぽい何かはすべて CSS で表現される軽量なテキストデータなので、サイズは高々 10 KB 程度だ。それでも、400000 アクセスされると一日の転送量は 3GB くらいになる。3GB というのは xrea の一日の転送量リミットの上限の数値だ(ただ、これが絶対の上限だと明言されているわけではない。これを超えると何がどうなると明言されてるわけでもない。このゆるゆるでテキトーでいい加減な管理ポリシーが今となっては xrea の最大の魅力だ)。従っていますぐ xrea を追い出されるということではないのだが、もしも転送量が倍々ゲームになるようだといろいろと対策を考えないといけない。

というわけでいろいろ改修を施した:

  • PHP のバージョンが上がることによって発生していた warning を潰した。これらはクラスのインスタンスを保持するのに [cci]=&[/cci] を使っていたとか、クラスのメソッドを static に呼び出していたという、PHP4 からの移行期の遺物だった。なにしろ偽監視所を作ったのは 2010 年のことでその後ずっとほったらかしのまま稼働させていたので仕方がない
  • 従来の仕様では、同じ IP アドレスを持つ複数の仮想名を持つサーバに関しては最初の 1 つだけ HEAD を飛ばし、それを残りのサーバにも適用していた。しかし今回 CloudFlare を挟むようになったことですべてのサーバが見かけ上同一の IP アドレスを共有するようになったので、HEAD は律儀に個別に飛ばすようにした
  • 従来の仕様では、サーバ名から IP アドレスを正引きのは単に [cci]gethostbyname()[/cci] していた。今回やはり CloudFlare を挟むようになったことで、複数の CloudFlare 側のアドレスが割り当てられるようになり、単に [cci]gethostbyname()[/cci] を呼ぶだけではラウンドロビンにより呼び出しの度異なるアドレスが返されるようになった。そこで [cci]gethostbynamel()[/cci] を使うようにし、ソートした上で先頭の IP アドレスを使うようにした
  • ユーザに返す html は 3 分ごとにリロードするようにしていたが、5 分 15 秒ごとにした

もうひとつ重要な心残りがある。偽監視所のグラフはいわば時系列を示している。しかし一般的な時系列グラフが、右端が最新を示すのに対して偽監視所は全くその逆で左端が最新なのである。6 年前の自分がなぜそういう仕様にしたのかこれっぽっちも全く覚えちゃいないのだが、今回見なおしたらやたら気になった。そこで虹裏に適当なスレが立った時に「」に聞いてみたところ、慣れたのでこれでいいという言質がとれた。これはつまり、これ以降「このグラフの仕様変じゃね?」という意見が出たとしても「」の総意ですからということでおあしすできるということだ…!

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
なにこの…なに?

ramen

ひところステマが話題になったが、この記事もまた徹頭徹尾ステマである。

NTT のフレッツ光を契約していた頃の話なのだが、この契約は月ごとにいくばくかのなんちゃらポイントが与えられるシステムになっていた。貯まったポイントに応じていろいろな景品と交換できるというまあありがちなものだ。そして、景品の中で上等なもの(例えば iPad とか)は、「まあ10万年くらいフレッツ光に契約し続ければそれくらいのポイントが貯まるかもね…」というような無茶な設定になっている点も非常にありがちであった。

もちろんそんなに待ってられないので、解約する直前で使っちゃったのだが:

ramen-2

ramen-1

使ったというのはつまりこのラーメンだ。これがとても美味しかった。1 杯分の単価で考えるとそのへんで売ってる袋麺の 2 倍くらいになると思うが、10 倍美味しいので全く問題ない。

このセットは web から注文できる:

喜多方ラーメン游泉三浦屋 醤油・味噌7食入り
http://shop.fm-kitakata.co.jp/shop/shopdetail.html?brandcode=000000000849

Ubuntu 16.04

Ubuntu 16.04.1 LTS (Xenial Xerus) がリリースされうちのマシン(Xubuntu)にもアップグレード通知が来たのだが、困ったことがある。

現状 AMD 製 GPU の Linux 向けドライバには 3つの選択肢があり:

  • Radeon ドライバ: OSS で、あんまりハードウェアの性能は引き出せない。多分安定はしている
  • fglrx ドライバ: AMD 製のプロプラ。まあまあ性能を引き出してるのだと思うけど(あと設定が Catalyst による GUI で行えるので試行錯誤しやすい)、あんまり安定してない
  • amdgpu ドライバ: 最新の Linux カーネルの管理下にある OSS ドライバだが、AMD からの技術公開を受けて安定さと性能を両立している(という触れ込み)だが超ナウい GPU しか今のところサポートしていない

うちのマシンは GPU が AMD Radon HD 6320 というやつなのだけど、困ったことに AMD は 16.04 向けの fglrx を作らない宣言を出しているのであった。なので、今 16.04 にアップグレードすると Radeon ドライバか amdgpu ドライバを選択せざるを得ないのだけど、amdgpu は今のところ HD 6320 をサポートしていないので対象外(最終的にはもっと古い GPU もサポートすると言っているのだがいつかは不明)。そういうわけで Radeon ドライバが最終候補になるのだが。前述のとおりこれは OSS 原理主義者のためのものであって性能はいまいちなのである。

というわけで、16.04 にアップグレードしたくてもできない。はやく amdgpu が進歩してほしい。

The TV program time table #2

CloudFlare 絡みでもうひとつ。オリジンサーバから送出される静的なコンテンツについて、CloudFlare は CDN として振る舞う。つまり CloudFlare 側でキャッシュし、クライアントのアクセスの際はそのキャッシュを利用すると同時にオリジンサーバの負荷を低減する。

この静的の定義とはなんぞやというと、このへんに詳しい。特定の拡張子を持つもの、Cache-Control ヘッダで public と指定されかつ max-age が 0 より大きいもの、PageRule で特別に指定されたもの…という感じ。意外なことに html ファイル自体はキャッシュされない(PageRule の設定でキャッシュさせるようにすることはできる)。

で。あべアニのうち最もファイルサイズがでかいのは、番組表のデータである json ファイルなのであるが(gzip して 200KB 弱くらい)、アクセス解析を見てみたらなんとこれが CloudFlare 側にキャッシュされていないのであった。これはいけません。

とりあえず適切に Cache-Control ヘッダを付加するようにしてみたところ、効果なし。そこで mod_rewrite で擬似的な js ファイルとして扱うようにして、かつクエリ文字列を使わないようにしてみたところ、キャッシュに乗るようになった。間抜けなことにそれらを同時に変更したので、乗るようになった要因が拡張子なのかクエリ文字列なのかはっきりしていないのだが、まあ結果オーライなのだ。

ちなみに PageRule で何とかする方法もあるのだが、フリープランだと何と登録できる PageRule は 3 つまでなのである。ダイヤモンドより貴重なのでホイホイ使うわけにはいかない。

CloudFlare のキャッシュに乗っているかどうかは、レスポンスヘッダに [cci]cf-cache-status: HIT[/cci] が含まれているかどうかで判断できる。また、CloudFlare に限らずコンテンツが CDN によって適切に分散されているかどうかは https://www.webpagetest.org/ でテストすることができる。

The TV program time table

Abema.tv というサービスがある。これはつまりインターネット上の無料 TV サービスである。様々なチャンネルを持っているのだが、その中に 5 つくらいアニメ専門チャンネルがあり、そのせいで(あるいはそのおかげで)虹裏 img はここのところ半分 Abema 実況板と化している状態だ。

というわけで見てみたところ、Abema のサービス自体は非常によく出来ているのだが、ただひとつ番組表が使いにくいと感じた。全体的に重いのと、1 ページに 5 チャンネル分の番組表が表示されるのだがこれが単純にチャンネル番号順で、カテゴリ分けされるわけではない。そのため例えばアニメだけで一覧、ということができない。また、ページを非アクティブ状態からアクティブにした瞬間、どうも現在時刻に合わせた番組枠を表示しようとスクロール位置を変えるっぽいのだが、それがバグってるっぽい上になかなか直らない。……などなど、いろいろと不満があるのである。

そういうわけで、代替物を作った:

あべ☆アニ
https://appsweets.net/abeani/

これを作る過程での技術的なポイントはなくもないのだが、本質的にはただの html と javascript だけのシンプルなページであり、特に書くほどのものではない。

しかしこの番組表自体には直接関係ない分野から強いて 1 つ書いておくとすると、CloudFlare のキャッシュとの連携が挙げられる。知ってのとおり CloudFlare は静的ファイルをキャッシュする。従って web ページを開発して頻繁にサーバにアップロードして動作確認をする際はそのキャッシュ機構が邪魔になる。そんなわけで CloudFlare のサイトにログインすると現れるダッシュボードには個別のファイルのキャッシュをパージする機能が用意されているのだが、もちろん手作業になるのでめんどくさい。

と、だれでもそう思うわけで、API が用意されている。で結局、rsync した結果を元に API を呼び出すスクリプトを書いて解決。

Talk about news #2

PhoneticNews は一種の RSS リーダーと考えることができるが(ただし NHK ニュースに関しては公式サイトが内部的に利用している、限りなく RSS に近い独自形式の XML を利用している)、任意のアドレスのフィードを利用できるわけではないので用途はかなり限定的だ。

なぜフィードを絞っているのかというと、発話の際のトラフィックの問題だ。PhoneticNews は発話データを得るのに VoiceTextAPI を利用している。この API のエンドポイントに文字列を POST すると、それを読み上げた発話の aac データが返ってくる。

この VoiceText へのネットワークアクセスは PhoneticNews をインストールした Chrome がそれぞれ個別に行うため、任意のフィードを登録できるようにすると VoiceText 側の負荷が結構なものになるかもしれないのである。まあ、勝手な想像なのですが。

API のリファレンスには呼び出しに関する回数や頻度の制限などは記述されていないので、どこまで許されるのか実際わからないのであった。うーんお問合せしたほうがいいのかなー。

いずれにしてもできうることを考えると、

  • 各 Chrome から個別に VoiceText にアクセスさせることをやめて、ここのサーバを経由させる。ここのサーバが代理的に VoiceText へのやり取りを行う: ここのサーバへの負荷はどうするのかという話になるが、現状でここのサーバは CloudFlare のお世話になっているのでたぶん負荷の面では大丈夫
  • 発話データを Chrome 自身の tts で生成する: ただし、日本語の発話データは OSX か、Windows10 以降じゃないと取得できない、らしい

あたりになると思う。はてさて。