Amebaなうと例のブログ記事に釣られてる人大杉

オリジナルを創りだす
サイバーエージェント、ミニブログ「Amebaなう」開始へ

いやあの、「Twitterクローンを作る」ことができる会社は日本に数千社とかあると思うし僕にもできるけど、「芸能人/著名人を600人集めてTwitterクローン上でつぶやかせる」ことができる会社ってサイバーエージェントくらいしかないんじゃないかな?少なくとも僕にはできないし、きっとはてなとかにも無理だよね。

※僕の勤めてる会社はサイバーエージェントのグループ会社なんだけど、だから擁護してるわけではもちろんないです。むしろサイバーエージェントのサービスって僕みたいな人間はターゲット範囲外なものばかりだからそんなに好きじゃないくらいの勢いです。でも釣られてる人はちょっとビジネスセンスがないんじゃないかな。僕はむしろ社長のブログ記事さえ狙って書いたんじゃないかくらい思うんだけど。

SICPの図形言語を JavaScript+html5のcanvasで解く

SICPの2章に図形言語というセクションがあって、環境を用意するのが面倒なのでスルーしていた(通常coLinux+gaucheとかDrSchemeとかでやってるのだけど、描画のためにLinux側にgauche+GL、Windows側にXサーバを入れて窓をXで飛ばすとか面倒だし、これ以外の目的に使いそうにない)。
しかし、canvasを使って本文中のSchemeプログラムをJavaScriptで記述してやればできるんじゃないか?と思ってやってみた。
なお土日は本当は月曜SICPのためにambインタプリタを書かなければいけなかったのだが、図形言語にかまけていて予習をやってない。でも単に遊んでいたわけではないんだよというアリバイ工作のためにこのような記事を書いてみる。

SICPではSchemeを使うとはいっても、マクロとかは取り扱っていない。SICPの範囲内で使われるScheme処理系の機能で、JavaScript処理系が持ってないのはquoteくらいのものである。図形言語のセクションではquoteは導入されていないので、ほとんど一字一句そのままJavaScript写像可能である。ただ、JavaScriptとしての記述の簡潔さのために、簡易なデータ構造の構築子/選択子の組はJavaScriptのオブジェクトを使うことにした。たとえば次のように。

//ベクトルの構築子&演算
//構築子は make_XXX のようなプリフィックスをやめる。
function vect(x, y){ return {x:x, y:y} }
//選択子は 単にオブジェクトプロパティのアクセスとする
function add_vect(a, b){ return vect(a.x+b.x, a.y+b.y) }
function sub_vect(a, b){ return vect(a.x-b.x, a.y-b.y) }
function scale_vect(s, v){ return vect(s*v.x, s*v.y) }

あとは、適切な箇所にreturnを自分でいれてやらなければならない程度で、ほとんどSICPソースそのまま移植可能だ。こんな具合に。

//フレーム内へベクタを写像
function frame_coord_map(f){
    return function(v){
        return add_vect(
                f.origin,
                add_vect(
                    scale_vect(v.x, f.edge1),
                    scale_vect(v.y, f.edge2)))
    }
}
//線分を描画するdraw_line手続きが所与であるとして、線分のリストを描画するペインタを返す
function segment2painter(ls){
    return function(f){
        var map = frame_coord_map(f);
        for( var i=0, len=ls.length; i<len; i++ ){
            draw_line( map(ls[i].start), map(ls[i].end) );
        }
    }
}

canvasAPIに依存するのは、絶対座標により線分を描画するとか、(アフィン変換+平行移動した上で)画像を描画するとかのプリミティブな処理くらいだ。

var ctx = document.getElementById("canvas").getContext("2d");
function draw_line(start, end){
    ctx.beginPath();
    ctx.moveTo(start.x, start.y);
    ctx.lineTo(end.x, end.y);
    ctx.stroke();
}
function draw_image(id){
    var img = document.getElementById(id);
    return function(f){
        var w = img.width;
        var h = img.height;
        ctx.setTransform(
                f.edge1.x/w, f.edge1.y/h,
                f.edge2.x/w, f.edge2.y/h,
                f.origin.x,  f.origin.y);
        ctx.drawImage(img, 0, 0);
        ctx.setTransform(1, 0, 0, 1, 0, 0);
    }
}

こんな感じ。あとは実際に動いているところでご確認を(IEcanvasを使えるようにするライブラリは入れてないので、モダンなブラウザでどうぞ)。
この図形言語抽象はなかなか面白いなと思った。もうちょい凝ったこともできる。それはまた今度。

