コマンドラインのFlash開発環境を整える

HTML5への流れが加速している昨今ですが、このところ仕事の中でFlashに触る機会が多くなってきました。

せっかくなので、忘れないうちにActionScriptによるFlash開発環境を整える手順をメモしておこうと思います。

前提条件

ポイントは以下の2つ。

さすがに動作確認はブラウザを使ってやりますが、その他の作業は基本的にSSH接続したターミナル上で行います。

サーバは CentOS 5.5 64bit版、クライアントは Windows 7 64bit版です。サーバ機にはGUI環境は入っていません。

Java実行環境の準備

後述するFlex SDKを動作させるために、Java実行環境(JRE)が必要になります。

上記ページから、今回は Linux x64 RPM をダウンロードし、以下のコマンドでインストールします。2010年11月20日現在の最新版は Version 6 Update 22 でした。

$ sudo sh jre-6u22-linux-x64-rpm.bin
...
$ which java
/usr/bin/java
$ java -version
java version "1.6.0_22"
Java(TM) SE Runtime Environment (build 1.6.0_22-b04)
Java HotSpot(TM) 64-Bit Server VM (build 17.1-b03, mixed mode)

コンパイラの準備

コンパイラにはAdobeが公開している Flex SDK を利用します。

Flex SDKには2種類あり、オープンソースプロダクトのみで構成された Open Source Flex SDK と、それにオープンソースでないものを含めた Free Adobe Flex SDK が用意されています。どちらも無料で使うことができます。

今回自分が想定している用途ではオープンソース版で特に不足はないので、ライセンスも馴染みのあるMPLですし、オープンソース版を使わせてもらうことにしました。

上記ページ中の Open Source Flex 4 SDK リンクからダウンロードできます。2010年11月20日現在の最新版は Flex 4.1 Update Release (4.1.0.16076) でした。

ZIPパッケージになっていますので、適当な場所に展開し、中のbinディレクトリにPATHを通しておきます。

$ unzip flex_sdk_4.1.0.16076_mpl.zip -d ~/flex
...
$ echo 'export PATH=$HOME/flex/bin:$PATH' >> ~/.bashrc
$ . ~/.bashrc
$ which mxmlc
~/flex/bin/mxmlc
$ mxmlc -version
Version 4.1.0 build 16076

サンプルプログラムの作成

早速なにか作ってみます。

package {
  import flash.display.Sprite;
  import flash.display.Bitmap;
  import flash.display.BitmapData;
  import flash.text.TextField;
  import flash.text.TextFieldAutoSize;
  import flash.text.TextFormat;
  import flash.events.Event;

  [SWF(width="640", height="480", frameRate="30")]

  public class HelloWorld extends Sprite {
    private var base:Sprite;

    public function HelloWorld():void {
      var txtFld:TextField = new TextField();
      txtFld.autoSize = TextFieldAutoSize.LEFT;
      txtFld.text = 'Hello World!';

      var fmt:TextFormat = new TextFormat();
      fmt.size = 32;
      txtFld.setTextFormat(fmt);

      var bmpData:BitmapData = new BitmapData(txtFld.width, txtFld.height);
      bmpData.draw(txtFld);

      var bmp:Bitmap = new Bitmap(bmpData);
      bmp.smoothing = true;
      bmp.x = -(bmp.width / 2);
      bmp.y = -(bmp.height / 2);

      base = new Sprite();
      base.x = 320;
      base.y = 240;
      base.addChild(bmp);
      addChild(base);

      addEventListener(Event.ENTER_FRAME, onEnterFrame);
    }

    private function onEnterFrame(event:Event):void {
      base.rotationY += 4;
    }
  }
}

上のコードをHelloWorld.asというファイル名で保存し、以下のコマンドでコンパイルします。

$ mxmlc HelloWorld.as

コンパイルが完了すれば、同じディレクトリにHelloWorld.swfというファイルが作成されています。あとはこれをブラウザで読み込むだけです。

せっかくサーバ上で開発しているのですから、コンテナとなるHTMLファイルも一緒に作り*1、そのままApache経由で参照できるようにしてしまうのが楽でしょう。

ちなみに、上記のサンプルは単に "Hello World!" が回転するだけのシンプルなFlashです。

コンパイルオプション

上記のコマンドでコンパイルすると警告メッセージが出ますが、これは -static-link-runtime-shared-libraries オプションを付けることで回避できます。

$ mxmlc -static-link-runtime-shared-libraries HelloWorld.as

コンパイルオプションは、上記のようにコマンドラインから指定する他に、設定ファイルに書いておくこともできます。

設定ファイルには、${FLEX_ROOT}/frameworks/flex-config.xml に配置されている共通設定ファイルと、コンパイル対象ファイルごとのローカル設定ファイルがあります。

ローカル設定ファイルは、コンパイル対象のASファイルと同じディレクトリに -config.xml サフィックスをつけて置いておきます。例えば先程の HelloWorld.as の場合、HelloWorld-config.xml というファイルを同じディレクトリに置いておくと、コンパイル時に特に指定をしなくても読み込んでくれます。

他にも、load-config オプションによって明示的に設定ファイルを指定することもできます。

設定ファイルのフォーマットは、一番単純な形だと以下のようになります。

<?xml version="1.0" encoding="UTF-8"?>
<flex-config>
  <static-link-runtime-shared-libraries>true</static-link-runtime-shared-libraries>
</flex-config>

オプション指定の優先度は「コマンドラインオプション > load-config で指定した設定ファイル > ローカル設定ファイル > flex-config.xml」となっています。

オプションの簡単なリストは以下のコマンドで確認できます。

$ mxmlc -help advanced list details

Makefile

頻繁にコンパイルを行う場合、make や ant などのビルドツールを使うことになるかと思います。自分は使い慣れた make を利用しています。

シンプルなswf生成用Makefileは以下のような形になります。

MXMLC = mxmlc
MXMLCFLAGS =

TARGETS = HelloWorld.swf

all: $(TARGETS)

clean:
        $(RM) $(TARGETS)

.SUFFIXES: .as .swf
.as.swf:
        $(MXMLC) $(MXMLCFLAGS) $<

traceログの出力

デバッグ版の Flash Player では trace() 関数を使ってファイルへのログ出力を行うことができます。

上記ページからデバッグ版プレイヤーをダウンロードしてインストールします。

スタンドアロン版もありますが、Firebugと組み合わせて使えると便利なので、自分は上記ページ中の "Windows Flash Player 10.1 Plugin content debugger (for Netscape-compatible browsers)" をインストールしてFirefoxで動かしています。*2

次に mm.cfg ファイルに以下の設定を加えます。ファイルが無ければ新規作成します。

ErrorReportingEnable=1
TraceOutputFileEnable=1

mm.cfg ファイルと trace() 関数からの出力が記録されるログファイルの場所は以下のページを参照してください。Windows 7 の場合は Windows Vista と同じ場所に配置されています。

trace() 関数によるログ出力を有効にするには、swfをdebugフラグ付きでコンパイルする必要があります。

設定ファイルで指定する場合は以下のようになります。

<?xml version="1.0" encoding="UTF-8"?>
<flex-config>
  <compiler>
    <debug>true</debug>
  </compiler>
  <static-link-runtime-shared-libraries>true</static-link-runtime-shared-libraries>
</flex-config>

JavaScriptコンソールへのログ出力

またはもっと簡単に console.log() を使ってFirebugGoogle ChromeJavaScriptコンソールにメッセージを出力することもできます。

ActionScriptからJavaScriptを呼び出すには ExternalInterface クラスを使います。以下のようなメソッドをどこかに定義しておくと便利かと思います。

  import flash.external.ExternalInterface;
  public function log(message:Object):void {
    ExternalInterface.call('console.log', message.toString());
  }

これで log() メソッドに渡した引数がFirebugGoogle ChromeJavaScriptコンソールに表示されます。

fdbによるリモートデバッグ

もっと本格的なデバッグにはActionScriptのデバッガである fdb が利用できます。fdb は Flex SDK に同梱されています。

開発環境と実行環境が同一ホストであるパターンがもっともシンプルなのですが、今回はfdbをサーバ上で、プレイヤーをクライアント上で動作させた状態でデバッグを行います。