結局ルンバ買いました。

使用感はすばらしいです。けなげに掃除する姿が愛らしい。奮発したかいがあった。静音とかいてあったのでなんか無音に近いくらいかと思ってたけど、音は普通に掃除機かけるのよりやや静かっていうくらい。
ゲロ問題は、今のところ運用でカバー。朝起きたらとりあえず布団をたたんで、(ゲロがないことを確認してから)ルンバを起動、シャワーを浴びたりしてるうちに出かける頃にはお掃除完了とかそんな風にしてます。
というか独身なので、万年床というか、布団なんか敷きっぱなしだったのですが、ルンバが動けるように布団をたたむようになりました。ルンバが動けるように、床に極力ものを置かないようにもなりました。この辺が実はルンバのすごいところ(ルンバが動けるように人間が動いてしまう)じゃないかなと思います。

ただ、ひとつ気になるのは、付属のリモコンが本体とペアリングできないところ。マニュアルを見てリモコンのリセットを試みたけど無理。ネットで調べたら同じ症状の人もいたけど解決方法は不明。ちょいと気になるなあ。

掃除の精度は、感覚値では人間が掃除機かける場合の8割くらいな感じ。猫の毛のような軽いものは、たまにくるくる回るルンバの排気で飛ばされちゃうこともありますがそこはご愛嬌。そのかわり全自動だから毎日掃除機かけても面倒じゃないし。

ルンバ、洗濯乾燥機、食器洗い機の三種の神器をそろえると、家事ってかなり楽になるよねえ。うちは洗濯乾燥機はスペースの問題で設置できず、食器洗い機も台所のレイアウトの問題で設置できなかったけど、次に引っ越すときは1.洗濯乾燥機が置ける 2.食器洗い機を設置できる を念頭において物件を探そうと思いました。21世紀ってすばらしい。

お掃除ロボットルンバが欲しい。

題名の通りなのだけど、うちは猫を飼っていて、猫はドライフードを食べ過ぎた状態で水を飲みすぎたりするとよくゲロを吐くので、ゲロを吸い込んで壊れると切ない。
かといって、家にいるときだけ起動するというのも若干本末転倒気味。どうせならスケジュールして家にいない間に自動起動させたい。
ルンバ+猫を実運用している方がいたら、ゲロ障害発生時の挙動や運用上の対処方などを教えてもらいたい今日この頃。

二重エンコードの話についての補足

AJITOで酒を飲みながらid:nTeTsと昨日書いた記事についてしゃべっていて、id:nTeTsがこの問題をPerl文字列の内部表現やUTF8フラグに関わる問題と認識している節があった。それは単に間違っていて、この問題はPerl固有ではないしPerl文字列の内部表現などは一切関係ないのだが、まあ混乱しても無理はないとも思うのでその辺について補足してみたい。なお僕はPerl5.8からPerlを使い始めたので、本当の歴史的な経緯などは知らない。現状の仕様からリバースエンジニアリングして歴史的経緯を推測したにすぎないので、誤りが含まれる可能性は指摘しておく。

Encode::encodeとEncode::decodeのシグネチャを仮想的に型付きで表現するとしたら、理想的には次のようになっているべきである。

//decodeはバイナリ(:byte[])から内部表現(:String)への写像
String decode(Encoding enc, byte[] src);
//encodeは内部表現(:String)からバイナリ(:byte[])への写像
byte[] encode(Encoding enc, String src);

だからこそ、「入り口でdecodeし(byteからStringを得て)、プログラムではString(内部表現文字列)を使い、出口でencodeせよ(byteを出力せよ)」となるわけだ。
もし型を厳しくチェックする言語であれば、二重エンコードは単にコンパイラシグネチャの不一致ではねられるので問題は顕在化しにくかっただろう。

String text = "unk";
//コンパイルエラー! Stringを期待しているのにbyte[]が渡された
byte[] fuck = encode(UTF8, encode(UTF8, text));

実際のPerlのEncode::encodeはStringだけではなくbyte[]も受け取り、二重エンコードを容易に生み出す。これは設計ミスではなく後方互換性のための仕様である

  • Perl5.6まではbyte[]のようなものとして文字列が表現されていた
  • その仕様に基づいて書かれた膨大なCPANモジュールが存在する
  • しかしPerl5.8でUNICODEをネイティブにサポートする
  • 過去の資産はすべてそのまま動くこと