まず、traceログ出力の際に行ったように、debugフラグ付きでswfをコンパイルし、クライアント上のデバッグ版プレイヤーで起動します。

次にサーバ上で以下のコマンドを実行し、fdbを接続待ち受け状態にします。

$ fdb
Adobe fdb (Flash Player Debugger) [ビルド 16076]
Copyright (c) 2004-2007 Adobe, Inc. All rights reserved.
(fdb) run
Player が接続するのを待っています

クライアント上のデバッグ版プレイヤーを右クリックし、メニュー中の「デバッガー」をクリックしてパネルを表示、接続先に「ほかのコンピューター」を選んでサーバのIPアドレスを入力します。

「接続する」を選択すると、デバッグ版プレイヤーが待ち受け状態のfdbに接続し、デバッグセッションが開始され、fdbによるデバッグが可能な状態になります。

なお、この際、サーバはクライアントに対して TCP/7935 のポートを開けておく必要があります。

$ fdb
Adobe fdb (Flash Player Debugger) [ビルド 16076]
Copyright (c) 2004-2007 Adobe, Inc. All rights reserved.
(fdb) run
Player が接続するのを待っています
Player が接続されました。セッションを開始しています。
ブレークポイントを設定して「continue」と入力し、セッションを再開してください。
[SWF] /sample/HelloWorld.swf - 2,287 バイト (解凍後)
(fdb)

処理の最初からデバッグを行ないたい場合、セッションを開始した状態でデバッグ版プレイヤーが動作しているページを再読み込みすればOKです。

fdbによるデバッグの詳細は以下のページが参考になります。

その他

あとはリファレンスやその他資料を参考にしながら開発を進めていきます。リファレンスには様々なパッケージが記載されていますが、Flex SDK で利用可能なのは flash, mx, spark の3つのパッケージだけですので注意してください。

他にFlashを開発していく上で詰まりやすそうなのは、クロスドメインポリシーまわりでしょうか。

Flashでクロスドメインの通信を行う場合、通信先のサーバにクロスドメインポリシーファイル "crossdomain.xml" が設置されており、かつ適切な設定がなされている必要があります。これは URLLoader/URLRequest のような単発リクエストの場合も、Socket/XMLSocket のような持続的接続のような場合も同様です。

Flash開発はまだまだ触り始めたばかりで慣れていませんが、ぼちぼち色々試していこうと思います。

*1:swfファイルを直接読み込んでもいいのですが、後述する ExternalInterface を利用するにはコンテナが必要になります。

*2:ただし結構頻繁にクラッシュする気がします。

いろんな画像をAA Quine化するよ!

この辺の流れを見て楽しそうだなぁと思ったので真似してみました。

ミクさんQuine

まずはこちらから。