というのがEncodeに課せられた使命だったと思われる。そのためにlatin-1とマルチバイトバイナリの文字列結合に際しての奇妙な仕様とか、Encode::encodeがflagged UTF8文字列以外は単に受け付けないという仕様ではない、といったデザインがなされたのだろう。

これは難しい問題なので、単純な解決方法はない。コストの支払いの一部は、Perl5.8以降に新たに多言語サポートするプログラムを書くプログラマに委ねられた。しかしこれはやむを得ないことである。そうではなくて過去の資産を書いた者にその支払いを求めていたら、あるいは過去の資産と断絶した新しい世界を新たに作っていたら、おそらく誰もついてこなかったと思う。(ANSI Cにはwchar_tというマルチバイト文字を取り扱う型が存在するが、Windows界隈以外でコレを使ってるのを滅多に見ない。)

で、Perlでは歴史的な経緯により二重エンコードが発生しやすいのではあるが、問題はPerl固有ではない。外部から入力されたデータが二重エンコードされていたら、結局他の方法でマルチバイト文字列を処理するプログラム言語であってもなんらかの対応をしなければならないのだから。

Perlで日本語文字列が文字化けしてるかどうか推測する&修復する

ちょっと最近Buzzurlに自作スクリプトか何かで、大量の二重エンコード文字列を含むブックマークが投稿されたので対策のために調べてみたことのまとめ。<追記>id:miyagawaさんのブクマで Encode::DoubleEncodedUTF8 というモジュールを教えてもらいました。調べたら作者もid:miyagawaさん。二重エンコード是正にはこちらを使うようにしましょう。
でもこれ"二重エンコード perl utf8"とかでぐぐったけど見つからなかった…。id:miyagawaさんのブログとかもっと検索に引っかかるべきだと思うのだが。

PerlでUTF8文字列を使うときの原則

PerlでUTF8文字列を扱うならば、Encodeの神であるところのid:dankogaiが何度も何度も口をすっぱくして言っている次の原則に従わなければならない。そうしないとすごく不愉快な目にあう。


入り口で decode して、内部ではすべて flagged utf8 で扱い、出口で encode する。これがすべてです!とにかくこの基本方針をまもっていれば幸せになれます。

しかしそれでも文字化けを見る場合がある。CPANモジュールなどで、「入り口でdecode/出口でencode」原則に従っているのだろうと期待してencode済みのバイナリ列を渡してみたら文字化けて、なぜかと調べていたらモジュールは引数としてdecodeされたflagged utf8文字列を期待していて文字化けるとか。まあこれは普通にテストしていれば検出できるのであまり問題にならない。

あと昔は、flagged UTF8文字列に対してさらにdecode_utf8することにより文字化けが発生したような気がするが(試そうと思ったが今手元に環境がない)、2.13以降の新しいEncodeではflagged UTF8文字列にdecode_utf8しても何もしないようになった。

となると、現実に見る文字化けパターンは以下の2つであろう。

  1. flagged UTF8文字列とバイナリの文字列結合
  2. 二重エンコード

二重エンコード

文字列結合はいいとして、二重エンコードとは何か。これは、UTF8エンコーディングされているバイナリをさらにUTF8エンコーディングしたときに起きる不具合だ。
どういうことか。例えば"ECナビ"というキャラクタ列は、UTF8では[(0x45) (0x43) (0xE3 0x83 0x8A) (0xE3 0x83 0x93)]と表現される(※()は区切りのための表示で、もちろんバイナリ表現には存在しない)。何かの都合で(例えばEncode::encodeに対する理解不足とか)、このようなUTF8のバイナリ表現に対してさらにUTF8エンコーディングをしてしまうことがありうる。
するとどうなるか。UTF8というエンコーディングは論理的な意味はともかく物理的には21bit(または31bit)までの任意のビット列をエンコーディング可能なので、各バイトを7または8bitのビット列としてUTF8エンコードされることになる。UTF8では7bitのビット列は1バイトで表現され、0x00〜0x7Fの間であり、8〜11bitのビット列は2バイトで表現され、 0xC080 〜 0xDFBF の間である。"ECナビ"のUTF8表現 [0x45 0x43 0xE3 0x83 0x8A 0xE3 0x83 0x93] をこのように二重UTF8エンコードすると次のようになる:[(0x45) (0x43) (0xc3 0xa3) (0xc2 0x83) (0xc2 0x8a) (0xc3 0xa3) (0xc2 0x83) (0xc2 0x93)]

use strict;
use warnings;
use utf8;
use Encode;

my $utf8str = "ECナビ";
my $utf8bin = encode_utf8($utf8str);
my $fuckbin = encode_utf8($utf8bin);
print $utf8bin, "\n";
print $fuckbin, "\n";

自分が注意深ければこのような腐ったUTF8バイナリを作り出さなくて済むが、問題となるのが外部からこのような腐ったUTF8バイナリを流し込まれたときである。無意味な表現を含むかもしれないが、少なくとも物理的には"正しいUTF8フォーマット"なので、decode_utf8はあなたを救ってはくれない。

二重エンコードを検出

このような腐ったUTF8バイナリを検出できるだろうか? このようなUTF8はすべて1〜2バイト表現で構成されるが、幸いなことにほとんどの日本語キャラクタはUTF8では3バイトで表現されるため、主に日本語キャラクタだけを使っている場合は、完全とはいえないが二重エンコードっぽい文字列かどうかを判定することができる。

sub only2bytes {
    my $ascii = my $to07b = "[\x{00}-\x{7f}]";
    my $chr2b = my $to11b = "[\x{c0}-\x{df}][\x{80}-\x{bf}]";
    my $re = qr/^($ascii|$chr2b)+$/o;

    #Encode2.13以降ではdecode_utf8()は二重デコードの心配はないので、
    #安全にencodeするためにdecode_utf8()してからencode_utf8()
    my $bin = encode_utf8(decode_utf8(shift));
    $bin =~ /$re/
}

my $utf8bin = encode_utf8("ECナビ");
my $fuckbin = encode_utf8($utf8bin);
warn( (only2bytes($utf8bin)) ? "fuck" : "valid" );
warn( (only2bytes($fuckbin)) ? "fuck" : "valid" );

ウラジミールプーチン検出関数

さて、「ほとんどの」といったが、2バイト表現のみからなるような日本語文字列というのは存在しないのだろうか?(ユニコードにおいて"日本語"というのは微妙な問題ではある。しかしWebアプリなどを作るのであれば例えば「今はEUC-JPやCP932と相互変換できる範囲内についてテストする。アラビア文字サポートについてはイラン市場にサービス展開するときに予算を取る」などという現実路線は考えられる。)
結論からいくと、ある。完全なものかどうか分からないが、ググって出てきたコード対応表からgrepしたら、例えば"Владимир Путин"(ウラジミールプーチン)などはUTF8にすると2バイト表現のみからなる。一部の記号と、ギリシャ文字キリル文字などが該当する。
さすがにこういう文字列をサポートしないとなるとKGBに消されるεπιστημη(えぴすてーめー)氏に失礼なので、こういう場合は救うようにしてみよう。

sub is_putin {
    my $ascii = my $to07b = "[\x{00}-\x{7f}]";
    my $jpn2b = "[´ ¨ ± × ÷ ° ¢ £ § ¬ ¶Α-Ωα-ωА-Яа-я]";
    my $re = qr/^($ascii|$jpn2b)+$/o;

    my $str = decode_utf8(shift);
    $str =~ /$re/
}

my $putin = "Владимир Путин";
warn( (only2bytes($putin)) ? "fuck" : "valid" );
warn( (is_putin($putin)) ? "ウラーー!!" : "fuck" );

あとは組み合わせるだけ

日本語のみのサポートとはいえ、検出さえできれば、修復は簡単である。二重エンコードされているのだから、もう一度decodeしてやればよい。(decode_utf8はflagged UTF8に対して何もしないので、二度目のデコードはdecodeを呼ぶ必要がある点に注意)

sub smart_decode_utf8 {
    my $utf8bin = shift;
    my $tmp = decode_utf8($utf8bin);
    (is_dual_encode($tmp)) ? decode("utf8", $tmp) : $tmp;
}
sub is_dual_encode {
    my $text = shift;
    only2bytes($text) && !is_putin($text);
}

my $str = "ECナビ";
print smart_decode_utf8($str), "\n";
my $fuck = encode_utf8(encode_utf8($str));
print smart_decode_utf8($fuck), "\n";
my $name = "Владимир Путин";
print smart_decode_utf8($name), "\n";

以上です。間違っているところがあったらきっとid:dankogaiが何とかしてくれる。

二重エンコードに関する関連情報