※2010年9月20日追記:ミクさんを更新しました。Gistのコードも更新済みです。詳しい更新内容は記事末尾の追記欄にて。

                                                            eval$s=%w~          
                   a=->(b,c,       d,e,f){%`#{(c)?"re       quire'zlib';":'     
                  '}g=Marshal.load(#{                (c)?'Z lib::Inflate.infl   
                 ate(':''}'#{b}'.                      unpack('m')[0]#{(c)?')': 
                 ''});h=  'eval$s                     =%w'<<126<<($s*#{d}  );i='
                ';j=-          1;                    #{   e*f}.times{|k|i<<  (g[
               k]==                 1?                    h[j+=1]:32);i<<10i   f
              (k%                   #{e}                   ==#{e-1})};i[-7,6]   
             ='       '      <      <126<<                 '.join';puts(i)#`};  
           $*[     0]?       (      require'                RMagick';include(Ma 
          gi      ck               );l=$*[0];m              =($*[1]||80).to_i;n=
         Qu                 a      ntu  mRange*(             $*[2]||0.5).to_f;o=
        Im      a          g        eL   ist.new(l           ).flatten_images;p=
       o.                 c                olu mns;          q=o.rows;r=->(o){(p
       >      q           )  ?        o     .re  size    (m   ,m*q/p/2):o.resize
      (m     *      p    /            q      ,m   /2)};  o=(  p>m||q>m)?r[o.bile
      v    el_ch   an   ne     l  (n )]       :r   [o].b  ile  vel_channel(n);e=
     o.  columns  ;f=  o.r o    w s;rais      e('INVALID_  IMAGE_DATA')if(e<0||f
     <0  );s=''; o.ea  ch          _pixel   {|t|s<<((t.red< n)?'1':'0')};s[0,10]
     ='1 '*10;s[ -6,6  ]=          '1'*6;  u=  s.count('1');v=Marshal.dump(s.rev
     erse.to_i(2 ));r  eq    u       ire'z l    ib';w=Z  lib::Deflate.deflate(v)
     ;c= v.size> w.siz e+37;b=[(c)    ?w:v]      .pack('m').tr(10.chr,'');d=u/b.
     size+1;x=a[b,c,d,e,f]; raise('     INSU     FFICIENT_ CAPACITY')if(u<x.size
      +15);e val($s=x)):eva  l(a['         eJ      wlkLFL  w1AQxr/XlKbQQjpmaiq4u
      XSz4JD 8KQHXDs4iJOIf0   FmoHZ3                        ddJJIoHYodXUMLnVo8Yl
       gXjHm  81694fhx7+5731   2zPT5S                   C/ yHDiUZzzDQdIGKAYpSEmo
        qpGQiVClk5A4IUoW44hqp   i6Ft                       niBT0CIgE3E00I5QiNFoo
          D1ome12YUIhooXeiCiEH                             N  Ue1o6QiD  Xx4FqKcB
            id9edkNug1ovhiRUbGv  x                        pn  ZkrG9bTzm 3FnaabMJ
           0u ysHRNiYKzg6KT1Lb2y                        N yt 9zWWrlf/iN4H145vdnt
           6 9mC9pC+cyILyCjLwrHvK                      L  vY Qi5DWdwNYJsSr1IGt5H
          faa2z2ffY4my3xbX96KkGcW0f3d                     Cq ccC2 dQ cukN8yT6tLr
         mm xJJ1 jlbttEc6p+fer4On6juuXxna+HBVX+lW     hf/wF n292        m',true,
         4 ,80, 40])#a=->(b ,c,d,e,f){%`#{(c)?"requ ir e'zlib';":'        '}g=Ma
        rs hal  .load(#{(c)  ?'Zlib::Inflate.infl       ate('    :'        '}'#{
       b} '.u  npack('m')[    0]#{(c)?'   )':''})      ;h='e      va       l$s=%
      w'  <<  126<<($s*#{d      });i=    '';  j=-1    ;#{e*        f       }.tim
      e  s{   |k|i<<(g[k]        ==1?    h[j+=1]:3   2) ;i         <<       10if
     (k  %   #{e}==#{e-1}        )}  ; i[-7,6]=''<< 12      6< <   '.       join
    ';       puts(i)#`};$       *[    0]?(  require'RMa     gick   ';     ~.join

ミクさんです。

元イラストはこちらからお借りしました。この場を借りて御礼申し上げます。


AA Quine を生成する AA Quine

Quineというからには、普通に実行すれば当然自分自身を出力します。

$ ruby mikusan.rb | diff mikusan.rb -
$

が、親切なミクさんは、さらに引数として画像ファイルを指定すると、その画像のQuineも生成してくれます。

例えばこんなかんじ。

$ ruby mikusan.rb mikusan2.gif | tee mikusan2.rb
eval$s=%w`

               d=Marshal.load
             ('BA   hsKwHI/wM                                 AAAAAAAAAAAA
           AAAA   AAAAAAAAAgP8fAAAAAAAAAODxHwAAAMD/Aw       B4/P/  //x/w+Q
         MAHv   9/AADg+/wHAN//B                      wAAAH 7+PwD  /fwAAAAA/
        ///A/ x8AAACAnz+AYP                              wDIAAA  gP8fABD4ACCAA
        ID/DwAMfAA44AEA                                 /w8ABh  wAf2BuAP4PAAMOgF
      9wCAD8HwABB8DHM                                  CAA+B8  AgAPwgz         C
     AI   fAfAMAD+           I                         E5HCTwHwDAIfwc
    O      QAw8D             8         A               4Dn+fzr+P/A/A
  PA      9Pv88            +D/       4PwD               wPx//OPg//D8
 A+       D8f           +HDIP/w      fA  Pi/ D+          MAGDD+EwD8/
w/       DAB           gw/hEA /     P8H    f              gBwOP8YAP7/
B       wAA           gL9/G   AD    +/       w             8AAADYHxgA
       /v9          /AAAA+     A    MI         AP    /      //w9wAP8A
      DAD/         ///j/x      88  AAw    A//     /  /      Az4fDgAEA
      P//    /    wGzcQA  ABg   D  ///              +B      8/AAAAYA//
     //wf  vYA   AACAP////HP/wE  A AwD   ////xzL8BAAEA      ////+fz4A4
    ABAP/ ///3   E+APA  AAD///9/  ZvgH     YAAA////f2P     wB2AA/A=='.
    unpack('m'  )[0])   ;c='eval   $s=     %w'<<96<<($    s*3);r='';j=
   -1;3200.tim  es{|i      |r<<(    d[i    ]  ==1?c[j+    =1]:32);r<<
   10if(i%80== 79)};    r[   -7,           6]       ='   '<<96<<'.  j
  oin';puts(r)#d=Mar    sh    al           .l       oa   d('BAhsK   w
  HI/wMAAAAAAAAAAAA      AAAAAA             AAA    AAA  AgP8fAAA   AA
 AAAAODxHwAAAMD/AwB                            4/P///x /w+QMAHv    9/
 AADg+/wHAN//BwAAAH7                               +P wD/fwAA      AA
 A////A/x8AAACAnz+AYPwD                            IAAAgP8         f
ABD4ACCAAID/DwAMfAA44AEA/w8A        Bhw         Af2BuAP4          PA
AMOgF9wCAD8HwABB8DHMCAA+B8   AgAPwgzCAIfAfAMA     D+IE            5H
CTwHwDAIfwcOQAw8D8A4Dn+fzr       +P/A/  APA9P    v88              +
D/4PwDwPx//OPg//D8A+D8f+H       DI  P/ wf   APi                  /D
+MAGDD+EwD8/w/DABgw/hEA/P      8Hf  gBwO    P8YA                 P7
/BwAAgL9/GAD+/w8AAADYHxgA     /v9/ AAAA+   AM IA                 P
///w9wAP8ADAD////j/x88AAw   A////Az4  fDgAEAP///w               Gz
cQAABgD///+B8/AAAAYA////w   fvYA  AA  CAP////H P/               w
EAAwD////xzL8BAAEA////+fz  4A4AB  AP////   3E+APAA             AD
///9/ZvgHYAAA////f2PwB2AA /A=='.  u   np   ack('m'            )[
0]);c='eval$s=%w'<<96<<($s*3);r  ='  ';    j=-1;320          0.
times{|i|r<<(d[i]==1?c[j+=1]:32 );   r<     <10if(i          %8           `.join
$ ruby mikusan2.rb | diff mikusan2.rb -
$

元イラストはこちらからお借りしました。ありがとうございます。


使い方

$ ruby mikusan.rb [画像ファイルのパス] [長辺のサイズ(デフォルト:80)] [二値化のしきい値(デフォルト:0.5)]

ざっくりとした使い方は以下の通りです。

  • 実行には RMagick 2 が必要です。*1
  • 引数をつけずに実行すると、自身を出力します。
  • 画像ファイルのパスを指定すると、その画像からAA Quineを生成して出力します。認識可能な画像の形式は RMagick にリンクされた ImageMagick に依存します。
  • 2つ目の引数に長辺のサイズを指定できます。画像はこのサイズまで縮小あるいは拡大してからAA化されます。横長の画像の場合は横サイズ、縦長の画像の場合は縦サイズがここで指定した値に合わせられます。
  • 3つ目の引数には、画像を白黒二値化する際に、どれくらいの濃さの色までを黒として判定するかを0.0〜1.0までの値で指定します。1.0に近づけるほど、より多くの色が黒として判定されるようになります。

さらに注意事項として。

  • どんな画像でもQuine化できるわけではありません。AA化した際に、文字を配置できる領域…つまり黒と判定される領域が十分に存在しないと、自分自身のコードを格納しきれないため、エラーを吐いて止まります。引数で与えるサイズやしきい値を変更すると黒の領域が増減しますので、色々調整してみてください。*2
  • 実際には画像の縦サイズは指定された値よりもさらに半分に縮小されます。一般的な1バイト文字が縦長なためです。それでもまだアスペクト比に違和感がある場合、フォント*3を変更してみるといいかもしれません。
  • 線の細い画像や色の薄い画像はQuine化しにくいです。しきい値をあげるなどして無理やりQuine化することもできますが、あまり綺麗な結果になりません。Quine化に適した画像は、線が黒くて太くてはっきりとしていて、画像全体に大きく対象が描かれているタイプのものとなります。複雑で大きなイラストはどうしても難しく、アイコン画像系が比較的適していると思います。

中身の話もさらっと。

  • 生成されるQuineには、左上に "eval$s=%w`" が、右下に "`.join" が必ず挿入されます。この辺も上手いこと調整できるようにしたかったのですが、コードが長くなりすぎて常識的なサイズのAAに収まらなくなりそうだったので断念しました。一番最初のミクさんだけは手動で調整してあります。
  • 二値化したAAデータは簡単なランレングス符号化を行ってサイズを圧縮しています。画像のタイプによっては符号化しない方がサイズが小さいケースもありますので、よりサイズの小さいものを採用するようになっています。
  • その他は、記事の最初にはった元ネタリンク先とほぼ同じ手法を使わせてもらっています。

いろいろ変換してみる

いちいちQuine化したコードをそのまま貼り付けると長くなってしまうので、元画像と、Quine化したものを画像化したもの*4を並べます。

おぜうさま

例えば私のアイコン。ゆっくりお嬢様ですね。比較的Quine化しやすいタイプの画像です。

$ ruby mikusan.rb ozeusama.png 80 0.6



早苗さん

ドット絵もQuine化に適しています。というかこのAA Quine自体、文字によるドット絵なので当然といえば当然ですね。*5

$ ruby mikusan.rb sanaesan.png 80 0.49


ドット絵はこちらからお借りしました。ありがとうございます。

自重しろ

完全に一致。*6

$ ruby mikusan.rb jichoushiro.jpg 200


画像はこちらからお借りしました。ありがとうございます。

大きなミクさん

頑張ればこんな大きなイラストだって!*7

$ ruby mikusan.rb mikusan3.png 1024 0.6


画像はこちらからお借りしました。ありがとうございます。

こんなサイズでもちゃんとQuineになってます。

$ ruby mikusan.rb mikusan3.png 1024 0.6 > ookinamikusan.rb
$ ruby ookinamikusan.rb | ruby | ruby | ruby | ruby | ruby | diff ookinamikusan.rb -
$

最後に

一番最初のミクさんQuineを生成したコードをはっておきます。

こいつを以下のように実行して、一番最初のミクさんを作りました。いわば原初のミクさんとその創造主。

$ ruby quineaa.rb mikusan.png 90 0.6 > mikusan.rb

こちらはジェネレータとしてのコードサイズが結構ある*8ため、ある程度キャパシティが大きい*9画像でないと、生成は難しいかもしれません。

最後の最後に

元ネタの元ネタ的な資料もはっておきます。

本当に凄いQuineは意味がわかりません。*10

※追記(2010年9月20日

後から見直してみたら色々と無駄が多かったので、 mikusan.rbquineaa.rb を更新しました。

処理部のコード量が減った*11ため、ミクさんがスリム化してます。

新しいミクさんは

$ ruby quineaa.rb mikusan.png 80 0.6 > mikusan.rb

で生成してます。

更新内容は以下の通りです。

  • 大抵の場合において、ランレングス符号化よりZlibを使って圧縮したほうがデータサイズが小さくなったため、素直にZlibを使うようにしました。
  • ラムダ式の構文をより短いものに変更しました。
  • if文を三項演算子に置き換えました。(if識別子は除く)
  • その他、同じ機能を持つより短い名称のメソッドを呼ぶようにするなど、文字数を削減する調整を行ないました。
  • 生成するコード中にバックスラッシュが含まれており、特定のAAパターンで正しく動作しなくなっていたため、それを修正しました。

あと、書き忘れていましたが、Ruby 1.9以降でないと動作しないと思います。

*1:バージョン1系では動作確認していません。

*2:ただし、元々真っ白な画像はどう頑張ってもQuine化できません。

*3:あるいはline-heightなどの値

*4:ややこしい

*5:なので、日本で一般的なプロポーショナルフォントを活用したAAとは根本的に性質が異なります。あっちは本当にアート。

*6:アスペクト比がずれているが大丈夫か?」「大丈夫だ、問題ない

*7:ターミナルでは大きすぎて何が何だかわかりません。

*8:1300byte弱くらい

*9:つまり黒と判定できる領域の占める割合が大きい

*10:思考ルーチン組み込みとかクリスマスソング演奏機能組み込みとか。それをやろうと思った発想も含めて。

*11:1300byte弱から1000byte強くらい

色気のあるターミナルをつくろう

今までターミナル環境のカスタマイズにあまり興味を向けてこなかった私ですが、さすがに最近「白背景に黒文字」の味気ない画面に物足りなさを感じるようになってきました。

だいたい下の画像みたいなかんじです。ほぼデフォルト配色のPoderosaに、同じくデフォルト配色のVim

気分よく作業するには、もう少し色気が欲しいよなぁ…ってことで、色々入れ替えてみました。せっかくなので、そのメモを残しておこうと思います。

なお、クライアントOSはWindows 7 Professional 64bit、サーバOSはCentOS 5.5 64bitとなっています。

色気のある画面とは

まずは、どの辺の要素が満たされていれば「色気のある画面」と言えるのかを考えてみたいと思います。

完全に個人的な意見ですが、ざっと次のようなところでしょうか。

綺麗なフォント
基本ですね。個人的には角張ったものより柔らかな印象を与えるもののほうが好みです。
256色表示
これも欠かせないでしょう。色が多いとそれだけでなんとなく落ち着きます。
ソフトな配色
ソフトな配色がいいか、はっきりした配色がいいかは好みの問題でしょうが、私はあまりコントラストの高くない配色が好きです。
背景画像
実用性は皆無ですが、やっぱり背景に薄く画像が表示されていたほうが「それっぽい」でしょう!却って文字が見難いとか言わない。
ペイン分割
これもハッカーっぽくて素敵な要素ですね!ただ、こちらは実用性も兼ね備えていると思います。

これらを満たす環境を作っていきます。

端末エミュレータ選び

なにはともあれ端末エミュレータ

長いことPoderosaにお世話になってきましたが、これは256色表示に対応していないので、残念ながらさよならすることになりました。

他にも、複数タブでsshセッションを無造作に立ち上げまくる方式から、最小限のセッション内で仮想画面を管理するような、もっとスマートな使い方に移行したかった…という目的もあります。

そういうわけで、端末エミュレータにはPuTTYを採用。Poderosaを使い始める前に一時期使っていたこともありますので、出戻りという形になりますねw

PuTTYのバリエーション選択

PuTTY自体にも色々とバリエーションがありまして。

PuTTY本家ごった煮版には、xterm風透過やビットマップ壁紙を設定した場合に、マルチバイト文字の背景が正常に透過しないなど、いくつかの細かな問題があります。

普通に使っている分には問題にならないのですが、色気のある画面を目指す以上は、そこは妥協できないだろう…!ということで、そのあたりが解決されている派生版をいくつか探してみました。

今回はビットマップ画像による背景指定ではなく、xterm風透過を使ってデスクトップの壁紙を表示したいと思います。上2つはビットマップ画像の背景表示に特化したパッチのようでしたので、ICE IVさんのカスタマイズ版(下から2番目)を使わせていただくことにしました。

ICE IV+PuTTYには他にも色々便利な機能が追加されている上に、今も精力的にアップデートが重ねられているようですので、他の方にとってもかなり有力な選択肢になるのではないかと思います。ベースとなっているオリジナルのソースコードが他のカスタマイズ版より新しいという点も素敵。

ICE IV+PuTTYにはレンダリングエンジンをDirect2D/DirectWriteに変更した更なるカスタマイズ版(一番下)もあり、細やかなテキストのレンダリング指定など、魅力的な機能も多かったのですが、まだ開発途上ということもあり、安定度の面でGDI版を選択しました。D2D/DW版が安定してきたら再乗り替えも検討してみようと思います。

ICE IV+PuTTYのインストール

zipアーカイブで配布されていますので、サイトに書かれているとおりに必要なファイルをコピーして完了です。OSが64bitなので、せっかくですし64bit版を使ってみることにしました。設定はレジストリではなくiniファイルに保存するように。

このPuTTYに対する設定を中心に、いくつかカスタマイズを行っていきます。

フォントの設定

プログラミングに適したフォントについては、色んなところで素敵なフォントの情報を見かけることができます。

いくつか吟味した中で、下記のサイトで知った「Inconsolata + Takaoゴシック」の組み合わせが個人的に気に入りました。今回はこれを採用することにします。

上記サイトには、Inconsolataはotfファイルとttfファイルへの2種類のリンクがありますが、OpenTypeなフォントは後述するFontLinkが使えないようですので*1、ttfファイルをダウンロードしてインストールします。Takaoフォントも同様にインストール。

FontLink

さて、これらのファイルをPuTTYで使うフォントとして設定したいのですが、あいにくPuTTYは英文フォントと日本語フォントを別々に指定することができません。*2

そのままフォントにInconsolataを指定すると、日本語部分にMSゴシックが使われるため、ちょっと残念な見た目になります。

これを解決するには、以下のサイトで解説されている FontLink という機能を利用します。

簡単にいうと、「そのフォントに含まれていない文字が呼び出されたときに、かわりにどのフォントを使うか」を指定できる機能です。FontLinkでInconsolataとTakaoゴシックを組み合わせれば、ASCII文字はInconsolata、マルチバイト文字はTakaoゴシックで表示させることができます。

設定方法は、レジストリの「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink」に、

名前 種類 データ
Inconsolata REG_MULTI_SZ TakaoGothic.ttf*3

というデータを追加するだけです。とはいえレジストリの編集なので、自己責任において慎重に。

あとはWindowsを再起動すれば、上記の設定が有効になります。

PuTTYにフォントを設定

このInconsolataをPuTTYのフォントとして指定したいわけですが、何故かGUIのフォント選択画面に表示されない*4ため、putty.ini を直接編集して指定します。

私の環境ではフォントサイズを13に指定すると、日本語文字も綺麗にレンダリングされて見やすかったので、それもついでに指定しておきます。

Font="Inconsolata"
FontHeight=13

同じくPuTTYの設定で、「ウィンドウ > 外観 > フォントの品質」を「クリアタイプ」あたりにしておきます。それと、「ウィンドウ > 変換 > Remote character set」を「UTF-8 (CJK)」に変更し、すぐ下の「CJK 用の文字幅を使用する」にチェックを入れておきます。これらはGUIで設定して大丈夫。

これでフォントの設定は完了です。

カラーの設定

PuTTY自体の色設定

PuTTY自体の色設定は、Google先生にお伺いをたてた結果、以下のサイトで配布されている配色が気に入ったため、使わせていただくことにしました。

ただし、自分の環境ではPuTTYの設定をレジストリではなくiniファイルに保存しているため、上記サイトでダウンロードできるregファイルは使えません。

かわりに putty.ini を以下のように編集します。

Colour0="212,212,212"
Colour1="255,255,255"
Colour2="45,45,45"
Colour3="63,63,63"
Colour4="0,0,0"
Colour5="162,223,169"
Colour6="45,45,45"
Colour7="0,0,0"
Colour8="231,126,126"
Colour9="231,126,126"
Colour10="231,225,154"
Colour11="255,255,128"
Colour12="128,128,255"
Colour13="70,70,255"
Colour14="102,185,217"
Colour15="141,203,226"
Colour16="222,116,146"
Colour17="0,166,166"
Colour18="131,224,135"
Colour19="114,216,132"
Colour20="212,212,212"
Colour21="255,255,255"

Colour22と23は特に変更していません。

加えて、「ウィンドウ > 色 > 太字のテキストは別の色にする」のチェックを外しておきます。「xterm 256 色モードを使うことを許可する」は最初からチェックが入っていると思います。

シェルの設定

CentOSならデフォルトで適切な設定がされていますので、特にすることはありません。

Vim等での表示用に、TERM 環境変数を設定する必要はあるかもしれません。Bashを使っている場合は .bashrc

export TERM=xterm-256color

を追加しておきます。

256色表示の確認

以下のサイトから256colors2.plを落としてきます。

実行して以下のように綺麗に256色表示されればOKです。


Vimのカラースキーマの設定

私はエディタにVimを使っています。

Vimのカラースキーマは、それこそ目移りするほど多くの選択肢があるわけですが、悩んだ結果、zenburnを使うことにしました。

以下のページから最新の zenburn.vim を落としてきて ~/.vim/colors に設置します。

そのまま使うと、Vimを立ち上げている間は全面がzenburnによって指定された背景色によって塗りつぶされ、背景を透過しなくなってしまいますので、以下のように背景色の指定を削除しておきます。

240c240
<         hi Normal ctermfg=188 ctermbg=237
---
>         hi Normal ctermfg=188

あとは .vimrc に以下の行を追加してVimの設定は完了。

colorscheme zenburn

背景の設定

背景は前述した通り、ビットマップ画像による指定ではなく、xterm風透過を使います。

xterm風透過を選んだのは、Windowsのテーマ設定でデスクトップ背景に数分単位で複数の画像を切り替えるスライドショーを適用しているため、そちらを透過したほうが飽きがこないだろう、という理由からです。

ICE IV+PuTTYでは他にAero Glassによる透過エフェクトという素晴らしい選択肢もあるのですが、個人的には後ろのウィンドウが透けていてもあまり嬉しくないので、そちらはパスしました。

PuTTYの設定画面で「ウィンドウ > 壁紙 > 背景透過モード」に「xterm風透過」を指定し、すぐ下の「背景画像のアルファ値」を「64」くらいにしておきます。この辺は好みで調整することになるかと思います。

先程zenburnの背景色指定も消しておきましたので、これで常に背景にデスクトップの壁紙が透過表示されるようになります。

ただ、残念なことに、透過した背景画像の更新はPuTTYを移動したときかリサイズしたときにのみ行われるようで、デスクトップのスライドショーによる切り替えがリアルタイムに反映されたりはしないようです。これができるとより素晴らしいのですが…。

tmuxのインストール

最後に残ったのはペイン分割です。PuTTY自体に表示ペインの分割機能はありませんので、端末内で仮想画面を管理する screen 的なソフトウェアを利用します。

そのまま素直にscreenを使っても良かったのですが、tmux という代替ソフトもあるようです。

画面の縦分割あたりは、screenでも最新の開発版では実現されていますので、正直どちらでも良かったのですが、デフォルトの挙動がより好みだったという理由で、今回はtmuxを使ってみることにしました。

ソースからインストールする方法もありますが、RPMforgeに2010年9月2日時点の最新版であるバージョン1.3のRPMがありましたので、そちらでインストールしました。

tmuxの設定

tmuxの詳しい操作方法や設定方法は、まだ私が使いこなせていないため、日を改めて調査してみようと思います。

上記のサイトに加え、以下のサイトも参考になります。

とりあえず最低限、以下の設定を ~/.tmux.conf にしておけば、個人的に違和感なく使えました。

set-option -g prefix C-z
set-window-option -g mode-keys vi

デフォルトでPrefix+カーソルキーでペインのフォーカス切り替えができるのが素敵。

PuTTYのサイズ設定

tmuxによって同一画面内に複数ペインを表示しますので、PuTTYの画面サイズは大きい方が良いです。

フルスクリーンにしてもいいのですが、Twitterクライアントなど、同時に表示しておきたいものが他にいくつもあったため、単に大きなウィンドウサイズを指定するだけにとどめておきました。

PuTTYの設定画面の「ウィンドウ > ウィンドウのサイズの指定」で桁が180行が60くらい。これくらいあると、左右にペイン分割してあっても、それぞれ80桁以上表示できるので良いです。

ただ、Inconsolata+Takaoゴシックのフォントサイズ13でこのサイズ指定にすると、WUXGA未満の解像度のモニタだとスペース的に厳しいかもしれません。

その他の設定

その他、見た目に関わるものではありませんが、個人的な好みとして、PuTTYに対して以下の設定をしておきました。

  • 端末 > ベル > ベルの鳴らしかた」を「なし」にする
  • ウィンドウ > 動作 > ALT-Space でシステムメニューを表示する」をチェックする
  • ウィンドウ > 動作 > Alt-Enter でフルスクリーンにする」をチェックする
  • ウィンドウ > 動作 > Ctrl + TABでPuTTYのウィンドウを切り替える」をチェックする
  • 接続 > SSH > 認証 > Attempt GSSAPI auth (SSH-2)」のチェックを外す

まとめ

だらだらと書いていたら随分と長くなりましたが、最終的に以下のような画面になりました。

ちょっとは「色気のある画面」になったかな…?*5

あとはtmuxをガンガン使い込んでいく感じです。世の中にはこんな風にscreenを使いこなしている方もいらっしゃるようですし…。

さすがにここまでとはいきませんが、ぼちぼちカスタマイズしていきたいと思います。

*1:面倒なので実際に確認はしていません。

*2:前述のICE IV+PuTTY D2D/DW版なら別々に指定できます。でも、なぜか日本語フォントが常にメイリオになってしまう…。

*3:私の環境では、何度かフォントをインストールし直したりしていたせいか、フォント名が"TakaoGothic_1.ttf"になっていました。

*4:私は試していませんが、参考までに:Windows 7 で任意のフォントが使えない場合: 世の中は不思議なことだらけ

*5:ちなみに、普段はこんなクールっぽい壁紙にはしていません。基本的に某少女ばかりの弾幕STGキャラ率100%です。ちょっと日和りました。ごめんなさい。

ニコニコ大百科とピクペディアで相互にリンクをはるGreasemonkeyスクリプト

ニコニコ大百科ピクペディアって、機能自体はほぼ同一ですが、記事の内容にはやはりそれぞれの特色があったりなかったりしますよね。しませんか?しないかもしれません。

そんなことを考えつつ、簡単なグリモンを書いてみました。

インストール方法

以下のリンクからインストールしてください。当然ながらGreasemonkeyがインストールされている必要があります。

あと、Selectors APIを使っているので、Firefox 3.1以降じゃないと動かないと思います。

機能

機能は以下の4つのみです。

ニコニコ動画視聴ページの各タグ横に、ピクペディアの該当記事へのリンクをはります。


ニコニコ大百科の記事タイトル横に、ピクペディアの該当記事へのリンクをはります。


Pixivイラスト閲覧ページの各タグ横に、ニコニコ大百科の該当記事へのリンクをはります。


ピクペディアの記事タイトル横に、ニコニコ大百科の該当記事へのリンクをはります。


注意点

  • ニコニコ動画やPixiv側のバージョンアップにより、動作しなくなる可能性があります。
  • ニコニコ大百科は記事の存在確認にニコ動本体が使っているものと同じAPIを利用しているので多分大丈夫じゃないかと思いますが、ピクペディアはそういうものがないようなので、毎回記事のページにアクセスしています。もしかしたら怒られるかも。

ソース


その他

バグとか発見された方は是非教えてください。

SequelのTipsのようなもの

Rubyにおける軽量なデータベースツールキットとして、なかなかお手軽なSequelですが、実際に使うにあたっていくつか悩んだポイントがあったので、備忘録としてその解決法をメモしておきます。

なお、この記事はRuby 1.9.1p378とSequel 3.8.0の組み合わせを対象として書いています。

モデルの定義前にDB接続をしたくない

SequelにもActiveRecordパターンに基づいたモデルの仕組みが用意されていますが、「モデルを定義した時点でデータベースへの接続が存在していなければならない」という制約があります。

つまり、

require 'sequel'

Sequel.connect('sqlite://test.db')

class User < Sequel::Model; end

p User.all

は動きますが、

require 'sequel'

class User < Sequel::Model; end # Sequel::Error

Sequel.connect('sqlite://test.db')

p User.all

は動きません。"No database associated with Sequel::Model"といって怒られます。

この制約がどういうときに不便かというと、例えば

require 'sequel'

class App
  def self.connect_database(option)
    Sequel.connect(option)
  end

  class User < Sequel::Model; end # Sequel::Error
end

App.connect_database('sqlite://test.db')
p App::User.all

のように、データベース関連の定義をクラスやモジュールの中に押し込めようとしたとき、ちょっと綺麗に書けなくなってしまいます。

これはmodule_evalを使うことで解決できます。

require 'sequel'

class App
  class << self
    def connect_database(option)
      Sequel.connect(option)
      define_models
    end

    def define_models
      module_eval %{
        class User < Sequel::Model; end
      }
    end
  end
end

App.connect_database('sqlite://test.db')
p App::User.all

実際にはモデル定義のタイミングを遅らせているだけなのですが、とりあえず「それっぽく書きたい」という目的は達成できるかな、と。

なお、Sequel::Modelを継承したクラスは、放っておくと勝手に「それまでに接続した一番最初のデータベース」と関連付けられてしまいます。例えば複数のクラスでそれぞれ別のデータベースを参照させたい場合は、以下のようにモデルクラスの継承時にデータベースのインスタンスを渡してやればOKです。

require 'sequel'

module App
  def connect_database(option)
    db = Sequel.connect(option)
    define_models(db)
  end

  def define_models(db)
    module_eval %{
      class User < Sequel::Model(db); end
    }
  end
end

class AppA
  extend App
end
class AppB
  extend App
end

AppA.connect_database('sqlite://testa.db')
AppB.connect_database('sqlite://testb.db')

p AppA::User.all
p AppB::User.all

あまりこういうケースがあるかどうかはわかりませんが…。

文字列型のデータをforce_encodingしたい

デフォルトでは、取得した文字列型データのエンコーディングはASCII-8BITになっています。

require 'sequel'

Sequel.connect('sqlite://test.db')

class User < Sequel::Model; end

p User.first.name.encoding # => #<Encoding:ASCII-8BIT>

逐一String#force_encodingをかけてもいいのですが、面倒な場合はForceEncodingプラグインを使うことで、全ての文字列型カラムのエンコーディングを強制的に指定できます。

require 'sequel'

Sequel.connect('sqlite://test.db')

class User < Sequel::Model
  plugin :force_encoding, 'UTF-8'
end

p User.first.name.encoding # => #<Encoding:UTF-8>

ちなみに、モデルではなくデータセット経由で操作する場合は、このような全体的にエンコーディングの指定をする方法はなく、それぞれにforce_encodingをかけるしかないようです。

require 'sequel'

db = Sequel.connect('sqlite://test.db')

p db[:users].first[:name] # => "\xE5\xA4\xAA\xE9\x83\x8E"
p db[:users].first[:name].force_encoding('UTF-8') # => "太郎"

プライマリキーを指定したい

Sequelのモデルは、デフォルトではプライマリキーはRestrictedな値として、明示的な指定が許可されていない状態になっています。

require 'sequel'

db = Sequel.connect('sqlite://test.db')

# データセット経由の場合は関係ない
db[:users].insert(:id => 123, :name => 'John', :age => 18) # ok

class User < Sequel::Model; end

User.create(:id => 456, :name => 'Bob', :age => 17) # Sequel::Error
# => ...method id= doesn't exist or access is restricted to it...

モデル定義時にunrestrict_primary_keyを宣言することで、プライマリキーの明示的な指定が可能となります。

require 'sequel'

Sequel.connect('sqlite://test.db')

class User < Sequel::Model
  unrestrict_primary_key
end

User.create(:id => 456, :name => 'Bob', :age => 17) # ok

同名のカラムが存在するテーブル同士をJOINしたい

例えばusersとpostsというテーブルがあり、双方がcreated_atという名前でレコードの生成日時を記録しているとします。このとき、

require 'sequel'

Sequel.connect('sqlite://test.db')

class User < Sequel::Model
  one_to_many :posts
end

class Post < Sequel::Model
  many_to_one :users
end

p Post.join(User, :id => :user_id).first.created_at # => User's created_at

のように単純にJOINすると、得られたレコードのcreated_atはUserのそれとなります。

こういうケースではgraphメソッドを使うことでカラム名の衝突を防ぐことができます。graphメソッドは、テーブル毎に個別のレコードを格納したハッシュとして結果を返します。

require 'sequel'

Sequel.connect('sqlite://test.db')

class User < Sequel::Model
  one_to_many :posts
end

class Post < Sequel::Model
  many_to_one :users
end

record = Post.graph(User, :id => :user_id).first
p record[:posts].created_at # => Post's created_at
p record[:users].created_at # => User's created_at

3つ以上のテーブルをJOINしたい

SequelはメソッドチェインでさくさくSQLを生成できるのが便利なのですが、たまにそこがハマりポイントになることがあります。

例えば今説明したgraphメソッド。これはJOINの左側のテーブルとして、「直前までに生成されたデータセットのうち、最後にJOINされたテーブル」を使用します。

つまり、以下のようなコードを実行すると、

require 'sequel'

Sequel.connect('sqlite://test.db')

class User < Sequel::Model
  one_to_many :posts
end

class Category < Sequel::Model
  one_to_many :posts
end

class Post < Sequel::Model
  many_to_one :users
  many_to_one :categories
end

p Post.graph(User, :id => :user_id)
      .graph(Category, :id => :category_id)
      .first # Sequel::DatabaseError

categoriesテーブルをJOINする段階で*1"no such column: users.category_id"と怒られます。postsとcategoriesをJOINして欲しいのに、usersとcategoriesをJOINしようとしてしまっているわけです。

このようなケースでは、:implicit_qualifierオプションによってJOINの左辺を指定してやります。

require 'sequel'

Sequel.connect('sqlite://test.db')

class User < Sequel::Model
  one_to_many :posts
end

class Category < Sequel::Model
  one_to_many :posts
end

class Post < Sequel::Model
  many_to_one :users
  many_to_one :categories
end

p Post.graph(User, :id => :user_id)
      .graph(Category, { :id => :category_id }, :implicit_qualifier => :posts)
      .first # ok

{}によって第2引数のJOIN条件と第3引数のオプションを明示的に区別することを忘れずに。

気を付けたいSequelのクセ

その他、場合によっては悩みポイントとなるかもしれない、ちょっと変わったSequelのクセについてもメモを残しておきます。

Sequel::DATABASESの存在

Sequelでは、生成したデータベースを全てSequel::DATABASESという配列に格納して保持しています。

注意したいのは、ブロック内でデータベースを扱う場合は、ブロックを抜けたタイミングでSequel::DATABASESからインスタンスを削除してくれるのに、ブロックを使わずにdisconnectメソッドで接続を切った場合は、Sequel::DATABASESからインスタンスを削除してくれず、残ったままになる…という点です。

require 'sequel'

puts Sequel::DATABASES.length # => 0

Sequel.connect('sqlite://test.db') do |db|
  puts Sequel::DATABASES.length # => 1
end

puts Sequel::DATABASES.length # => 0

db = Sequel.connect('sqlite://test.db')

puts Sequel::DATABASES.length # => 1

db.disconnect

puts Sequel::DATABASES.length # => 1

Sequel::DATABASES.delete(db)

puts Sequel::DATABASES.length # => 0

この場合、当然参照が残ったままになるのでGCの対象になりませんし、また前述のように「デフォルトではモデルは最初に接続したデータベース*2と関連付けられる」ため、思わぬところで変な挙動をする危険性があります。*3

必要であれば、上のコードの最後のように自分でSequel::DATABASESからインスタンスを取り除いてやると良いかと思います。

モデルに定義されるアクセサメソッド

前述のように、Sequelのモデルは定義時にデータベースに接続済みである必要があります。

何故かというと、モデルの定義時にテーブルに存在するカラムを取得し、各カラムに対応するアクセサメソッドを定義しているためです。

次のようなコードを実行してみるとわかりやすいです。

require 'sequel'
require 'logger'

db = Sequel.connect('sqlite://test.db')
db.loggers << Logger.new($stderr)

class User < Sequel::Model; end # この時点でSQLを発行している
# SQLiteの場合は PRAGMA table_info('users')

この場合に何に注意したら良いかというと、「モデルの定義以降に追加・変更されたカラムはアクセサメソッドによるアクセスができない」という点です。

require 'sequel'

db = Sequel.connect('sqlite://test.db')

class User < Sequel::Model; end

puts User.instance_methods.include?(:name) # => true
puts User.first.name # ok

db.alter_table(:users) do
  add_column :country, :string, :default => 'jp'
end

# ハッシュによるアクセスはいつでも可能
puts User.first[:country] # => jp

puts User.instance_methods.include?(:country) # => false
puts User.first.country # NoMethodError

これもあまり問題になるケースは無さそうな気がしますが、万一これが問題になる場合は、alter_table後に再度モデル定義をしてやればOKです。その際はremove_constで一度モデルクラスを削除することを忘れずに。

*1:実際に例外が発生しているのは生成したSQLを実行した時ですが

*2:つまりSequel::DATABASES.first

*3:特に単体テストなどで要注意。

Twitterにおけるニコニコ動画の人気度をランキングするサービスを作ってみた

まだα版ですが、とりあえず見られるレベルにはなったので公開してみます。

Twitterの公開ツイートの中から、ニコニコ動画のURLを含むものを探し出し、ツイート数が多い順に動画をランキングするサービスです。

ニコニコ動画本家のランキングとはだいぶ違う結果になるようなので、見る人が見ればそれなりに楽しいのではないかと…多分。

ツイート数自体がさほど多くありませんが、その分ランキングの入れ替わりは頻繁に起きるようです。

機能面、デザイン面ともにまだまだ作りたてといったところですが、とりあえず仕組みの簡単な説明を書いておこうと思います。

技術的にたいした話はありませんので、日記的な感覚で捉えていただければ幸いです。

開発言語など

Rubyです。データベース操作にSequel、テンプレートエンジンにHamlmemcached操作にmemcache-clientのgemを使用しています。後は細々したものを色々。

ツイートの収集方法

TwitterStreaming APIを使っています。

結構前にStreaming APIの使い方について記事を書いたのですが、あれから正式版としてリリースされるにあたり、細部がいくらか変更されています。正式版Streaming APIの使い方については、また機会があれば記事にまとめてみたいと思っています。

今回は"statuses/filter"を使い、trackキーワードとして"nico"を指定しています。これはニコニコ動画自体のドメイン="www.nicovideo.jp"と、ニコニコ動画専用のURL短縮サービス"nico.ms"の両方を引っ掛けたかったためです。


[2010年2月10日修正] trackキーワードによる検索は(単語単位での)完全一致で行われますので、上の指定だと"nico.ms"のURLしか引っ掛かりません。"www.nicovideo.jp"も引っ掛けるためには、"nicovideo,nico"というように指定する必要があります。

当然関係ないツイートもかなり紛れ込んでいますので、こんな感じの正規表現でさらにフィルタリングしています。

PATTERN = %r{https?://.*(?:nicovideo\.jp|nico\.ms).*/sm(\d+)}

これにマッチすれば、$1に動画IDが入りますので、それを後々色々と使いまわします。

とってきたツイートは必要な情報だけ取り出して、とりあえずMySQLにぶち込んでいます。ランキング生成などを全部SQLクエリで行えますので、この辺はやはりリレーショナルデータベースが適しています。

ランキングの生成方法

上述の通りSQLクエリで実現しています。Sequelで書くとこんな感じ。テーブル名、カラム名とかの説明は面倒なので省かせてください。

@statuses.filter { |o| o.created_at > Time.now - @target_period }
         .group_and_count(:video_id.as(:video_id))
         .order(:count.desc, :video_id.desc)
         .limit(@max_video_count)

"group_and_count"メソッドが便利ですね。個人的に結構わかりやすく書けるのでお気に入りです、Sequel。

各動画につける最近のツイートは次のような感じで取り出しています。

@statuses.graph(:users, :id => :user_id)
         .filter { |o| o.created_at > Time.now - @target_period }
         .where(:video_id => video_id)
         .order(:created_at.desc)
         .limit(@max_tweet_count)

JOINする双方のテーブルに同名のカラムがある場合、"join"メソッドではなく"graph"メソッドを使うと、結果がテーブル毎のハッシュとして返ってきますので、カラム名の重複を回避することができます。

動画情報の取得

ランキングとして表示するからには、動画の名前やサムネイルなど、色々な情報が必要になってきます。

この辺はニコニコ動画APIとしてきちんと用意されていますので、これを使わせてもらいます。

とりあえず"getthumbinfo" APIを使っておけば、通常必要となる情報は全部取得できます。

今の実装では、トップページの「最新のツイート」を一分ごとに更新していますので、そのままだとかなり頻繁にAPIへアクセスすることになってしまいます。

そこで、取ってきた情報はmemcachedにも保存しておき、memcachedに情報がある場合はAPIへはアクセスしないようにしています。

こういう「とりあえず入れておくだけ」なデータにはリレーショナルデータベースは大げさになってしまいますので、この辺のkey/value式のデータストアが重宝します。

ちなみに、動画情報にはある程度の新鮮さを持たせておきたいので、memcachedに入れたデータも30分経ったら捨てるようにしています。

モジュールの可動場所

サービスのURLはさくらのレンタルサーバですが、ここには静的なHTMLと各種リソースしか置いてありません。上で説明したようなスクリプトは全て自宅で動かしています。

さくらのレンタルサーバではStreaming APIで使うような長寿命のプロセスは許可されていませんし、かといって自宅で固定IPをとってサービスを公開するのもちょっと億劫です。…ということで、こんな構成になりました。

自宅サーバではStreaming APIによるトラッキングの他に、cronで定期的にランキングHTML生成プロセスを動かしており、そいつがさくらサーバ上にアップロードするようになっています。

ちょっと面倒な構成ではありますが、さくらサーバ側の負荷も減りますし、まぁそんなに悪くもないのかな…なんて思っています。

そんなわけで、自宅サーバの能力はフルに使えますので、本当はやろうと思えば1分毎に毎時ランキングを更新する、といったこともできるのですが、それは果たして“毎時ランキング”なのか?という思いもあり、今のような1時間毎の更新としています。

この辺、他の方のご意見も伺いたいところです。

テンプレート

前述の通りテンプレートエンジンはHamlを使っていますが、フォーマットはHTML5で書いています。

といっても新しいAPIとかと使っているわけではなく、タグがHTML5的なだけですが。そのおかげでIEではJavaScriptを無効にしているとレイアウトがガタガタになります。

しかもW3Cのバリデータに通すとエラーになります。どうもHTML5ではmetaタグのhttp-equiv属性に"pragma"や"cache-control"などが使えない*1らしいのですが、さくらサーバでは.htaccessでのキャッシュコントロールはできませんし、仕方なくエラーとなることを承知でそのままにしてあります。

あとはCSS3のborder-radiusとかgradientとかもちょっと使ってみたり。Firefox, Safari, Chromeあたりでしか反映されませんが、その辺のブラウザで見るとちょこっと見た目が変わります。元々のデザインが味気ないので、たいした違いはありませんが。

今後について

Twitterで軽く確認してみましたが、どうやら今の時点では被っているサービスはないようなので*2、もう少し発展させてみようかと思っています。

せっかくStreaming APIを使ってリアルタイムにツイートが取れているのですから、それを活かした機能が欲しいところです。

ニコ動風のUIで、動画のサムネをバックに、取ってきたツイートをリアルタイムで*3流す…みたいなものも考えたのですが、そんなのあまり見ない気がして。どうでしょうね。どちらかというとニコ生風ってことになるのでしょうか。

bit.lyなど、他のURL短縮サービスにも対応できないかと考えましたが、いい案が浮かばないので実現できていません。良い策をご存じの方がいらっしゃいましたらご教示お願い致します。

あとはランキングの更新間隔とか、動画やツイートの表示数とか、細かい部分もあるのですが、なんといってもまずなんとかしたいのはデザインです。

自分デザインセンスは本当に無いもので…。もうちょっと見栄えの良いデザインにしたいなぁ。

そんな感じで、色々迷っているところですので、ご意見ご要望などありましたら是非@shibasonまでご連絡ください。よろしくお願い致します。

あ、あと、名前も募集しています!

*1:まだ定義されていない?

*2:それが一番心配でした。

*3:前述のようにちょっと変わった構成なので、「擬似リアルタイム」的なことしかできないかもしれませんが。

D510MOでVMware ESXi 4.0をインストールするには

自宅のミニサーバのマザーボードをD945GCLF2からD510MOに換装しました。

そのまま既存のCentOS + VMware Server構成で運用を続けても良かったのですが、せっかくなので前から気になっていたVMware ESXiを入れてみることにしました。

ただし、VMware ESXiはD510MOに搭載されているRealtek製のNICに対応していませんので、インストールにあたってちょっとした作業が必要になります。

その辺に関してはありがたい事にWeb上に多くの情報がありました。いくつかの方法があるようでしたので、備忘録として私がとった手順も書きとめておこうと思います。

今回は

  • D510MOマシンには光学ドライブがついていない
  • 使うメディアはUSBメモリだけで済ませたい
  • ただしUSBブートではなくHDDブートにしたい
  • 既存のLinux環境はない*1

という前提で作業を行いました。

なお、以下の手順はVMware ESXi 4.0 Update 1を対象としています。

インストール手順

USBメモリを用意する

FAT32でフォーマットされた、容量1GB以上のUSBメモリが一本あればOKです。クライアントPCに挿しておきます。

UNetbootinをダウンロードする

UNetbootinはISOイメージから簡単にブータブルUSBメモリを作成できる便利ソフトです。トップページ上部にダウンロードボタンがありますので、Windows用を落としてきます。

特にインストールの必要はなく、落とした実行ファイルをそのまま起動しますので、適当な場所に置いておきます。

ESXiのインストーラブルISOイメージをダウンロードする

VMwareのサイトからダウンロードしてきます。無料ですが、ユーザ登録が必要です。

インストーラUSBメモリを作成する

先ほど落としてきたUNetbootinを起動し、

  • Diskimage > ISO > (ISOイメージファイル)
  • Type > USB Drive
  • Drive > (USBメモリのドライブ)

を選択してOKをクリックします。完了すると再起動するか問われますが、再起動せずに閉じます。

インストーラUSBメモリoem.tgzを追加する

これでもうUSBメモリからインストーラを起動できますが、そのままだと起動途中に"No compatible network adapter found."と言われて止まってしまいます。

有志の方々の手によってESXi用の各種デバイスドライバが作成されていますので、それを使うことでESXiにNICを認識させることができます。

まず、下記のページから"RTL8111_8168_P55_integr_SATA_Ctrl.(AHCI).oem.tgz"をダウンロードします。

そのファイルをUSBメモリのルートフォルダ*2にコピーし、"oem.tgz"にリネームします。

最後に同じフォルダにある"syslinux.cfg"をエディタで開き、

append initrd=/ubninit vmkboot.gz --- vmkernel.gz --- sys.vgz --- cim.vgz --- ienviron.tgz --- image.tgz --- install.tgz

の行の最後に" --- oem.tgz"を付け加えて保存します。

append initrd=/ubninit vmkboot.gz --- vmkernel.gz --- sys.vgz --- cim.vgz --- ienviron.tgz --- image.tgz --- install.tgz --- oem.tgz

同じ行が2行ありますので、2行とも変更しておきます。

ESXiをインストールする

これでインストーラは完成ですので、クライアントPCからUSBメモリを抜き、D510MOマシンに挿して起動します。BIOSのブートメニューでUSBメモリからのブートを選択するのを忘れずに。

ブートローダーの画面で"ESXi Installer"を選択するとインストーラが起動します。やや時間がかかりますが、しばらく待っていると入力待ちの画面が出てきますので、あとは画面の表示通りにインストールを進めます。

注意点として、インストーラPS/2接続のキーボードを認識してくれませんでしたので、USB接続のキーボードを使う必要があります。

再起動を促す画面が出てくれば、めでたくインストール完了…とはいかず、もう少し作業が必要になります。再起動しても例によって"No compatible network adapter found."で止まりますので、USBメモリを抜き、しばらくそのまま置いておきます。

USBブートのCentOSを作成する

実は先ほど追加した"oem.tgz"はあくまでも「インストーラが読み込む用」なので、今インストールしたESXiは依然としてRealtekNICを認識してくれない状態になっています。ESXi本体に再度oem.tgzの追加が必要になります。

そのために、インストーラUSBメモリを今度はCentOS化して、D510MOマシン上でCentOSをUSBブートして作業を行うことにします。*3 *4

USBメモリを再度フォーマットしたら、CentOSのサイトからLiveCD版のISOファイルをダウンロードし、先ほどと同じようにUNetbootinを使ってUSBメモリにインストールします。

インストールが完了したら、USBメモリのルートフォルダに"RTL8111_8168_P55_integr_SATA_Ctrl.(AHCI).oem.tgz"をコピーして、クライアントPCから抜きます。

ESXiにoem.tgzを追加する

再起動待ちのD510MOマシンにUSBメモリを挿して再起動し、USBブートでCentOSを起動します。

CentOSが起動したら、ESXiをインストールしたHDDのパーティションがマウントされていることを確認します。通常は自動でマウントされていますが、されていなければfdiskでデバイス名を確認してマウントします。

# mount -l
...(省略)...
/dev/sda4 on /mnt/disc/sda4 type vfat (ro,uid=500) [Hypervisor0]
/dev/sda5 on /mnt/disc/sda5 type vfat (ro,uid=500) [Hypervisor1]
/dev/sda6 on /mnt/disc/sda6 type vfat (ro,uid=500) [Hypervisor2]
/dev/sda8 on /mnt/disc/sda8 type vfat (ro,uid=500) [Hypervisor3]
...(省略)...

ターゲットは"Hypervisor1"のボリュームラベルがついているパーティションです。私の場合は/dev/sda5で、既に/mnt/disc/sda5にマウントされていました。違うデバイス名だった場合は適宜読み替えてください。

この中に"oem.tgz"が入っていると思います。

# ls /mnt/disc/sda5
boot.cfg    cim.vgz      mod.tgz  pkgdb.tgz  vmkboot.gz
cimstg.tgz  license.tgz  oem.tgz  sys.vgz    vmk.gz

中身を見れば分かりますが、ただの空tgzファイルなので、これを先ほどUSBメモリにコピーした"RTL8111_8168_P55_integr_SATA_Ctrl.(AHCI).oem.tgz"に置き換えます。

読み取り専用でマウントされていますので、読み書きモードでマウントし直したあと、ファイルをコピーします。なお、私の環境ではコピー元のUSBメモリは/mnt/disc/sdb1としてマウントされていましたが、こちらも適宜読み替えてください。

# umount /mnt/disc/sda5
# mount /dev/sda5 /mnt/disc/sda5
# cp -p /mnt/disc/sdb1/RTL8111_8168_P55_integr_SATA_Ctrl.\(AHCI\).oem.tgz /mnt/disc/sda5/oem.tgz

これでCentOSでの作業は終わりです。再起動をかけてUSBメモリを取り外します。

インストール完了

再起動後、通常通りHDDからブートすれば、無事ESXiが立ち上がると思います。めでたしめでたし。

ESXi自体の設定や使い方については、以下の参考サイトの下3つなどをどうぞ。

*1:D945GCLF2時代に使っていたCentOS環境は既に潰してしまっていました。

*2:image.tgzなどがあるフォルダ。

*3:本当はUbuntuを使うつもりだったのですが、D510MOでUbuntuを起動するにはBIOSのアップデートが必要になるようなので、手順の省略のためにCentOSを選択しました。参考:Intel 新 Atom MB (D510MO) で Ubuntu 9.10 を使うには - metastable blog

*4:インストーラに含まれるDDイメージを直接編集することもできます。その場合はインストール直後からESXiを使うことができます。参考:Updating the VMWare ESXi Disk Dump File | Grid.org