SSブログ
プログラム三昧 ブログトップ
前の20件 | -

JavaScript でリサジュー [プログラム三昧]このエントリーを含むはてなブックマーク#

お久しぶりでございます。 休載していた間に JavaScript がずいぶん変わってしまったみたいで。 ためしに JavaScript でリサージュ(リサジュー)を描いてみよう。


onclik が無くなって、 var が無くなって、かなり変わりましたね。

くわしく


nice!(0)  コメント(0)  このエントリーを含むはてなブックマーク#

scilab で、 LFSR の実験 [プログラム三昧]このエントリーを含むはてなブックマーク#

Scilab 5.5.2

Linear Feedback Shift Register (LFSR) って、ご存知ですか? シフトレジスタにちょっとしたフィードバック論理を加えて、ビット列を出力させる仕組みの事です。 この記事では、 scilab で LFSR のシミュレーションを行います。 scilab を使うのも、久々だねぇ。

LFSR って、何だろう

シフトレジスタ

フリップフロップを何段も接続したものをシフトレジスタと呼びます。 シフトレジスタは、 D から入ってきた入力が、その段数分のクロックを与えると出力に出てきます。


フィードバック付きシフトレジスタ

ここで、個々のフリップフロップの出力にある演算をほどこした結果を D 入力に与えてやると、シフトレジスタの出力からは、自動的にビット列が出力されます。

このとき、演算式を工夫すると、疑似乱数と呼ばれる出力を得られるようになります。 ここでは、 scilab を使って、疑似乱数の生成と解析を行います。

LFSR のシミュレーション

ガロアの LFSR 実装

LFSR の実現方法には、フィボナッチとガロアの二種類の実装が知られています。 ここでは、ガロアの実装にしたがって、実装を行います。 XOR 演算を使用してフィードバックを実装します。

ガロアの実装を表記するためにタップの位置をリストしたものを使います。 例えば、このような配列で表記します。


6 段 LFSR の実装例

配列の最初がシフトレジスタの段数に相当します。 残りは、 XOR 演算を取り入れるタップ位置を表現しています。 これをそのまま scilab の配列で表現して、フィードバック論理を作成します。

Galois_tap=[6 5];
lfsr_stage=Galois_tap(1);
lfsr_period=2**lfsr_stage-1;
lfsr_mask=zeros(1,lfsr_stage-1);
for i=2:size(Galois_tap,2);
  lfsr_mask(Galois_tap(i))=1;
end

これは、6段 LFSR 実装の例です。 タップリスト Galois_tap の第一要素は、シフトレジスタの段数 lfsr_stage を表しています。 この段数の LFSR で作成可能な疑似乱数の周期を lfsr_period で表します。 次に、フィードバックの演算で使用するタップの位置を lfsr_mask 配列で表現します。 Galois_tap で指定された位置に '1' を立てています。 この例では、 lfsr_mask=[0 0 0 0 1] となります。

lfsr_sr=ones(1,lfsr_stage);
lfsr=zeros(1,lfsr_period);
for i=1:lfsr_period;
  bit0=lfsr_sr(lfsr_stage);
  lfsr_sr=[bit0,bitxor(lfsr_sr(1:lfsr_stage-1),lfsr_mask*bit0)];
  lfsr(i)=bit0;
end

lfsr_mask を使って、ビット列を生成します。 シフトレジスタは、 lfsr_sr 配列に配置されます。 ビット列は、 lfsr 配列に出てきます。

scf(1);clf;plot((0:lfsr_period-1),lfsr)
LFSR の波形

出来上がったビット列をグラフ表示すると、このようになります。 LFSR で作成される疑似乱数は、その特徴として、1の数が 2**(lfsr_stage-1) となります。 この例では、 sum(lsfr)=32 となります。

y=abs(fft(lfsr));
scf(2);clf;plot((0:lfsr_period-1),y)
LFSR の周波数特性

さらに、LFSR の周波数特性を確認するために fft で解析しました。 すると、このように、 DC 成分以外は、平坦な特性になることがわかります。

DC 成分が 31 なので、 AC 部分はその1/8 (-18dB) になっています。

PWM で実装すると

pwm=zeros(1:lfsr_period);
for i=1:lfsr_period;
  pwm(i)=int(modulo(i+1,4)/2);
end
scf(1);clf;plot((0:lfsr_period-1),pwm)
PWM による実装

同じパルス数の波形を一定周期の PWM で実現する事もできます。 ある一定の数のパルスを得たいのであれば、これでも十分です。 しかし、周波数特性は大きく違います。

y=abs(fft(pwm));
scf(2);clf;plot((0:lfsr_period-1),y)
PWM の周波数特性

このように、ある特定の周波数で大きなエネルギーが出ている事がわかります。 この信号が原因でノイズが出る場合、突出してエネルギーが出ている特定周波数でのノイズを減らさなくてはなりません。 疑似乱数を使うと、ノイズが各周波数にまんべんなく広がり、エネルギーの突出が無くなります。 このため、ノイズ対策も行いやすくなります。

オーバーサンプリングしてみたら

oversample=16;
n_sample=lfsr_period*oversample;
s=zeros(1,n_sample);
k=1;
for i=1:lfsr_period;
  for j=1:oversample;
    s(k)=lfsr(i);
    k=k+1;
  end
end
scf(1);clf;plot((0:n_sample-1)/oversample,s)
オーバーサンプルした LFSR

上で作った LFSR 波形は、インパルスを使って表現していました。 実際には、各パルスが幅を持つことになります。 そこで、16倍のオーバーサンプルを行ってみました。

y=20*log10(max(abs(fft(s)/n_sample),ones(s)*1e-5));
scf(2);clf;plot((0:n_sample-1)/lfsr_period,y)
オーバーサンプルした LFSR の周波数特性

同様に、周波数特性を計算させました。 このグラフの縦軸は、 DC 成分を 1/2 (-6dB) としたデシベル表示で表現しています。 また、 log10() 関数でエラーが起こらないように、 -100dB を下限とする制限も設けています。

見た所、これまでとは少々異なっている様子です。 各周波数に広くエネルギーが分散しているのは同じですが、低い周波数の方がエネルギーが高く見えています。 LFSR を単体で解析した時には、すべての AC 成分が -24dB にそろっていたのですが、この解析結果では、 AC 成分の最大値が -24dB になっており、他の成分はすべて -24dB を下回っています。 また、 LFSR のクロック周波数の幅で、山が出来ています。

関連文献

Scilabで学ぶシステム制御の基礎

Scilabで学ぶシステム制御の基礎

  • 作者: 橋本 洋志
  • 出版社/メーカー: オーム社
  • 発売日: 2007/04
  • メディア: 単行本

ARM アセンブラで 32ビット定数をロードする [プログラム三昧]このエントリーを含むはてなブックマーク#

ARM Cortex-M プロセッサでは、もちろんアセンブラでプログラムを書くことが出来ます。 基本的な動作として、32ビットの定数をレジスタに格納する操作について調べてみました。

LDR Rd,=const という疑似命令

LDR Rd,=const 構文

32ビットの定数をレジスタに格納する場合、アセンブラで使用される "LDR Rd,=const" という構文があります。 この構文は、疑似命令として解釈され、適切な別の命令に変換されます。 詳細は、 LDR Rd, =const を使用したイミディエート値のロード に書いてあります。 これによると、変換できるものは MOV, MOVN 命令に変換され、それ以外は PC 相対アドレッシングでメモリアクセスを行うのだそうです。 どんな風に変換されるのか、試してみます。

Cortex-M4 の場合

最初は、 Cortex-M4 の場合です。 uVision 5.14 評価版を使い、いくつかの定数ロードをアセンブルしてみました。

        AREA    |.text|, CODE, READONLY

__main\
        PROC
        EXPORT  __main
            
        LDR     R0,=0x000000FF
        LDR     R0,=0x01540000
        LDR     R0,=0xAAAAAAAA
        LDR     R0,=0x00550055
        LDR     R0,=0xAA00AA00
        LDR     R0,=0xFFF66FFF
        LDR     R0,=0x0000FFFF
        LDR     R0,=0x12341234
        LDR     R0,=0x12345678

        B       .
        ENDP
            
        END

MOV 命令または MOVN 命令に変換できる条件は、MOV および MVN を使用したイミディエート値のロードに書いてあります。

0x0 ~ 0xFF(0 ~ 255)の範囲内にある任意の 8 ビットイミディエート値。
符号なし8ビットの値は、そのままインストラクションに組み込まれます。
     7:         LDR     R0,=0x000000FF 
0x000000C0 F04F00FF  MOV      r0,#0xFF
任意のビット数だけ左シフトした任意の 8 ビットイミディエート値。
8ビットの値をシフトした値も、インストラクションに組み込まれます。 たとえば、 0x55 を18ビットシフトした値 0x01540000 がロードできます。
     8:         LDR     R0,=0x01540000 
0x000000C4 F04F70AA  MOV      r0,#0x1540000
レジスタのすべての 4 バイトに対して複製した任意の 8 ビットパターン。
4バイト(32ビット)の各バイトに8ビットのパターンを入れた値も、インストラクションに組み込まれます。 たとえば、すべてのバイトに 0xAA が入った 0xAAAAAAAA がロードできます。
     9:         LDR     R0,=0xAAAAAAAA 
0x000000C8 F04F30AA  MOV      r0,#0xAAAAAAAA
バイト 1 と 3 がゼロに設定されているときに、バイト 0 と 2 に対して複製した任意の 8 ビットパターン。
バイト 1 と 3 が 0x00 で、バイト 0 と 2 に同じ値が入った値も、インストラクションに組み込まれます。 たとえば、バイト 3:2 と 1:0 に16ビットの 0x0055 が入った 0x00550055 がロードできます。
    10:         LDR     R0,=0x00550055 
0x000000CC F04F1055  MOV      r0,#0x550055
バイト 0 と 2 がゼロに設定されているときに、バイト 1 と 3 に対して複製した任意の 8 ビットパターン。
同様に、バイト 0 と 2 が 0x00 で、バイト 1 と 3 に同じ値が入った値も、インストラクションに組み込まれます。 たとえば、バイト 3:2 と 1:0 に16ビットの 0xAA00 が入った 0xAA00AA00 がロードできます。
    11:         LDR     R0,=0xAA00AA00 
0x000000D0 F04F20AA  MOV      r0,#0xAA00AA00
32 ビットの MVN 命令では、これらの値のビット単位の補数をロードできます。その数値は -(n+1) です。 ここで、 n は MOV で使用できる値です。
上記の1の補数(符号付き数値では負の値)を扱うことが出来ます。 たとえば、 0x00099000 の補数である 0xFFF66FFF をロードできます。
    12:         LDR     R0,=0xFFF66FFF 
0x000000D4 F46F2019  MVN      r0,#0x99000
32 ビットの MOV 命令では、0x0 ~ 0xFFFF(0 ~ 65535)の範囲内にある任意の 16 ビットの数値をロードできます。
16ビットの値であれば、そのままインストラクションに組み込まれます。 この時のニーモニックは MOVW と表現されます。
    13:         LDR     R0,=0x0000FFFF 
0x000000D8 F64F70FF  MOVW     r0,#0xFFFF
その他の値
その他の値は、32ビットのインストラクションに組み込まれることなく、 PC 相対アドレッシングでリテラルプールと呼ばれるメモリ領域に配置された32ビットの値を読み込む命令に変換されます。
    14:         LDR     R0,=0x12341234 
0x000000DC 4801      LDR      r0,[pc,#4]  ; @0x000000E4
    15:         LDR     R0,=0x12345678 
0x000000DE 4802      LDR      r0,[pc,#8]  ; @0x000000E8
:
:
0x000000E4 1234      DCW      0x1234
0x000000E6 1234      DCW      0x1234
0x000000E8 5678      DCW      0x5678
0x000000EA 1234      DCW      0x1234

Cortex-M3 の場合

次は、ターゲットを Coretex-M3 に変更して実験しました。

     7:         LDR     R0,=0x000000FF 
0x000000C0 F04F00FF  MOV      r0,#0xFF
     8:         LDR     R0,=0x01540000 
0x000000C4 F04F70AA  MOV      r0,#0x1540000
     9:         LDR     R0,=0xAAAAAAAA 
0x000000C8 F04F30AA  MOV      r0,#0xAAAAAAAA
    10:         LDR     R0,=0x00550055 
0x000000CC F04F1055  MOV      r0,#0x550055
    11:         LDR     R0,=0xAA00AA00 
0x000000D0 F04F20AA  MOV      r0,#0xAA00AA00
    12:         LDR     R0,=0xFFF66FFF 
0x000000D4 F46F2019  MVN      r0,#0x99000
    13:         LDR     R0,=0x0000FFFF 
0x000000D8 F64F70FF  MOVW     r0,#0xFFFF
    14:         LDR     R0,=0x12341234 
0x000000DC 4801      LDR      r0,[pc,#4]  ; @0x000000E4
    15:         LDR     R0,=0x12345678 
0x000000DE 4802      LDR      r0,[pc,#8]  ; @0x000000E8
:
:
0x000000E4 1234      DCW      0x1234
0x000000E6 1234      DCW      0x1234
0x000000E8 5678      DCW      0x5678
0x000000EA 1234      DCW      0x1234

その結果、 Cortex-M4 と同じ結果となりました。 この範囲では、 Cortex-M4Cortex-M3 には、違いが無いようです。

Cortex-M0 の場合

次は、 Cortex-M0 で試してみました。結果は、以下の通りです。

     7:         LDR     R0,=0x000000FF 
0x000000C0 4804      LDR      r0,[pc,#16]  ; @0x000000D4
     8:         LDR     R0,=0x01540000 
0x000000C2 4805      LDR      r0,[pc,#20]  ; @0x000000D8
     9:         LDR     R0,=0xAAAAAAAA 
0x000000C4 4805      LDR      r0,[pc,#20]  ; @0x000000DC
    10:         LDR     R0,=0x00550055 
0x000000C6 4806      LDR      r0,[pc,#24]  ; @0x000000E0
    11:         LDR     R0,=0xAA00AA00 
0x000000C8 4806      LDR      r0,[pc,#24]  ; @0x000000E4
    12:         LDR     R0,=0xFFF66FFF 
0x000000CA 4807      LDR      r0,[pc,#28]  ; @0x000000E8
    13:         LDR     R0,=0x0000FFFF 
0x000000CC 4807      LDR      r0,[pc,#28]  ; @0x000000EC
    14:         LDR     R0,=0x12341234 
0x000000CE 4808      LDR      r0,[pc,#32]  ; @0x000000F0
    15:         LDR     R0,=0x12345678 
    16:  
0x000000D0 4808      LDR      r0,[pc,#32]  ; @0x000000F4
    17:         B       . 
0x000000D2 E7FE      B        0x000000D2
0x000000D4 00FF      DCW      0x00FF
0x000000D6 0000      DCW      0x0000
0x000000D8 0000      DCW      0x0000
0x000000DA 0154      DCW      0x0154
0x000000DC AAAA      DCW      0xAAAA
0x000000DE AAAA      DCW      0xAAAA
0x000000E0 0055      DCW      0x0055
0x000000E2 0055      DCW      0x0055
0x000000E4 AA00      DCW      0xAA00
0x000000E6 AA00      DCW      0xAA00
0x000000E8 6FFF      DCW      0x6FFF
0x000000EA FFF6      DCW      0xFFF6
0x000000EC FFFF      DCW      0xFFFF
0x000000EE 0000      DCW      0x0000
0x000000F0 1234      DCW      0x1234
0x000000F2 1234      DCW      0x1234
0x000000F4 5678      DCW      0x5678
0x000000F6 1234      DCW      0x1234

全ての定数ロードが PC 相対アドレッシングに変換されました。 Cortex-M0 では、32ビット MOV 命令が使用できないので、いずれもメモリアクセスに変換されてしまいました。

Thumb Mode を使ったら

Thumb Mode を使うオプション

しかしながら、 Cortex-M0 には、 Thumb Mode と呼ばれている命令体系にインストラクションに値を組み込んだものが存在します。 アセンブラで Thumb Mode を使うには、メニューアイテムの Project → Options... で、 Asm タブの "Thumb Mode" をチェックします。

     7:         LDR     R0,=0x000000FF 
0x000000C0 20FF      MOVS     r0,#0xFF
     8:         LDR     R0,=0x01540000 
0x000000C2 4804      LDR      r0,[pc,#16]  ; @0x000000D4
     9:         LDR     R0,=0xAAAAAAAA 
0x000000C4 4804      LDR      r0,[pc,#16]  ; @0x000000D8
    10:         LDR     R0,=0x00550055 
0x000000C6 4805      LDR      r0,[pc,#20]  ; @0x000000DC
    11:         LDR     R0,=0xAA00AA00 
0x000000C8 4805      LDR      r0,[pc,#20]  ; @0x000000E0
    12:         LDR     R0,=0xFFF66FFF 
0x000000CA 4806      LDR      r0,[pc,#24]  ; @0x000000E4
    13:         LDR     R0,=0x0000FFFF 
0x000000CC 4806      LDR      r0,[pc,#24]  ; @0x000000E8
    14:         LDR     R0,=0x12341234 
0x000000CE 4807      LDR      r0,[pc,#28]  ; @0x000000EC
    15:         LDR     R0,=0x12345678 
    16:  
0x000000D0 4807      LDR      r0,[pc,#28]  ; @0x000000F0
    17:         B       . 
0x000000D2 E7FE      B        0x000000D2
0x000000D4 0000      DCW      0x0000
0x000000D6 0154      DCW      0x0154
0x000000D8 AAAA      DCW      0xAAAA
0x000000DA AAAA      DCW      0xAAAA
0x000000DC 0055      DCW      0x0055
0x000000DE 0055      DCW      0x0055
0x000000E0 AA00      DCW      0xAA00
0x000000E2 AA00      DCW      0xAA00
0x000000E4 6FFF      DCW      0x6FFF
0x000000E6 FFF6      DCW      0xFFF6
0x000000E8 FFFF      DCW      0xFFFF
0x000000EA 0000      DCW      0x0000
0x000000EC 1234      DCW      0x1234
0x000000EE 1234      DCW      0x1234
0x000000F0 5678      DCW      0x5678
0x000000F2 1234      DCW      0x1234

すると、8ビットのデータを扱った場合に限り、インストラクションに値を組み込んだ MOVS という命令を使いました。 これで、高いメモリアクセスコストを支払わなくて済みます。

参考文書

LDR Rd, =const を使用したイミディエート値のロード
LDR 疑似命令について書かれたページです。
MOV および MVN を使用したイミディエート値のロード
MOV でインストラクションに組み込む事の出来る値について書かれたページです。

eclipse GALILEO に EGit をインストールする。 [プログラム三昧]このエントリーを含むはてなブックマーク#

eclipse GALILEO

FX3 のソフトウェア開発キット (Software Development Kit; SDK) には、 eclipse が同梱されています。 せっかくソフトウェアを開発するのなら、どこかにリポジトリを立てて、共有しましょう。 リポジトリは、最近はやりの github を使ってみたいな。

github を使うには、 git クライアントが必要

FX3 SDK に同梱されている eclipse GALILEO には、 CVS クライアントがインストールされています。 そのため、そのままで、 CVS サーバのリポジトリに対してデータのやり取りを行うことができます。

ところが、この eclipse には、 git クライアントはインストールされていません。 必要であれば、自分でインストールする必要があります。 もちろん、 eclipse の他に git クライアントをインストールすれば、使えないことはありませんが、せっかくの統合開発環境ですから、 eclipse にインストールして使いたいところです。

eclipse 向けの git クライアントとして、 EGit というのが、あるので、これをインストールしてみます。

インストールするソフトウェアの場所を設定する

Install New Software...

まず、最初に、 EGit が格納されている場所を指定します。 新たにソフトウェアをインストールするときには、メニューバーから "Help" → "Install New Software..." を選択します。

すると、ダイアログボックス "Install" が開きます。


Install ダイアログ

"Work with:" テキストフィールドの隣にある "Add..." ボタンをクリックします。 すると、さらに "Add Site" ダイアログボックスが開きます。


Add Site ダイアログ

このダイアログボックスでは、 EGit が格納されているサイトの場所 "Location" と、その識別名 "Name" を指定します。 ここでは、 "Name" に "Egit" を、 "Location" に "http://download.eclipse.org/egit/updates" を指定します。 そして、 "OK" ボタンをクリックして、確定させます。

必要なソフトウェアをインストールする

必要なソフトウェアを選択する

"Add Site" ダイアログボックスが閉じたら、 "Install" ダイアログには、指定したサイトからインストールできるソフトウェアの一覧が表示されます。 今回、必要なソフトウェアは、 "Eclipse EGit" と "EGit Project Set Support" の二つです。 いずれも、 "Eclipse Git Team Provider" の中に入っていますので、それぞれをチェックします。

ここで、ついでだからと、全てのソフトウェアにチェックを入れると、それらが必要としているソフトウェアが芋づる式に増えていきます。 ここでは、必要最低限のソフトウェアをインストールして、後の手間を減らしています。


ソフトウェアを確定

必要なソフトウェアを選んだら、 "Install" ダイアログボックスの "Next>" ボタンをクリックして、次の画面に移行します。


インストールの最終確認

すると、インストールすべく選ばれたソフトウェアの一覧が表示されます。 ここで、 "Finish" ボタンをクリックすると、インストールが始まります。

インストール後の再起動

再起動しますか

インストール後には、このようなダイアログが表示されて、 eclipse を再起動するように勧めます。 ここでは、お勧めに従って再起動すべく、 "Yes" ボタンをクリックして、 eclipse が再起動されるのを待ちます。


http://yfrog.com/ebqp4p

eclipse が再起動したら、 git インターフェイスが使用できるようになっています。 git の機能は、 "Project Explorer" のコンテキストメニューから、使用することができるようになっています。

RSA キーの場所を指定する

Preference メニュー

git のリポジトリに使おうとしている github は、 SSH2 の機能を利用して、ファイルのやり取りをしています。 SSH2 には、暗号に使われる秘密鍵と公開鍵が必要です。 これらの鍵の置き場所を指定するには、メニューバーの "Window" → "Preference" を選択し、 "Preferences" ダイアログボックスを開きます。


鍵のありかを指定する

鍵の場所を指定するためには、 "Preferences" ダイアログボックスで、 "General" → "Network Connection" → "SSH2" を選択して、さらに "General" タブをクリックして、設定画面を呼び出します。

鍵のありかは、 "SSH2 home:" テキストフィールドで指定します。 デフォルトの状態では、ホームディレクトリの "ssh" ディレクトリが選択されていますが、今回は、ほかの git クライアントも使っている ".ssh" を指定します。 これで、鍵の置き場所が確保できましたので、公開鍵を github の設定画面で指定すると、リポジトリへのアクセスが簡単になります。

さいごに

ここで述べた手順は、新しい FX3 SDK が発行されるたびに必要で、リリースされるたびに行う作業となっています。 そのため、記録しておく必要に迫られたというのが、この文書の目的の一つです。 FX3 SDKEGit を使いたいという方のお役に立てれば、幸いです。

関連文献

関連文献を並べてみましたが、私自身は、 WEB の情報をたよりに使っているので、文献を参照してはいません。 ほんの、ご参考までに。

Gitによるバージョン管理

Gitによるバージョン管理

  • 作者: 岩松 信洋
  • 出版社/メーカー: オーム社
  • 発売日: 2011/10/25
  • メディア: 単行本(ソフトカバー)

Quartus II を Ubuntu 10.04 にインストールするお話 [プログラム三昧]このエントリーを含むはてなブックマーク#

WS001011-800.png

先ごろ,ALTERA社から統合開発環境である Quartus II v10.0 が公開されました.今のところ,対応するハードウェアも持っていないのですが,ためしにインストールしてみることにしました.ターゲットは, Ubuntu 10.04 です.

Ubuntu は,推奨環境に含まれていない

サポートするオペレーティングシステムを見たところ,サポートされている Linux ディストリビューションは, Red Hat, SUSE, CentOS の三つで, Ubuntu は入っていません.「サポートしません」と言われたら,やってみたくなるのが心情ですよね.

インストーラを呼び出してみた

まずは,インストーラを入手します.インストーラは,ALTERA社ダウンロードページから, Linux 版のボタンをクリックすると,ダウンロードが始まります.ダウンロードしたファイルは, /var/tmp ディレクトリに入れて,ここを作業場所とします.端末アプリケーションを開いて,インストーラを起動してみましょう.

noritan@ubuntu:~$ cd /var/tmp
noritan@ubuntu:/var/tmp$ chmod +x altera_installer.external.sh 
noritan@ubuntu:/var/tmp$ sudo ./altera_installer.external.sh
Creating directory bin
Verifying archive integrity... All good.
Uncompressing Altera Installer............................................................................................................................
Fontconfig error: "conf.d", line 1: no element found
Fontconfig warning: line 73: unknown element "cachedir"
Fontconfig warning: line 74: unknown element "cachedir"
./altera_installer_gui: symbol lookup error: /usr/lib/libXi.so.6: undefined symbol: XESetWireToEventCookie

インストーラを起動しようとしましたが,なぞのエラーが発生してインストールできません.こまりましたね.

古いライブラリを使ってインストールする

エラーの原因を探るべく, google さんにお尋ねしたところ, Ubuntu 10.04 に含まれている /usr/lib/libXi.so.6 はインターフェイスが変更されているので,このようなエラーが発生するとのことでした.解決するには,古い物を探してこなくてはならないようです.

古いファイルは, karmic 版の Ubuntu にあるこのページからダウンロードできるパッケージに含まれています.ただし, /usr/lib に配置されている libXi.so.6 を入れ替えると,他の場所で問題を起こしかねないので,パッケージごとインストールするわけにもいきません.そこで,パッケージを展開して,必要なものだけ使うことにしました.パッケージファイルを /var/tmp に保存して,以下のとおりタイプします.

noritan@ubuntu:/var/tmp$ mkdir lib
noritan@ubuntu:/var/tmp$ cd lib
noritan@ubuntu:/var/tmp/lib$ ar x ../libxi6_1.2.1-2ubuntu1_i386.deb
noritan@ubuntu:/var/tmp/lib$ tar xf data.tar.gz
noritan@ubuntu:/var/tmp/lib$ ls -l usr/lib/libXi.so.6
lrwxrwxrwx 1 noritan noritan 14 2010-07-08 22:01 usr/lib/libXi.so.6 -> libXi.so.6.0.0

以上の操作で, /var/tmp/lib/usr/lib ディレクトリにライブラリファイルが展開されます.このライブラリファイルを使用してインストーラを呼び出すには,以下のようにタイプします.

noritan@ubuntu:/var/tmp$ sudo LD_LIBRARY_PATH=/var/tmp/lib/usr/lib ./altera_installer.external.sh 
[sudo] password for noritan: 
Creating directory bin
Verifying archive integrity... All good.
Uncompressing Altera Installer............................................................................................................................
Fontconfig error: "conf.d", line 1: no element found
Fontconfig warning: line 73: unknown element "cachedir"
Fontconfig warning: line 74: unknown element "cachedir"

なんだか,エラーに警告が出てますが,まあ,いいでしょう.これで,インストーラが立ち上がりました.

インストールには,時間がかかる

インストール前の設定で,各種パラメータを要求してきます.私は,インストール先ディレクトリに /opt/altera/10.0 一時ファイル保管ディレクトリに /var/tmp を指定しました.また,インストールするオプションは, Quartus II Web Edition software (Free), ModelSim-Altera Starter Edition (Free), Nios II Embedded Design Suite, Stand-Alone Quartus II Programmer (Free) の四つです.

このインストーラ自体は, 20Mバイトほどのサイズなのですが,実際にインストールするソフトウェアは,十数Gバイトの大きさになってしまいました.この巨大なソフトウェアは,インストール時にインストーラがネットワークを介してダウンロードしてきます.ダウンロードするファイルのサイズは,数Gバイトです.そのため,インストールには,やたらと時間がかかりますし,ネットワークを切断するわけにもいきません.気長に待ちましょう.

パスを指定する

インストールされた実行ファイルは, /opt/altera/10.0/quartus/bin ディレクトリに展開されます.端末アプリケーションから quartus とタイプするだけで, Quartus II を呼び出すことができるように,このディレクトリに実行ファイルが存在することを示さなくてはなりません.そのため, ~/.bashrc ファイルを編集して,末尾行に以下の一行を加えます.

noritan@ubuntu:~$ tail -5 ~/.bashrc
if [ -f /etc/bash_completion ] && ! shopt -oq posix; then
    . /etc/bash_completion
fi

PATH=${PATH}:/opt/altera/10.0/quartus/bin

これで,端末アプリケーションから Quartus II を呼び出すことができます.メニューにも並べられるはずなのですが,さて,どうやるのかな?

参考文献

Ubuntu Magazine Japan vol.04 (アスキームック)

Ubuntu Magazine Japan vol.04 (アスキームック)

  • 作者:
  • 出版社/メーカー: アスキー・メディアワークス
  • 発売日: 2010/05/31
  • メディア: 大型本

"Twitter Friends" つくりました. [プログラム三昧]このエントリーを含むはてなブックマーク#

WS000963.png

Twitter でフォローしてきた ID .突如として他の人のつぶやきに現れた ID .「この方は,どのようなクラスタの方なのかしら.」という疑問の解決に少しでも役立てようと, "Twitter Friends" なる CGI を作成しました.


ID という個人に近い情報を表示するプログラムなので,使用例は掲載していません.ご自由にお試しください.

二つのアカウントのつながりを列挙したい

この CGI は,「自分とアノ人」などのように,二つの ID をつないでくれる ID を列挙してくれます.二つの ID の間に共通のフォロー関係が存在していれば,どのようなクラスタに属する人なのかをある程度は判断することができます.

使い方は,簡単です.二つの Textbox のそれぞれに ID を入力して, Submit ボタンをクリックします.しばらくすると, follow/follower 関係ごとに二つの ID ともにつながりのある ID が表示されます.

参考文献

Python Cookbook

Python Cookbook

  • 作者:
  • 出版社/メーカー: Oreilly & Associates Inc
  • 発売日: 2005/05/05
  • メディア: ペーパーバック

続きを読む (長いよ)


Windows で tar ファイルを展開する法 [プログラム三昧]このエントリーを含むはてなブックマーク#

先日, python-twitter というモジュールを Windows で使っている Python に入れようと思い立ちました. python-twitter は, http://code.google.com/p/python-twitter/ で配布されている, PythonTwitter API を使用するためのラッパーライブラリです.

tar ファイルを展開せよ

このページの中に,インストール手順が記述されています.

  1. simplejson をインストールする

    simplejson は,私が使っている Python 2.6 には,インストールされているらしいので,OK.

  2. ライブラリファイルをダウンロードする

    リストに並んでいる, python-twitter-0.6-tar.gz をダウンロードしました.

  3. untar して, build, install で,出来上がり

    "untar" ですか? ZIP なら, Windows でも展開できますが.

というわけで, Windows で, ".tar.gz" ファイルを展開する必要ができてしまいました.

ご存じない方のために : ".tar.gz" ファイルとは何ぞや

UNIX の世界では,ディレクトリを丸ごとオープンリールの1/2インチテープに保存して,流通させている時代がありました.そのため,ディレクトリをシーケンシャル・ファイルに書き込むコマンドが用意されていました.それが, tar (tape archives) コマンドです. tar コマンドによって作られたファイルには,慣例として .tar というサフィックスが付けられており,アーカイブすること自体を tar と呼んでいます.

逆に,アーカイブ・ファイルからディレクトリを再構築する場合にも tar コマンドが使用されます.この工程のことを untar と呼んでいます.

tar されたアーカイブ・ファイルは,ディレクトリを丸ごと保存したものなので,巨大なファイルになりがちです.ファイルの大きさは,そのままテープの量に影響するので,流通させるためには,なるべく,サイズの小さなアーカイブ・ファイルに圧縮する必要があります.この圧縮の工程を行うのが, gzip というコマンドで,圧縮されたファイルには, .gz というサフィックスが付けられます.

このように, tar して gzip した状態のファイルに付けられたサフィックスが, ".tar.gz" というわけです.このファイルを展開するときには, gunzip というコマンドで,圧縮前のファイルに戻してから, untar します.

ちなみに,オープンリールテープを流通させていた時代には, compress という圧縮コマンドが使用されており,サフィックスとして, .Z が使用されていました.その時代のライブラリには, ".tar.Z" というサフィックスが良く使われていました.

横着して, untar するには

もちろん, Windows 向けの "gzip" と "tar" を探してきて,インストールすれば,ライブラリを展開することができます.でも,めんどくせ~.他に楽な方法は無いかな?

と,考えていて,ピンときました. Python 自身が, tar ファイルを展開する機能を持っていないかな?調べてみたら,やっぱり,ありましたよ. "tarfile" という名前のライブラリです.マニュアルによると, gzip で圧縮されたファイルもそのまま使えるらしい.これを使って, Python からコマンドを打ち込んで,展開しちゃいましょう.

Python で, .tar.gz を展開する

展開作業をおこなう場所を "C:\Temp" にしました.まず,このディレクトリに,ライブラリ・ファイルを配置します.そして, Python を起動し,カレントディレクトリを移動します.

PythonWin 2.6.4 (r264:75706, Nov  3 2009, 13:23:17) [MSC v.1500 32 bit (Intel)] on win32.
Portions Copyright 1994-2008 Mark Hammond - see 'Help/About PythonWin' for further copyright information.
>>> import os
>>> os.chdir("C:/Temp")

次に, untar します.

>>> import tarfile
>>> tf=tarfile.open("python-twitter-0.6.tar.gz")
>>> tf.extractall()

以上で,展開終了です.サフィックスから判断して,適切に扱ってくれました.

参考文献

Python Cookbook

Python Cookbook

  • 作者:
  • 出版社/メーカー: Oreilly & Associates Inc
  • 発売日: 2005/05/05
  • メディア: ペーパーバック

この本によると, tarfile.TarFile.open() を使うと書かれていますが,オンラインマニュアルによると, tarfile.open() の方が推奨されています.おそらく, Python のバージョンの違いによるものだと思われます.


Android SDK で,ゲームを作る (3) [プログラム三昧]このエントリーを含むはてなブックマーク#

WS000944.png

Model 部分を切り出された残りが,ユーザインターフェイスとつなぎです.

SeekNumberActivity オブジェクト

Model 部分を別ファイルに独立させたので,見通しが良くなりました.このオブジェクトは, GUI に関わる部分と, GUI と Model をつなぐインスタンスで構成されています.

/*
 * $Id$
 * --------------------------------------------
 * File         : SeekNumberActivity.java
 * Package      : org.noritan.seeknumber
 * Copyright    : Copyright (c) 2010 noritan.org
 * Organization : noritan.org
 * Created      : 2010/03/12
 * --------------------------------------------
 */
package org.noritan.seeknumber;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.GridView;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener;

/**
 * This <code>SeekNumberActivity</code> class is an <code>Activity</code>
 * of the Seek Number Game.
 * 
 * @author noritan
 */
public class SeekNumberActivity extends Activity {
    private int columnCount = 4;
    private int rowCount = 4;
    private GridView content;
    private TextView statusBar;
    private Button startButton;
    private ButtonAdapter adapter;
    private SeekNumberModel model;
    
	/** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        int buttonCount = columnCount * rowCount;
        
        // Create a model object.
        model = new SeekNumberModel(buttonCount);

        // Create an adapter to model
        adapter = new ButtonAdapter(this, buttonCount);
        
        // Set-up the content GridView
        content = (GridView) findViewById(R.id.content);
        content.setNumColumns(columnCount);
        content.setAdapter(adapter);
        content.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick (
                AdapterView<?> parent,
                View view,
                int position,
                long id
            ) {
                model.click(position);
            }
        });
        
        // Prepare a reference to the status bar.
        statusBar = (TextView) findViewById(R.id.status);
        
        // Set-up the START button.
        startButton = (Button) findViewById(R.id.start);
        startButton.setOnClickListener(new OnClickListener(){
            @Override
            public void onClick(View v) {
                model.reorder();
            }
        });
        
        // Prepare an event listener of the model.
        model.addListener(new SeekNumberListener(){
            @Override
            public void initialized(int[] assignment) {
                // Initialize the status message and grid.
                statusBar.setText("GO!!");
                adapter.initialize(assignment);
            }
            @Override
            public void hit(int position) {
                // Modify the view of hit button.
                adapter.hit(position);
            }

            @Override
            public void finished() {
                // Set a message as a status.
                statusBar.setText("Finished !!");
            }
        });
    }
    

    /**
     * This <code>ButtonAdapter</code> class is used to provide views
     * for an attached {@link GridView} object.
     * 
     * @author noritan
     */
    public class ButtonAdapter extends BaseAdapter {
        /**
         * A <code>TextView</code> array type of <code>buttonArray</code>
         * field.
         * This field have a list of buttons provided as Views
         * put on a {@link android.widget.GridView} object.
         * 
         */
        private TextView[] buttonArray;
        
        /**
         * Construct a <code>ButtonAdapter</code> object.
         * Creates and initializes buttons regarding the buttonCount
         * parameter.
         *
         * @param context a parent {@link android.widget.GridView} object including
         * views provided by this object.
         * @param buttonCount Number of buttons to be created on the
         * parent {@link android.widget.GridView} object.
         * 
         * @see android.widget.GridView
         */
        public ButtonAdapter(Context context, int buttonCount) {
            // create a list of buttons.
            buttonArray = new TextView[buttonCount];
            for (int i = 0; i < buttonCount; i++) {
                TextView button = new TextView(context);
                button.setText(String.valueOf(i+1));
                button.setGravity(Gravity.CENTER);
                button.setPadding(5, 5, 5, 5);
                buttonArray[i] = button;
            }
        }
        /**
         * Initialize the label of each buttons.
         * The label for all buttons are re-assigned regarding the
         * parameter.
         * All buttons are initializes to be visible.
         * 
         * @param assignment indicates the label assignments for each
         * buttons.
         */
        public void initialize(int[] assignment) {
            for (int i = 0; i < buttonArray.length; i++) {
                TextView button = buttonArray[i];
                button.setText(String.valueOf(assignment[i]+1));
                button.setVisibility(View.VISIBLE);
            }
            notifyDataSetInvalidated();
        }
        /**
         * Show a behavior when a button is hit.
         * The position of the hit button is specified by
         * the parameter.
         * The hit button is set to INVISIBLE.
         * 
         * @param position position of the hit button.
         */
        public void hit(int position) {
            buttonArray[position].setVisibility(View.INVISIBLE);
            notifyDataSetInvalidated();
        }
        @Override
        public int getCount() {
            return buttonArray.length;
        }
        @Override
        public Object getItem(int position) {
            return null;
        }
        @Override
        public long getItemId(int position) {
            return 0;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return buttonArray[position];
        }
    }    
}

ゲームロジックである Model を GUI に依存しないようにすることは,できましたが,さすがに Model に依存しない GUI とすることは出来ませんでした.このオブジェクトは, SeekNumberModel というオブジェクトに依存しています.

が, Model 内部の状態に関する情報を持たないように設計されていますので,ゲームのルールを変更する場合には, Model を変更するだけで対応することができます.また,見た目を変更する場合でも,この GUI オブジェクト部分だけを変更すれば良いようになっています.

レイアウト表現 : main.xml

ファイル main.xml で,レイアウトに必要なリソースを表現しています.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:gravity="center"
    >
    <TextView  
        android:id="@+id/title"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="@string/app_name"
        android:gravity="center"
        android:layout_margin="10dp"
    />
    <GridView 
        android:id="@+id/content"
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent"
        android:layout_weight="1"
        android:columnWidth="60dp"
        android:numColumns="auto_fit"
        android:verticalSpacing="10dp"
        android:horizontalSpacing="10dp"
        android:stretchMode="columnWidth"
        android:gravity="center"
        android:background="#333333"
    />
    <TextView  
        android:id="@+id/status"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text=""
        android:gravity="center"
        android:layout_margin="10dp"
    />
    <Button
        android:id="@+id/start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_margin="10dp"
        android:text="START"
    />
</LinearLayout>

次回は,GUI 部分を GridView ではなく TableView で表現するように変更してみます.


Android SDK で,ゲームを作る (2) [プログラム三昧]このエントリーを含むはてなブックマーク#

WS000944.png

前回は,ひとつのファイルに何もかもゴチャマゼに入れてしまいました.今回は, Model 部分を切りだします.

ゲームロジック(Model オブジェクト)

/*
 * $Id$
 * --------------------------------------------
 * File         : SeekNumberModel.java
 * Package      : org.noritan.seeknumber
 * Copyright    : Copyright (c) 2010 noritan.org
 * Organization : noritan.org
 * Created      : 2010/03/12
 * --------------------------------------------
 */
package org.noritan.seeknumber;

import java.util.ArrayList;

/**
 * This <code>SeekNumberModel</code> class is a game logic
 * of the Seek Number Game.
 * 
 * @author noritan
 * @see {@link SeekNumberListener}
 */
public class SeekNumberModel {
    private int buttonCount;
    private int[] indexOrdered; // position to id table
    private int expected;
    private ArrayList<SeekNumberListener> listeners = new ArrayList<SeekNumberListener>();
    
    /**
     * Construct a <code>SeekNumberModel</code> object.
     * It is assumed that the game has a number of buttons
     * indicated by the parameter <code>buttonCount</code>
     * The <code>indexOrdered</code> table and
     * the <code>expected</code> variable are initialized.  
     *
     * @param buttonCount The number of buttons to be handled by this object.
     */
    public SeekNumberModel(int buttonCount) {
        this.buttonCount = buttonCount;
        indexOrdered = new int[buttonCount];
        for (int i = 0; i < buttonCount; i++) {
            indexOrdered[i] = i;
        }
        expected = Integer.MAX_VALUE;
    }
    
    /**
     * Reorder the <code>indexOrdered</code> table with a random
     * number generator of <code>Math</code> class.
     * In addition, the <code>expected</code> variable is
     * initialized too.
     * A {@link #notifyInitialize(int[])} event is issued when
     * the table is initialized.
     */
    public void reorder() {
        ArrayList<Integer> buttonLeft = new ArrayList<Integer>();
        for (int i = 0; i < buttonCount; i++) {
            buttonLeft.add(i);
        }
        for (int i = 0; i < buttonCount; i++) {
            int k = (int)(Math.random() * (buttonCount - i));
            int index = buttonLeft.get(k);
            indexOrdered[i] = index;
            buttonLeft.remove(k);
        }
        notifyInitialize(indexOrdered);
        expected = 0;
    }
    /**
     * This method notifies a click event is occurred on a button.
     * The position of the clicked button is indicated by the
     * parameter <code>position</code>.
     * This method cause a {@link #notifyHit(int)} event when
     * the hit button is an expected one indicated by
     * the variable <code>expected</code>. 
     * Whe clicked button is the last one, an additional event
     * {@link #notifyFinish()} is issued too. 
     * 
     * @param position The position of the hit button.
     */
    public void click(int position) {
        if (expected >= buttonCount) {
            // Nothing is expected.
            return;
        }
        // Is the button an expected one ?
        if (indexOrdered[position] == expected) {
            // Right selection.
            notifyHit(position);
            // point next button to be expected.
            expected++;
            if (expected >= buttonCount) {
                // All buttons are clicked.
                notifyFinish();
            }
        }            
    }
    /**
     * Add a {@link SeekNumberListener} object as an event listener
     * of this object.
     * 
     * @param listener An event listener to accept events issued by
     * this object.
     */
    public void addListener(SeekNumberListener listener) {
        listeners.add(listener);
    }
    /**
     * Notify all event listeners that this object is initialized.
     * The {@link SeekNumberListener#initialized(int[])} method is
     * used to issue the event.
     * The parameter <code>assignment</code> is cloned prior to
     * issue the event not to modify the assignment map of this
     * object. 
     * 
     * @param assignment An assignment map from button's position
     * to the button's order.
     */
    protected void notifyInitialize(int[] assignment) {
        assignment = assignment.clone(); 
        for (SeekNumberListener listener:listeners) {
            listener.initialized(assignment);
        }
    }
    /**
     * Notify all event listeners that this object recognizes
     * an expected button is clicked at the position.
     * 
     * @param position The position of the hit button.
     */
    protected void notifyHit(int position) {
        for (SeekNumberListener listener:listeners) {
            listener.hit(position);
        }
    }
    /**
     * Notify all event listeners that this object detects the end
     * of a game when all buttons are hit.
     */
    protected void notifyFinish() {
        for (SeekNumberListener listener:listeners ) {
            listener.finished();
        }
    }
}

View に依存する部分を取り去って,ゲームロジックとして独立させました.このオブジェクトは,どのユーザ・インターフェイスにも使用することができます.

このモデルに対してメッセージを伝えるのは, public メソッド reorder() と click(int) です.それぞれ,ユーザがゲームを開始した時とユーザがボタンをクリックした時に呼び出されます.

public メソッドには,もう一つ addListener(SeekNumberListener) というものがあります.このメソッドは,モデルオブジェクトから発生られるメッセージを受け取るオブジェクト(Listener)を登録するために使用されます.

SeekNumberListener インターフェイス

/*
 * $Id$
 * --------------------------------------------
 * File         : SeekNumberListener.java
 * Package      : org.noritan.seeknumber
 * Copyright    : Copyright (c) 2010 noritan.org
 * Organization : noritan.org
 * Created      : 2010/03/12
 * --------------------------------------------
 */
package org.noritan.seeknumber;

/**
 * Objects implementing this <code>SeekNumberListener</code> interface
 * accepts events issued by a {@link SeekNumberModel}
 * class instance.
 * The <code>SeekNumberListener</code> interface is used to be
 * registered with the
 * {@link SeekNumberModel#addListener(SeekNumberListener)}
 * method.
 * 
 * @author noritan
 * @see SeekNumberModel
 * @see SeekNumberModel#addListener(SeekNumberListener)
 */
public interface SeekNumberListener {
    /**
     * Notify the listener that new labels are assigned to the buttons.
     * 
     * @param assignment An assignment map from the position of
     * a button to the label corresponding to the button.
     */
    void initialized(int[] assignment);
    /**
     * Notify the listener that a button at a <code>position</code>
     * is hit correctly.
     * 
     * @param position The position of the hit button.
     */
    void hit(int position);
    /**
     * Notify the listener that all buttons are hit and a game
     * has finished.
     */
    void finished();
}

SeekNumberListener インターフェイスは, Model オブジェクトから発せられたメッセージを受け取ります.三つのメソッド initialized(int[]) hit(int) finished() を装備しており,それぞれ,ゲームの開始を知らせる,当たりボタンが押されたことを示す,ゲームの終了を知らせる,という役割があります.

GUI 関連のコードは,後日.


Android SDK で,ゲームを作る (1) [プログラム三昧]このエントリーを含むはてなブックマーク#

WS000944.png

Android SDK を使って,ゲームを作ってみました.その昔,アプレットで作成した「数字探しゲーム」の焼き直しです.ボタンの並べ方がわからないので, GridView に TextView を並べてみました.不完全ながらも,ゲームとして機能しそうなことがわかってきたので,ここに足跡を残します.

main.xml の記述

このアプリケーションは,4個の部品を縦に配置して作られています.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:gravity="center"
    >
    <TextView  
        android:id="@+id/title"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="@string/app_name"
        android:gravity="center"
        android:layout_margin="10dp"
    />
    <GridView 
        android:id="@+id/content"
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent"
        android:layout_weight="1"
        android:columnWidth="60dp"
        android:numColumns="auto_fit"
        android:verticalSpacing="10dp"
        android:horizontalSpacing="10dp"
        android:stretchMode="columnWidth"
        android:gravity="center"
        android:background="#333333"
    />
    <TextView  
        android:id="@+id/status"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text=""
        android:gravity="center"
        android:layout_margin="10dp"
    />
    <Button
        android:id="@+id/start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_margin="10dp"
        android:text="START"
    />
</LinearLayout>

タイトル行は,ただの TextView です. content と名付けられた GridView には,制御コードでボタンを配置します. status は,現在の状態を示す TextView です.最後の start ボタンをクリックするとゲームが開始されます.

strings.xml の記述

アプリケーションで使用するリソースを strings.xml というファイルに入れて置きます.まだ,有効活用されていないもので.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Seek Number Game</string>
</resources>

これだけです.

SeekNumberActivity.java の記述

ゲームのすべてをひとつのクラスにまとめてみました.コメントも無いし,モデルとビューがゴチャゴチャになったプログラムですが,ご了承ください.

package org.noritan.seeknumber;

import java.util.ArrayList;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.GridView;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener;

public class SeekNumberActivity extends Activity {
    private int columnCount = 4;
    private int rowCount = 4;
    private GridView content;
    private TextView statusBar;
    private Button startButton;
    private ButtonAdapter buttonAdapter;
    
	/** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // Create a model object.
        buttonAdapter = new ButtonAdapter(this, columnCount, rowCount);
        
        content = (GridView) findViewById(R.id.content);
        content.setOnItemClickListener(buttonAdapter);
        content.setNumColumns(columnCount);
        content.setAdapter(buttonAdapter);
        
        statusBar = (TextView) findViewById(R.id.status);
        
        startButton = (Button) findViewById(R.id.start);
        startButton.setOnClickListener(new OnClickListener(){
            @Override
            public void onClick(View v) {
                buttonAdapter.reorder();
            }
        });
    }
    

    private class ButtonAdapter extends BaseAdapter implements OnItemClickListener {
        private int buttonCount;
        private ArrayList<TextView> buttonList;
        private ArrayList<TextView> buttonOrdered;
        private int expected = Integer.MAX_VALUE;

        public ButtonAdapter(
                Context context,
                int columnCount,
                int rowCount
            ) {
            // Save button count for future use.
            this.buttonCount = columnCount * rowCount;
            
            buttonList = new ArrayList<TextView>();
            // create and put buttons.
            for (int i = 0; i < buttonCount; i++) {
                TextView button = new TextView(context);
                button.setText(String.valueOf(i+1));
                button.setGravity(Gravity.CENTER);
                button.setPadding(5, 5, 5, 5);
                buttonList.add(button);
            }
            // Initialize ordered button list.
            buttonOrdered = new ArrayList<TextView>(buttonList);
            notifyDataSetInvalidated();
        }
        public void reorder() {
            ArrayList<TextView> buttonLeft = new ArrayList<TextView>(buttonList);
            buttonOrdered.clear();
            for (int i = buttonList.size(); i > 0; i--) {
                int k = (int)(Math.random() * i);
                TextView button = buttonLeft.get(k);
                button.setVisibility(TextView.VISIBLE);
                buttonOrdered.add(button);
                buttonLeft.remove(k);
            }
            notifyDataSetInvalidated();
            expected = 0;
            statusBar.setText("GO!!");
        }
        
        @Override
        public int getCount() {
            return buttonCount;
        }
        @Override
        public Object getItem(int position) {
            return null;
        }
        @Override
        public long getItemId(int position) {
            return 0;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return buttonOrdered.get(position);
        }
        @Override
        public void onItemClick (
                AdapterView<?> parent,
                View view,
                int position,
                long id
            ) {
            if (expected >= buttonCount) {
                // Nothing is expected.
                return;
            }
            // Select a button expected as next choice.
            TextView buttonExpected = buttonList.get(expected);
            if (view == buttonExpected) {
                // Right selection.
                // Make the right button invisible.
                buttonExpected.setVisibility(View.INVISIBLE);
                notifyDataSetInvalidated();
                // point next button to be expected.
                expected++;
                if (expected >= buttonCount) {
                    // All buttons are clicked.
                    statusBar.setText("Finished !!");
                }
            }            
        }
    }
}

START ボタンをクリックすると, TextView がランダムに配置されます.ユーザが 1 から順に TextView を押して消していって,最後まで消せたら,完了です.完了までの時間を競わせたいのだけど,タイマ機能は,まだはいっていません.

GridView に並べた部品をクリックすると「選択」してしまうので,このゲームの用途に使用するべきではないことがわかってきました.次は,他の Layout を試してみます.

参考サイト

Hello, Android
Android 版, "Hello World" は,各種 Layout オブジェクトの使い方実例まで付いています.こいつらを,片っ端から試してみりゃいいわけだ.

Android SDK で, LinearLayout の動作を確認してみた. [プログラム三昧]このエントリーを含むはてなブックマーク#

ヒマも無いのに, Android SDK に手を出してしまいました.

LinearLayout というオブジェクト

Android SDK では,画面構成を「リソース」という外部情報で規定しています.「リソース」は,XML形式で記述されます.そのため,プログラム本体を変更することなく,見た目を変更することができます.

いくつかの部品を横一列,縦一列に配置するためのコンテナとして, LenarLayout というオブジェクトが用意されています.このコンテナも,「リソース」にちょちょいと書くだけで,簡単に配置できます.

weight を指定すると子どもが大きくなる

WS000937.png

LinearLayout は,子部品を収納するためのコンテナです.収納した結果,場所が余ることもあります.そんな時には,残った場所には空白が残ります.この例では,4個の TextView 部品を縦に並べています.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <TextView
        android:text="weight=null"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />
    <TextView
        android:text="weight=null"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />
    <TextView
        android:text="weight=null"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />
    <TextView
        android:text="weight=null"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />
</LinearLayout>

WS000938.png

余った部分いっぱいに子部品を広げたいこともあるでしょう.そんな時には, weight 属性で,「どの部品に残り部分を占有させたいか.」を指定することができます.この例では,余った部分を4行目の部品に占有させるために,4行目だけに weight=1 を与えています,

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <TextView
        android:text="weight=null"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />
    <TextView
        android:text="weight=null"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />
    <TextView
        android:text="weight=null"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />
    <TextView
        android:text="weight=1"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"/>
</LinearLayout>

WS000939.png

ちなみに, weight のデフォルトは, "0" です.以降の例では, weight を明示的に指定します.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <TextView
        android:text="weight=0"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0"/>
    <TextView
        android:text="weight=0"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0"/>
    <TextView
        android:text="weight=0"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0"/>
    <TextView
        android:text="weight=1"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"/>
</LinearLayout>

weight の数値は,子供の取り分を表す

WS000940.png

weight を使って,余った部分を占有する子部品を指定することがわかりました.では,複数の子部品に weight を付けたらどうなるでしょうか.この例では,4個の部品すべてに異なる weight を指定しました.すると, weight の大きい子部品ほど広い領域を占有することができるようになりました.オンライン・マニュアルによると,占有する領域の広さは, weight に指定した値に比例するのだそうです.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <TextView
        android:text="weight=0"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0"/>
    <TextView
        android:text="weight=1"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"/>
    <TextView
        android:text="weight=2"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="2"/>
    <TextView
        android:text="weight=3"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="3"/>
</LinearLayout>

子部品が fill_parent を宣言した場合

WS000941.png

さて,ここまでは,マニュアル通りに動作することがわかりました.この時,すべての小部品には, height 属性として wrap_content が付いています.これは,「私に必要な分だけください」という意味です.そこで,すべての属性を fill_parent として,「持ってるだけ出せ」に変更してみます.まずは,すべての weight を "0" にした場合です.この場合,余った領域のすべてを子部品が分けあうのか,と思ったら,一行目の部品が独り占めしてしまいました.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <TextView
        android:text="1:weight=0"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="0"/>
    <TextView
        android:text="2:weight=0"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="0"/>
    <TextView
        android:text="3:weight=0"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="0"/>
    <TextView
        android:text="4:weight=0"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="0"/>
</LinearLayout>
WS000942.png

今度は,すべての子部品の weight を "1" にしてみました.すると,四つの子部品で余った領域を分け合うようになりました.あれ?何でこうなるの?ちなみに,すべての weight を "2" にしても,同じ結果が得られました. "0" だけが,特別なんですか?

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <TextView
        android:text="1:weight=1"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1"/>
    <TextView
        android:text="2:weight=1"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1"/>
    <TextView
        android:text="3:weight=1"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1"/>
    <TextView
        android:text="4:weight=1"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1"/>
</LinearLayout>
WS000943.png

そこで,四つの子部品に "1" から "4" までのそれぞれ別の値を与えてみました.すると, weight に与えた値が小さいほど占有する面積が大きくなり, weight が大きいものは,表示もされないという, wrap_content の場合とは逆の結果が得られました.どうなってるの?

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <TextView
        android:text="1:weight=1"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1"/>
    <TextView
        android:text="2:weight=2"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="2"/>
    <TextView
        android:text="3:weight=3"
        android:textSize="15pt"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="3"/>
    <TextView
        android:text="4:weight=4"
        android:textSize="15pt"
        android:background="#333333"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="4"/>
</LinearLayout>

という,疑問を呈したところで,この記事は終わりです.オチは,ありません.また,機会があったら,調べてみます.

参考サイト

Hello, Views → Linear Layout
この記事は,一連の Hello, Android 例題集のパラメータを変更していて見つけた疑問をもとにしています.
Common Layout Objects → LinearLayout
各種 Layout オブジェクトに関するマニュアルですが,ここに書いてある記述と,実際の動作は違っているように見えます. LinearLayout を入れ子にした場合に,どのような動作になるか,子部品の状態によって動作が異なってくるというのは,困りませんか?

Python で日本時間を表示するには [プログラム三昧]このエントリーを含むはてなブックマーク#

Python で日本時間を表示するには,何が必要なんでしょうか. この話題は,ググると,わんさか転がっているので,いまさら書かなくてもいいかな.

TZINFOという仕掛け

Pythonでは,夏時間などに代表される,どんなに無茶な時間制度にも対応できるように, "datetime.tzinfo" という abstract クラスが用意されています. このクラスを継承したクラスを準備するだけで,どの国の時間にも対応出来ます.

ただ,逆に言うと,このクラスを準備しないとローカルな時間には対応出来ないのです. そこで,必要最低限のメソッドを実装したクラスを作成してみました.

import datetime

class JapanTZ(datetime.tzinfo):
    def tzname(self, dt):
        return "JST"
    def utcoffset(self, dt):
        return datetime.timedelta(hours=9)
    def dst(self, dt):
        return datetime.timedelta(0)

print datetime.datetime.now(JapanTZ())

実装したメソッドは, tzname, utcoffset, dst の三つです. メソッドを呼び出す度にインスタンスを生成していますが,お気になさらぬよう.

最後の一行は,日本時間で現在時刻を表示します.

2010-01-18 23:14:24.841000+09:00

今ひとつ,あか抜けない表示ですが,まあ,いいでしょう. 表示方法を工夫したいときは, "datetime.strfdate" をご参照ください.

>>> print datetime.datetime.now(JapanTZ()).strftime("%Y-%m-%d %H:%M:%S %Z")
2010-01-18 23:34:27 JST

参考サイト

9.1. datetime — Basic date and time types
"datetime" モジュールのマニュアルです. よく読んで理解した結果,自分でクラスを作成する必要があることがわかりました.
World timezone definitions, modern and historical
2009年より以前の世界各国の "tzinof" 情報を詰め合わせたモジュールです. ただし,インストールが必要なので, Python に手を加えることができない,レンタルサーバなどで使用することはできません. そんな時は,自分で作るか,あきらめて UTC を使いましょう.

参考文献

ウサギ本には, "dateutil" というパッケージが紹介されていますが,これもインストールしなきゃ使えないみたいです. そもそも, Time Zone の話が少なすぎます.

Python クックブック 第2版

Python クックブック 第2版

  • 作者: Alex Martelli
  • 出版社/メーカー: オライリー・ジャパン
  • 発売日: 2007/06/26
  • メディア: 大型本
Python Cookbook

Python Cookbook

  • 作者:
  • 出版社/メーカー: Oreilly & Associates Inc
  • 発売日: 2005/05/05
  • メディア: ペーパーバック

Python CGI で 掲示板みたいなものを作る~sqlite3モジュール編~ [プログラム三昧]このエントリーを含むはてなブックマーク#

WS000365.png

Python CGI で 掲示板みたいなものを作る~Ajax編~では、Ajaxという仕組みを利用してページ遷移を起こさない掲示板システムを作成しました。 しかし、SQLiteとのインターフェイスは、相変わらず"system"関数によるコマンドを呼び出しで、標準入力を通じてSQLコマンドを送り込み、標準出力から結果を受け取る方式になっていました。 Python CGI で作るアクセスカウンタ~sqlite3モジュール編~でsqlite3モジュールが使えるようになったので、掲示板もsqlite3モジュールを使用するように変更してみました。

データベースの構成

今までの掲示板は、一つのデータベースを使いまわしてきましたが、今回は別のデータベースを作成しました。 その理由は、文字エンコーディングの扱いが異なってきたからです。

カラム名タイプ内容
timeNUMBER記録時刻を表す数値です。
descriptionTEXTメッセージの内容をユニコードで表現した文字列です。

このテーブルに "visitor" という名前をつけて、 "visitor-world9.sqlite" というレンタル・サーバ上のファイルに格納します。

今までのデータベースでは、メッセージを "url.quote" を通すことによって、 "ascii" すなわち7ビットのバイト列で表現していました。 今回は、 Python から直接データを入れることが出来るので、 Python の標準文字列エンコーディングである、ユニコードに変更したというわけです。 ところが、そのために、かなり苦労をすることになってしまいました。 その話は、後ほど。

データベース初期化CGI : visitor-world9-init.cgi

データベースの初期化も、"sqlite3"モジュールを使用しました。 要するに、"CREATE"文を使って"visitor"表を"cisitor-world9.swlite"データベースに作成しているだけです。 すでにデータベースファイルが存在したり、表が存在したりした場合は、エラーが発生しますが、最初の一回だけしか使用しないので、なんらエラー処理を行っていません。

#!/usr/local/bin/python
# $Id: visitor-world9-init.cgi,v 1.1 2010/01/16 08:41:14 noritan Exp $

import sys
import cgi
import sqlite3
import cgitb

# Parameters
db_file = "visitor-world9.sqlite"

# Enable debug output
cgitb.enable()

# Issue SQL
con = sqlite3.connect(db_file)
cur = con.cursor()
cur.execute(
  "CREATE TABLE visitor (time NUMBER, description TEXT)"
)
con.commit()
cur.close()
con.close()

# Execute command
print """Content-type: text/plain

OK"""

このCGIは、最後に"OK"と返答します。 まあ、こんなものでいいでしょう。

アプリケーションページ HTML : visitor-world9.html

HTMLファイルは、以前作成した"Ajax"版から呼び出すべきCGIファイルを変更しただけです。 あ、タイトルも変更してますね。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<!--
$Id: visitor-world9.html,v 1.2 2010/01/16 09:05:20 noritan Exp $
-->
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" >
<head profile="http://www.w3.org/2005/10/profile">
<title>VISITOR WORLD 9</title>
</head>
<body>
<h1>VISITOR WORLD 9</h1>
<form action="#">
<div>
<textarea name="message" id="message" rows="4" cols="40"></textarea>
</div>
<p>
<input type="button" value="Submit" onclick="submit_message();" />
<input type="reset" value="Clear" />
</p>
</form>
<div id="table"> </div>
<script src="xmlhttprequest.js" type="text/javascript"></script>
<script src="visitor-world9.js" type="text/javascript"></script>
<p>
  <a href="http://validator.w3.org/check?uri=referer"><img
      src="http://www.w3.org/Icons/valid-xhtml11"
      alt="Valid XHTML 1.1" height="31" width="88" /></a>
</p>
</body>
</html>

メッセージの記録と表示に必要な処理は、すべて、”JavaScript"と"CGI"に入れてあるので、"HTML"ファイルには、ロジックは入っていません。

メッセージ記録および表示 JavaScript : visitor-world9.js

"HTML"ファイルから呼び出される"submit_message()"関数がこの中に記述されています。

//
//   BBS using Ajax technique
//
// $Id: visitor-world9.js,v 1.1 2010/01/16 08:41:14 noritan Exp $

//
//   Submit a message to the server
//
function submit_message() {
  var element = document.getElementById('message');
  var query = 'message='+encodeURIComponent(element.value);
  xmlhttp = new XMLHttpRequest();
  xmlhttp.open('POST', 'visitor-world9.cgi', true);
  xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
      update_table(xmlhttp.responseXML);
    }
  }
  xmlhttp.setRequestHeader(
    'Content-Type', 'application/x-www-form-urlencoded;charset=utf-8'
  );
  xmlhttp.setRequestHeader("Content-Length", query.length);
  xmlhttp.send(query);
}

//
//   Get a table of messages.
//
function get_table() {
  xmlhttp = new XMLHttpRequest();
  xmlhttp.open('GET', 'visitor-world9.cgi', true);
  xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
      update_table(xmlhttp.responseXML);
    }
  }
  xmlhttp.send(null);
}

//
//   Escape a string with entity references
//
function escapeHTML(str) {
  str = str.split("&").join("&amp;");
  str = str.split("<").join("&lt;");
  str = str.split(">").join("&gt;");
  str = str.split('"').join("&quot;");
  str = str.split("{").join("&#123;");
  str = str.split("}").join("&#125;");
  str = str.split("'").join("&#039;");
  return str;
}

//
//   Update a table of message with a received XML
//
function update_table(doc) {
  var str = "";
  var element   = document.getElementById('table');
  var topnode   = doc.getElementsByTagName("visitor-memo")[0];
  var mes_list  = topnode.getElementsByTagName("message");
  
  str += '<dl>\n'
  for (var i = 0; i < mes_list.length; i++) {
    var date     = escapeHTML(mes_list[i].getAttribute("date"));
    var message  = escapeHTML(mes_list[i].firstChild.nodeValue);
    str += "<dt>" + date + "</dt>\n";
    str += "<dd>" + message + "</dd>\n";
  }
  str += "</dl>\n"
  element.innerHTML = str;
}

//
//   Initialize the table visualization
//
get_table()

このファイルも、呼び出している"CGI"以外は、"Ajax"版と同じですね。

メッセージ記録兼取り出し CGI : visitor-world9.cgi

大幅に変更されたのは、この"CGI"ファイルです。 単に"sqlite3"モジュールを使うだけで終わりかと思っていたら、文字エンコーディングでかなり苦労しました。

#!/usr/local/bin/python
# $Id: visitor-world9.cgi,v 1.1 2010/01/16 08:41:14 noritan Exp $

import cgi
import cgitb
import codecs
import exceptions
import sqlite3
import sys
import time

# Parameters
db_file = "visitor-world9.sqlite"

# Enable debug output
cgitb.enable()

# Get a POST data.
form = cgi.FieldStorage()

# Get Current time
now = time.time()

# Get and escape a MESSAGE
# At first, confirm as UTF-8 encoding
# and then trying SJIS encoding
# at last, give up encodings.
message_key = 'message'
if message_key in form:
   try:
        message = form.getvalue(message_key)
        message = unicode(message, 'utf-8')
   except exceptions.UnicodeDecodeError as ex:
        try:
            message = unicode(message, 'sjis')
        except exceptions.UnicodeDecodeError as ex:
            message = "ILLEGAL MESSAGE %s" % type(message)
else:
    message = ""

# Connect to the DATABASE
con = sqlite3.connect(db_file)
con.text_factory = sqlite3.OptimizedUnicode
cur = con.cursor()

# INSERT if expected
if len(message) > 0:
    cur.execute(
      "INSERT INTO visitor VALUES (?,?)",
      (now, message)
    )
    con.commit()

# SELECT messages
cur.execute(
  "SELECT time, description FROM visitor ORDER BY time DESC"
)

# Create UTF writer as BROWSER expected
writer = codecs.getwriter('utf-8')(sys.stdout)

# Show HTML header
writer.write("""Content-type: text/xml; charset="utf-8"

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE visitor-memo [
<!ELEMENT visitor-memo (message)* >
<!ELEMENT message (#PCDATA) >
<!ATTLIST message date CDATA #REQUIRED >
]>
""")

# Show a list of visitor record
writer.write("""<visitor-memo>
""")

# Make a list of messages
for field in cur.fetchall():
    asctime = time.strftime(
      "%Y-%m-%d (%A) %H:%M:%S",
      time.localtime(float(field[0]))
    )
    message = cgi.escape(field[1])
    writer.write("<message date=\"%s\">%s</message>\n" % (asctime, message))

# Show footer
writer.write("""</visitor-memo>
""")

# Close the writer
writer.close()

# Close DATABASE
cur.close()
con.close()

"CGI"が受け取った"FORM"情報は、"URI"形式にエンコードされていますが、"cgi.FieldStorage"の作用で文字列に変換されて受け渡されます。 ところが、この時の文字エンコーディングは、指定されていません。 "JavaScript"が送り込んだ文字エンコーディングは、"JavaScript"だけが知っているのです。 ただ、このシステムの場合には、"HTML"ファイルで指定されている"UTF-8"エンコーディングが使用されているはずです。 この"UTF-8"エンコーディングを"Python"の内部エンコーディングである"Unicode"に変換するのが、"unicode"関数です。

   try:
        message = form.getvalue(message_key)
        message = unicode(message, 'utf-8')
   except exceptions.UnicodeDecodeError as ex:
        try:
            message = unicode(message, 'sjis')
        except exceptions.UnicodeDecodeError as ex:
            message = "ILLEGAL MESSAGE %s" % type(message)

このプログラムでは、まず、'utf-8'エンコーディングであると仮定してメッセージを'Unicode'に変換します。 このとき、受け取ったメッセージが'utf-8'エンコーディングではなかった場合、 "exceptions.UnicodeDecodeError" 例外が発生します。 'utf-8'エンコーディングではなかった場合には、「特別サービス」として、'sjis'エンコーディングと仮定して変換を行います。 それでも、変換に失敗した場合には、"ILLEGAL MESSAGE"というメッセージを記録します。

もし、ここで'Unicode'に変換されなかった場合、'Unicode'ではない文字列がデータベースに記録されてしまい、表示するときにエラーを発生させてしまいます。 そのため、この入り口部分で、しっかりとエンコーディングを確認しておく必要があります。

    cur.execute(
      "INSERT INTO visitor VALUES (?,?)",
      (now, message)
    )
    con.commit()

メッセージが'Unicode'になったら、しめたものです。 "sqlite3"モジュールに"SQL"を発行してもらうだけで、データベースに文字列が入ります。 以前の版では、SQL文をコマンドの一部として発行するために、”cgi.encode"などで文字列をエンコードする操作が入っていたのですが、もう必要ありません。 単純明快でしょ。

# SELECT messages
cur.execute(
  "SELECT time, description FROM visitor ORDER BY time DESC"
)

メッセージの記録が終わったら、データベースにアクセスして、掲示板の内容を取り出します。 この部分もSQL文を直接渡すだけで、データベースへのアクセスができます。

# Create UTF writer as BROWSER expected
writer = codecs.getwriter('utf-8')(sys.stdout)

データベースから取り出した情報を元にXMLを作成するのですが、ここで一苦労ありました。 データベースに記録した文字列は、Python標準の'Unicode'です。 ところが、XML文書は、'UTF-8'で作成しようとしています。 このため、「'Unicode'の文字列を'UTF-8'に変換する」作業が必要になってきます。

そこで、使用したのが、"codecs.StreamWriter"です。 この"factory"と呼ばれる「関数」は、'Unicode'で渡した文字列を任意のエンコーディング(ここでは、'UTF-8')に変換して関数の引数として渡した"file"に書き込んでくれる"Writer"というオブジェクトを返してくれます。 ここでは、標準出力(sys.stdout)に対して'UTF-8'エンコーディングで書き出してくれる"Writer"オブジェクトを作成しています。

# Make a list of messages
for field in cur.fetchall():
    asctime = time.strftime(
      "%Y-%m-%d (%A) %H:%M:%S",
      time.localtime(float(field[0]))
    )
    message = cgi.escape(field[1])
    writer.write("<message date=\"%s\">%s</message>\n" % (asctime, message))

あとは、すべてのレコードに対応するXMLエレメントを表示してやれば、XML文書の出来上がりです。

参考サイト

8.8. codecs — Codec registry and base classes
Pythonでのエンコーディングについては、ここに書いてあるはずなのですが、読んだだけではわかりませんでした。 何本かプログラムを書いているうちに、哲学が見えてきます。
12.13. sqlite3 — DB-API 2.0 interface for SQLite databases
Python2.6になって、"sqlite3"モジュールが標準で装備されたため、プログラムが楽にはなりましたが、マニュアルは、必要です。

参考文献

Python クックブック 第2版

Python クックブック 第2版

  • 作者: Alex Martelli
  • 出版社/メーカー: オライリー・ジャパン
  • 発売日: 2007/06/26
  • メディア: 大型本
Python Cookbook

Python Cookbook

  • 作者:
  • 出版社/メーカー: Oreilly & Associates Inc
  • 発売日: 2005/05/05
  • メディア: ペーパーバック

「チッターぽい」解説します [プログラム三昧]このエントリーを含むはてなブックマーク#

WS000363.png

「チッターぽい」作りましたで、作成したプログラムですが、忙しさにかまけて、全く整備していませんでした。 認証なんか装備していないにもかかわらず、いままで、SPAMの標的にされなかったのが、不思議なぐらいです。 それでは、作成したソースコードを説明していきます。

データベース初期化CGI : chitter-poi-init.cgi

おなじみのデータベース初期化CGIです。 さくらのレンタルサーバでは、シェルが使えないので、データベースを初期化するためだけに CGI スクリプトを作成します。

#!/usr/local/bin/python
# $Id: chitter-poi-init.cgi,v 1.1 2010/01/16 03:01:47 noritan Exp $

import sys
import os
import cgi

db_file = "chitter-poi.sqlite"
table_name = "visitor"
sql = "CREATE TABLE %s (time NUMBER, description TEXT)" % table_name
command = "/usr/local/bin/sqlite3 %s \"%s\"" % (db_file, sql)

# Show error as a page description.
sys.stderr = sys.stdout

# Execute command
print """Content-type: text/plain

STATUS=%s
""" % (os.system(command))

データベースの構造自体は、今まで作ってきた「掲示板みたいなもの」と同じです。 数値型(NUMBER)の"time"と文字列型(TEXT)の"description"という二つのカラムから出来た"visitor"という表が一つだけ存在する"chitter-poi.sqlite"というデータベース・ファイルを作成しています。

レコード追加兼表示CGI : chitter-poi.cgi

すでに、Python CGI で作るアクセスカウンタ~sqlite3モジュール編~で、 Python から sqlite3 モジュールを使う手法を獲得しました。 しかしながら、この「チッターぽい」では、「納期優先」の旗の下、Python CGI で 掲示板みたいなものを作る~FieldStorage編~で使用した、「"sqlite3"コマンドを呼び出して、その入出力からデータベースを扱う」手法を取り入れました。 そのため、変更箇所は、ほんの少しです。 「再利用」と呼んでください。

#!/usr/local/bin/python
# $Id: chitter-poi.cgi,v 1.1 2010/01/16 03:09:52 noritan Exp $

import os
import sys
import cgi
import urllib
import time
import cgitb
import subprocess

# Show error as a page description.
sys.stderr = sys.stdout
cgitb.enable()

# Database releated information
db_file = "chitter-poi.sqlite"
table_name = "visitor"
command = "/usr/local/bin/sqlite3 %s" % (db_file)

# Get a POST data.
form = cgi.FieldStorage()

# Get Current time
now = time.time()

# Get and escape a MESSAGE
message_key = 'message'
if message_key in form:
    message = urllib.quote(cgi.escape(str(form.getvalue(message_key))))
else:
    message = ""

# Compose a SQL
sql = "SELECT time, description FROM %(table)s ORDER BY time DESC LIMIT %(limit)s;"
if len(message) > 0:
    sql = \
      "INSERT INTO %(table)s VALUES (%(now)f, \"%(message)s\");\n" \
      + sql

# Show HTML header
print """Content-type: text/html; charset="utf-8"

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" >
<head profile="http://www.w3.org/2005/10/profile">
<title>Chitter-poi</title>
</head>
<body>
<h1>Chitter-poi</h1>
<form action="./chitter-poi.cgi" method="post">
<div>
<textarea name="message" rows="2" cols="20"></textarea>
</div>
<p>
<input type="submit" value="Submit" />
<a href="./chitter-poi.cgi">Reload</a>
</p>
</form>
"""

# Access to the DATABSE
pipe = subprocess.Popen(command, shell=True,
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    close_fds=True
);
(pipe_out, pipe_in) = (pipe.stdin, pipe.stdout)
pipe_out.write(
  sql % {
    "table"   :table_name,
    "now"     :now,
    "message" :message,
    "limit"   :16
  }
)
pipe_out.close()

# Show a list of visitor record
print """<dl>
"""

# Make a list of messages
try:
    for line in pipe_in:
        field = line.split("|")
        asctime = time.strftime(
          "%Y-%m-%d %H:%M",
          time.localtime(float(field[0]))
        )
        message = urllib.unquote(field[1])
        print "<dt>%s</dt><dd>%s</dd>\n" % (asctime, message)
finally:
    pipe_in.close()


# Show footer
print """
</dl>
</body>
</html>
"""

この CGI では、 HTML 文書の作成までをおこなっています。 そのため、連動する別の HTML などは必要なく、この CGI だけですべての処理を行っています。

もともと、遅い携帯端末で使用することを考えて作成したので、メッセージを書き込む textarea もちいさめです。 また、余分なパケットを飛ばさないために、 CSS などの装飾やカワイイ画像などは、一切ありません。

この「チッターぽい」では、最新の書き込み16件のみを表示して、端末の負荷を軽くしています。 初版のプログラムでは、すべてのレコードを取り出して、最初の16件だけを表示していました。 しかし、それでは、レコードが増加するとサーバ側の負荷が増えてしまいます。

SQL で、上位レコードだけを取り出す構文として最初に紹介されているのが "TOP n" という句です。 この句を SELECT 文にくっつけると、最初の数レコードだけを取り出すことが出来ます。

ところが、 SQLite では、 TOP 句を使用することが出来ませんでした。 おそらく、 SQLite が対応していないのが原因だとは思います。 調べていくうちに、 SQLite では、 TOP 句の代わりに LIMIT 句というものが使えるらしいとわかってきました。

SELECT time, description FROM %(table)s ORDER BY time DESC LIMIT %(limit)s;

SELECT 文に "LIMIT n" を加えると、最初の n レコードのみを取り出すことが出来ます。 これに "ORDER BY" 句を組み合わせると、最新の書き込みだけを表示することが出来ます。

次は、 "sqlite3" モジュールを使って書き直すことですが、時間あるかな。 他には、センサーノードの受け皿専用の「チッターロボ」も計画しています。

参考サイト

SQL As Understood By SQLite
SQLite 本家のSQL文法書です。 これを見ると、 LIMIT 句には、範囲を指定する使い方もあることがわかります。

参考文献

ウサギ本は、 SQL の文法までは解説していません。

Python クックブック 第2版

Python クックブック 第2版

  • 作者: Alex Martelli
  • 出版社/メーカー: オライリー・ジャパン
  • 発売日: 2007/06/26
  • メディア: 大型本
Python Cookbook

Python Cookbook

  • 作者:
  • 出版社/メーカー: Oreilly & Associates Inc
  • 発売日: 2005/05/05
  • メディア: ペーパーバック

PythonのStreamReaderとJavaのInputStreamReaderは、意味が異なる [プログラム三昧]このエントリーを含むはてなブックマーク#

今日の記事は、自分のために残したメモです。 昨日の記事では、PythonでUTF-8ファイルを読み込むための工夫を書きました。 その中で、codecs.StreamReaderというクラスを見つけたのですが、どうも、クラスに見えない。 そこで、私が理解可能なJavaに戻って、StreamReaderについて考えてみました。

Javaの場合のUTF-8ファイルの読み込み

簡単な例として、UTF-8ファイルを読み込んで、コンソールに表示するプログラムを作成してみました。

package org.noritan;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.nio.charset.Charset;
import java.io.FileNotFoundException;
import java.io.IOException;

public class UtfFileRead {
  public static void main(String argv[]) {
    try {
      FileInputStream stream = new FileInputStream(argv[0]);
      try {
        BufferedReader reader = new BufferedReader(
          new InputStreamReader(
            stream, Charset.forName("UTF-8")
          )
        );
        try {
          while (reader.ready()) {
            System.out.println(reader.readLine());
          }
        } catch (IOException ex) {
          // Failed to read from the reader
          // Abort reading.
        } finally {
          try {
            reader.close();
          } catch (IOException ex) {
            // Failed to close reader
            // Do nothing
          }
        }
      } finally {
        try {
          stream.close();
        } catch (IOException ex) {
          // Failed to close stream
          // Do nothing
        }
      }
    } catch (FileNotFoundException ex) {
      // Failed to create a FileInputStream.
      // Do nothing
    }
  }
}

実行すると、このようになります。

Z:\noritan\java>java org.noritan.UtfFileRead in-utf.txt
あいうえお
かきくけこ
ただいま、マイクのテスト中。

大した事をやっているわけでもないのに、例外処理がわんさか出てきました。

Javaでも、InputStreamという概念とReaderという概念が存在します。 InputStreamは、ファイルから入力した生のデータです。 したがって、複数バイトにわたる文字もバイト列として入ってきます。 それに対して、Readerは、バイト列に「文字」という概念を取り入れて、「文字」を一つずつ入力することが出来ます。

このInputStreamから入ってきたバイト列を特定のエンコーディングにしたがって、「文字列」として取り入れてReaderオブジェクトとして振舞うのが、InputStreamReaderです。 InputStreamReaderオブジェクトは、引数にInputStreamオブジェクトとエンコーディングをあらわすCharsetオブジェクトを与えて生成します。

このプログラムでは、一行ごとの処理を行わせるために、さらにBufferedReaderクラスをかぶせて使っています。

Pythonの場合のUTF-8ファイルの読み込み

Pythonでも、読み込んだUTF-8ファイルをコンソールに表示するプログラムを作成してみました。

import codecs
import sys

utfReaderFactory = codecs.getreader('utf-8-sig')
reader = utfReaderFactory(open(sys.argv[1],'r'))
sjisWriterFactory = codecs.getwriter('sjis')
writer = sjisWriterFactory(sys.stdout)
writer.writelines(reader.readlines())
writer.close()
reader.close()

実行すると、このようになります。

Z:\noritan\python>python utfFileRead.py in-utf.txt
あいうえお
かきくけこ
ただいま、マイクのテスト中。

Javaの場合と異なり、出力にもエンコーディングを指定しています。 これは、デフォルトの状態では、"ascii"エンコーディングになっていて、表示ができなかったためです。

Pythonの場合、"codecs.getreader('utf-8-sig')"で戻ってくるのがStreamReaderです。 しかし、これはオブジェクトでは、ありません。 Pythonのマニュアルでは、"factory"と書いてあります。 簡単に言うと、「オブジェクトを返す関数」です。

Javaの場合には、関数が直接オブジェクトになることはありません。 かならず、オブジェクトのメソッドとして関数が定義されます。

ところが、Pythonの場合には、関数単体でもオブジェクトとなることができます。 そのため、"utfReaderFactory"という変数に入れた関数を呼び出して、「UTF-8ファイルを入力するReaderオブジェクトを作成する」ことができます。 このような構成の事をデザインパターンでは、"FactoryMethod"と呼ぶのですが、"utfReaderFactory"は、関数であって、メソッドではありません。 こういう場合、なんと呼ぶのでしょうか。

まとめ

Javaの場合、関数がオブジェクトになることは出来ないので、「オブジェクトを作成する関数」を直接渡すことは出来ません。 代わりに、「オブジェクトを作成するメソッドを装備したオブジェクト」を渡して、そのメソッドを呼び出すことでオブジェクトを作成します。 このような構成を"FactoryMethod"と呼んでいます。

Pythonの場合、関数をオブジェクトとして扱うことが出来るので、"Factory"と呼ばれる「オブジェクトを作成する関数」を渡して、目的のオブジェクトを作成します。 この構成の場合には、単に"Factory"と呼んでいるようです。

参考文献

オブジェクト指向における再利用のためのデザインパターン

オブジェクト指向における再利用のためのデザインパターン

  • 作者: エリック ガンマ
  • 出版社/メーカー: ソフトバンククリエイティブ
  • 発売日: 1999/10
  • メディア: 単行本
増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

  • 作者: 結城 浩
  • 出版社/メーカー: ソフトバンククリエイティブ
  • 発売日: 2004/06/19
  • メディア: 単行本

BOMに悩む [プログラム三昧]このエントリーを含むはてなブックマーク#

BOMって、ご存知でしょうか。 電子工作界隈であれば、 "Bill Of Materials" (部品表)なのですが、プログラマ界隈では、 "Byte Order Mark" (バイト順のしるし)の略なのだそうです。

Pythonで日本語を扱いたい

Python という言語で日本語を扱いたい場合、 Python 自身が内部でユニコードを使っていることから、すんなり使えると考えていました。 ところが、実際に使ってみると、 "UTF-8" で書いたはずのファイルが読み込めないという事態に陥ってしまいます。

どうも、ファイルの最初の3バイトに余計なデータが入っているようなのです。

0xef, 0xbb, 0xbf

これは、 Microsoft の Notepad (日本語ではメモ帳)でファイルの先頭に追加される記号です。 これが "Byte Order Mark" (BOM) と呼ばれています。

Python は、BOMを認めていない

Microsoft に偏った情報によると、 BOM の入ったファイルを UTF-8 と呼び、入っていないファイルを UTF-8N と呼ぶ宗派もあるようです。 ところが、 Python 的には、 UTF-8 エンコードというのは、あくまでも BOM の無いファイルを指すのであって、 BOM が入るようなものは UTF-8 とは認めていません。 そのため、 PythonBOM 付きのファイルを読ませようとすると、「ユニコードでは無い文字があるぞ。」と言って受け付けてくれません。

UnicodeEncodeError: 'cp932' codec can't encode character u'\ufeff' in position 0: illegal multibyte sequence

このメッセージでは、 "cp932" に問題があるように見えますが、問題は、 "cp932" では表現できない BOM にあるのです。

とはいえ、抜け道もある

まあ、突っ張っているだけでは、解決しないので、抜け道も用意されています。 それは、 "utf-8-sig" という呪文のようなエンコーディングを使うことです。 このエンコーディングでは、邪魔な BOM を取り除いてファイルを読み込んでくれます。 Notepad で書いた "UTF-8" ファイルを "MS漢字" ファイルに変換するプログラムは、こんな風になります。

fi=codecs.getreader('utf-8-sig')(open('in.txt','r'))
fo=codecs.getwriter('cp932')(open('out.txt','w'))
for line in fi.readlines():
  fo.write(line)
fo.close()
fi.close()

これで、すっきりしました。

参考文献

8.8.2. Encodings and Unicode
Python のオンラインドキュメントです。 Python は、内部処理こそユニコードを使っていますが、外部ファイルなどとのインターフェイスは、ユニコードばかりとは限りません。 そこで、考えられたのが、エンコーディングという考え方です。 これで、どんな文字コードにも対応できるはずなのですが、反面、エンコーディングの認識に失敗すると、文字化けとなって現れてしまいます。

「チッターぽい」作りました [プログラム三昧]このエントリーを含むはてなブックマーク#

この大事なときに、Twitterがメンテナンスをするというので、急遽、「チッターぽい」ものを作りました。

http://noritan.org/cgi/chitter-poi.cgi

マニュアルは、いらないよね。 元祖Twitterと違い、「誰が」発言したのかは、全くチェックしておりません。 Twitterがメンテナンスに入った時の連絡用に使いましょう。

納期優先プログラムのため、コード解説は省略。


Python CGI で作るアクセスカウンタ~sqlite3モジュール編~ [プログラム三昧]このエントリーを含むはてなブックマーク#

PythonからSQLiteモジュールが直接使えるように、さくらインターネットに対応してもらいました。 詳しくは、Python 2.5.2 で、 SQLite が使えるはずだったのに。に書きました。 これで、ついに、コマンドラインを介さずにデータベースの操作をすることができます。 そこで、アクセスカウンタをデータベースに対応させてみました。

データベースの構成

アクセスカウンタで記録する情報は、「LOGファイル版」のものと同じです。 これらをテキストファイルではなく、データベースの「行」として記録していきます。

カラム名タイプ内容
timeREAL記録時刻を表す数値です。
urlTEXTアクセス先のURL (document.URL) を記録します。
referrerTEXT参照元のURL (document.referrer) を記録します。参照元が不明な場合には、 "None" が入ります。
r_addrTEXTページの表示を要求したクライアントのIPアドレス (REMOTE_ADDR) です。
r_hostTEXTクライアントが申告したホスト名 (REMOTE_HOST) です。クライアントが申告しなかった場合には、 "None" が入ります。

このテーブルに "visitor" という名前をつけて、 "acount1.sqlite" というレンタル・サーバ上のファイルに格納します。

データベース初期化CGI : acounter1_init.cgi

データベース版アクセスカウンタをBLOGに実装するため、いくつかのプログラムを作成しました。 最初は、データベースを初期化するためのCGIプログラムです。

#!/usr/local/bin/python
# $Id: acounter1_init.cgi,v 1.2 2009/07/21 07:49:36 noritan Exp $
# Create a table

import sys
import cgi
import sqlite3
import cgitb

# Parameters
db_file = "acount1.sqlite"

# Enable debug output
cgitb.enable()

# Issue SQL
con = sqlite3.connect(db_file)
cur = con.cursor()
cur.execute("CREATE TABLE visitor (time REAL, url TEXT, referrer TEXT, r_addr TEXT, r_host TEXT)")
con.commit()
cur.close()
con.close()

# Executed successfully
print """Content-Type: text/plain

OK"""

仕様に従って、 "acount1.sqlite" ファイルに "visitor" テーブルを作成するだけの CGI です。 "sqlite3" モジュールが使えるおかげで、従来の方法に比べてこんなにシンプルになりました。

アクセス記録 JavaScript : acounter1.js

アクセスを記録するための構成は、Python CGI で作るアクセスカウンタ~LOGファイル版~と同じです。 "JavaScript" のプログラムから "Python" の CGI を呼び出します。

//
//   Access counter in a SQLite file
//
// $Id: acounter1.js,v 1.1 2009/07/05 06:01:41 noritan Exp $

//
//   Construct a javascript statement
//   to invoke a CGI with parameters as text/javascript.
//
document.writeln('<script type="text/javascript" charset="utf-8"'
+ ' src="http://noritan.org/cgi/acounter1.cgi'
+ '?URL=' + escape(document.URL)
+ "&HTTP_REFERER=" + escape(document.referrer)
+ '"></script>');

単に呼び出される CGI の URL が変更されただけです。 呼び出された CGI の出力は、 "JavaScript" プログラムとして扱われます。

アクセス記録 CGI : acounter1.cgi

実際にデータベースを操作するための CGI プログラムです。 "sqlite3" モジュールが使えるおかげで、ずいぶんとすっきりしました。

#!/usr/local/bin/python
#
#   Access counter in a SQLite file
#
# $Id: acounter1.cgi,v 1.1 2009/07/05 06:58:55 noritan Exp $

import os
import cgi
import time
import sqlite3
import cgitb

# Parameters
db_file = 'acount1.sqlite'

# Show error as a page description.
cgitb.enable()

# Get a POST data.
form = cgi.FieldStorage()

# Collect VALUEs of a record.
now = time.time()
url = form.getvalue('URL', 'None')
referrer = form.getvalue('HTTP_REFERER', 'None')
r_addr = os.getenv('REMOTE_ADDR', 'None')
r_host = os.getenv('REMOTE_HOST', 'None')

# Insert a new record
con = sqlite3.connect(db_file)
cur = con.cursor()
cur.execute(
    'INSERT INTO visitor VALUES (?,?,?,?,?)',
    (now, url, referrer, r_addr, r_host)
)
con.commit()
cur.close()
con.close()

# Show HTTP response
print """Content-type: text/javascript; charset="utf-8"

document.writeln('<div>OK</div>')
"""

ご覧のように、 "Python" で生成した文字列を何ら変換することなく格納することが出来るので、文字をエスケープしたりする手間がかかりません。 また、SQL文が生成されるときに適切なエスケープ処理が行われるので、安全にデータベースを操作することができます。

データベースの操作は、 "Cursor.execute" メソッドによる操作指示と "Connection.commit" メソッドによる操作確定から構成されています。 この操作を確定させる機能が備わっているために、データベースの一貫性が保たれます。

実行の結果、 "OK" を表示する "JavaScript" プログラムが生成されます。

BLOGにアクセスカウンタを仕掛ける

このアクセスカウンタを仕掛ける方法も従来と同じです。 以下のような記述をHTML文書の中に仕込みます。

<script type='text/javascript' charset='utf-8' src='http://noritan.org/cgi/acounter1.js'></script>

これで、アクセス記録を作成する部分は完了です。

お試しCGI : acounter1_show_me.cgi

LOGファイル版では、テキストファイルに全てのアクセス記録が見えてしまうため、LOGファイルの場所は隠蔽してありました。 今回は、データベースを使用しているので、必要なアクセス記録だけを表示させることが出来ます。 そこで、結果を表示しようとしているクライアントと同じIPアドレスの記録だけを表示する CGI プログラムも作成しました。

#!/usr/local/bin/python
# $Id: acounter1_show_me.cgi,v 1.1 2009/07/21 07:56:30 noritan Exp $
# Show my records

import os
import sys
import cgi
import urllib
import time
import sqlite3
import cgitb

# Show error as a page description.
cgitb.enable()

# Database releated information
db_file = "acount1.sqlite"

# Get Current time
now = time.time()

# Get current IP address
r_addr = os.getenv('REMOTE_ADDR', 'None')

# Compose a SQL
con = sqlite3.connect(db_file)
cur = con.cursor()
cur.execute(
  'SELECT * FROM visitor WHERE r_addr=? ORDER BY time',
  (r_addr, )
)

# Show HTML header
print """Content-type: text/html; charset="utf-8"

<html>
<body>
<pre>
"""

# Access to the DATABASE
for field in cur.fetchall():
    print "DATE: %s" % time.strftime(
        "%Y-%m-%d (%A) %H:%M:%S",
        time.localtime(float(field[0]))
    )
    print "URL : %s" % cgi.escape(field[1])
    if (field[2] != 'None') :
        print 'HTTP_REFERER : <a href="%s">%s</a>' % (
            cgi.escape(field[2]),
            cgi.escape(field[2])
        )
    print "REMOTE_ADDR : %s" % cgi.escape(field[3])
    if (field[4] != 'None') :
        print "REMOTE_HOST : %s" % cgi.escape(field[4])
    print ""

# Show footer
print """
</pre>
</body>
</html>
"""

cur.close()
con.close()

WHERE句を使うことによって、データベースの "visitor" テーブルの中から、アクセス中の "REMOTE_ADDR" と同じ r_addr を持つ行だけを表示します。 この CGI によって、例えば、以下のような結果が得られます。

DATE: 2009-07-21 (Tuesday) 16:40:06
URL : http://noritan-micon.blog.so-net.ne.jp/
HTTP_REFERER : http://noritan.org/
REMOTE_ADDR : XXX.XXX.XXX.XXX
REMOTE_HOST : XXXXXX.XXXXXX.net

DATE: 2009-07-21 (Tuesday) 16:55:21
URL : http://noritan-micon.blog.so-net.ne.jp/2009-06-27
HTTP_REFERER : http://noritan-micon.blog.so-net.ne.jp/
REMOTE_ADDR : XXX.XXX.XXX.XXX
REMOTE_HOST : XXXXXX.XXXXXX.net

アドレスとホスト名は、伏字にしてあります。

データベース版のプログラムを試しに仕込んでみたところ、5781行を記録したファイルは、1,421,312 バイトになりました。 一行あたり246バイトです。 データベースの構成を考えると、もっと小さく出来るはずです。 それは、今後の課題とします。

参考文献

PythonでSQLiteを使う方法は、これを参考にしました。

Python Cookbook

Python Cookbook

  • 作者:
  • 出版社/メーカー: Oreilly & Associates Inc
  • 発売日: 2005/05/05
  • メディア: ペーパーバック

翻訳版もあります。

Python クックブック 第2版

Python クックブック 第2版

  • 作者: Alex Martelli
  • 出版社/メーカー: オライリー・ジャパン
  • 発売日: 2007/06/26
  • メディア: 大型本

Python CGI で作る「秘密鍵・公開鍵」発行システムの構想 [プログラム三昧]このエントリーを含むはてなブックマーク#

アクセス・カウンタを作ろうと、ログ・ファイルを作成するところまで、たどり着きました。 でも、ログ・ファイルをそのまま公開するのは危なそうです。

お見せしたいのは、やまやまなれど

ログファイル版アクセス記録を作成しました。 そこで、みなさまに、どんな情報が収集できたかお見せしたいのですが、どうも公開すると差し障りのありそうな内容になってしまいそうです。 特に HTTP_REFERRER には、検索エンジンに与えたキーワードが含まれているので、「○○○社が○○○というキーワードで検索をかけてきて、○○○という情報に興味を示した。」という事までわかってしまいます。 場合によっては、「売れる情報」です。

そのため、すべての情報を公開するようなまねはしません。 ただ、このアクセス・カウンタを使ってみたいと思われる方もいらっしゃると思いますので、公開できる情報だけ表示するシステムを考えてみました。

需要1 : ログを見てみたいだけ

興味だけで、どんなログが表示されるか見たい方むけには、見に来た方に関連した情報だけをお出しします。 具体的には、「見に来た方のIPアドレス」と「アクセス記録のIPアドレス」が一致する情報だけ抜き出して表示するページを考えています。 もっとも、「抜き出す」作業が必要なので、データベース版を作った後の話ですが。 当アクセス記録に参加している BLOG に出たり入ったりして、記録のされ方を確認することが出来ます。 あっ、出たほうは記録に残らないか。

この方法にも、問題点が無いわけではありません。 それは、一般的なプロバイダでは DHCP (Dynamic Host Configuration Protocol) を使っているので、「見に来た方のIPアドレス」は決して固定では無いという事です。 そのため、ネットワークが物理的に近い人のアクセス記録がたまたま見えてしまう可能性もあります。 また、LANにゲートウェイを設置している企業などの場合にも、大域IPアドレスが数個に限定されると思いますので、社内の多くの端末からのアクセス記録が見えてしまいます。

まあ、DHCPがコロコロ変更される状況は、そうそう起こらないだろうし、社内からのアクセス記録が社内の他の人に見えると困るという状況(それはそれで、別の問題がありそう。)も考えにくいと思います。 データベース版を作成したら、最初にこのタイプのアクセス記録表示ページを作成しようと思っていますので、懸念のある方は、あらかじめお知らせください。

需要2 : 自分のWEBページ(BLOG)のアクセス記録を取りたい

このアクセス・カウンタの本来の目的は、こっちです。 もちろん、自前のレンタル・サーバを用意すれば、やりたい放題ですが、そこまでコストをかけずに、"noritan.org"のデータベースを共同利用する方法もあります。 この場合、「見に来た人がオーナになっているWEBページに関連する情報だけ提供する」というのが、正しい姿だと思います。 いくつかアクセス記録参照の際の問題点を考えました。

  1. アクセス記録を参照する方法

    あるWEBページのアクセス記録を参照する場合を考えます。 性善説に基づけば、「WEBのホスト名(と上位ディレクトリ)を入れてね。」だけで済むのですが、最近のインターネットは善人ばかりじゃないらしいので、この方法は使えません。

    そこで、WEBページのオーナしか知らないアクセス・キー(秘密鍵と名づけます)を指定してアクセス記録を参照するようにします。 秘密鍵は、アクセス記録参照ページの URL に query を入れるだけで十分でしょう。 盗聴までは、考えないことにします。

  2. 秘密鍵を発行する方法

    秘密鍵を発行する時、それほど、重いセキュリティが必要だとも思えないので、「あなたのWEBページに対応する秘密鍵は、コレです。」というメールを送れば十分です。 この方法は、簡単なのですが、「メール・アドレスの受け渡しをどうするか」「メール・アドレスが本人のものなのか」という問題が解決できません。

    そこで、「秘密鍵発行ページ」を用意し、秘密鍵を発行すると同時に"noritan.org"の鍵サーバに登録する作業を自動で行います。 これで、WEBページのオーナしか知らない秘密鍵ができました。

  3. WEBページのオーナであることを証明する方法

    WEBページのオーナが秘密鍵を受け取ることが出来ましたが、これだけでは、秘密鍵をどのWEBページに紐付けすればよいかが判断できません。 「秘密鍵発行ページ」で「WEBのホスト名(と上位ディレクトリ)を入れてね。」とやってしまうと、誰でも他人のWEBページのアクセス記録を見ることが出来ます。

    「私が持っている秘密鍵はコレです。」という記事をWEBページに貼ってもらうと、簡単に確認できますが、秘密鍵を公開してしまうことになるので、意味がありません。 そこで、「秘密鍵発行ページ」で「秘密鍵」と同時に「公開鍵」を発行し、「公開鍵」の方をWEBページに貼ってもらいます。 「秘密鍵」と「公開鍵」を対にして鍵サーバに登録すれば、WEBページのオーナが持っている「秘密鍵」がどれなのかを判断することが出来ます。

  4. 他人が「公開鍵」を貼るのを防ぐ方法

    「公開鍵」をWEBページに貼る方法には、欠点があります。 それは、『WEBページに貼られた「公開鍵」を別のWEBページにコピーすることができる。』ということです。

    こういった場合、「本物は誰だ!!」と問われても、判断できません。 そこで、「公開鍵」を貼ると同時に当BLOGのコメントまたはトラックバックで「ここに貼りました。」と通知してもらいます。 これなら、もし、別の人が「公開鍵」をコピーしたとしても、コメントまたはトラックバックの到着順から本物を見分けることが出来ます。 つまり、「早い者勝ち」システムです。

    もっとも、「公開鍵」をコピーした人は、「秘密鍵」を持っていないため、情報にアクセスすることはできません。 コピーした人に嫌がらせ以外の利益は無いはずなので、こういう事件は発生しないと思います。

  5. 「秘密鍵」と「WEBページ」の紐付け方法

    ここは、手動で行います。 当BLOGオーナがコメントまたはトラックバックを確認して、紐付け情報を鍵サーバに登録します。 ここだけは、自動化しない方がきっと無難でしょう。

というわけで、人間も大いに介在するシステムが出来上がりました。

アクセス記録を参照するまでの手順

DataBaseKeyProvider.png

色々と問題点を並べましたが、すべて解決できそうなので、システム全体のおさらいです。


  • ユーザが「鍵発行ページ」から鍵の発行を申請をする (1)
  • 自動的に「公開鍵」と「秘密鍵」が鍵サーバに登録される (2)
  • ユーザが表示された「公開鍵」と「秘密鍵」をメモする (3)
  • ユーザが紐付けを希望するWEBページに「公開鍵」を記入し公開する (4)
  • ユーザがDBオーナに「公開鍵」の公開をコメントまたはトラックバックで通知する (5)
  • DBオーナが鍵サーバに「WEBページ情報」を登録する (6)
  • ユーザが「秘密鍵」を使ってアクセス記録を参照する (7)
  • アクセス記録参照ページが「秘密鍵」を認証し記録を表示する (8)

これ以外に「ユーザが自身のWEBページにJavaScriptを仕込む」というステップがありますが、これはいつでもかまいません。

以上の手順で安全にアクセス記録を参照できるようになると思います。 抜けは、ありませんかね。

参考文献

昔、この本で読んだエキスが少しは入っていると思う。

暗号技術大全

暗号技術大全

  • 作者: ブルース・シュナイアー
  • 出版社/メーカー: ソフトバンククリエイティブ
  • 発売日: 2003/05/31
  • メディア: 単行本

Python CGI で作るアクセスカウンタ~LOGファイル版~ [プログラム三昧]このエントリーを含むはてなブックマーク#

Python CGI を利用して、アクセスカウンタを作ります。 第一歩は、LOGファイルを作ることから。

動機

このアクセスカウンタを作る動機になったのは、某BLOG某記事にあった一言「一体どこからいらっしゃってるのかが気になります。」です。 BLOGパーツにもアクセスカウンタがあります。 また、BLOGのアクセス解析という機能もあります。 しかし、もっと詳しく分析する必要が出てきたときには、何らかの仕掛けを入れてアクセス記録をデータベース化したいと思うはずです。

条件は、「BLOGのドメインの外にアクセスカウンタを置く必要がある。」です。 これは、BLOGそのものに手を付けるわけにはいかないし、自前でBLOGを立ち上げる気も無いからです。

JavaScript : visitor_world8.js

まず、HTML文書からJavaScriptを呼び出します。 呼び出すスクリプトがこれです。

//
//   Access counter in a log file
//
// $Id: visitor_world8.js,v 1.3 2009/06/16 20:11:37 noritan Exp $

//
//   Construct a javascript statement
//   to invoke a CGI with parameters as text/javascript.
//
document.writeln('<script type="text/javascript" charset="utf-8"'
+ ' src="http://noritan.org/cgi/visitor_world8.cgi'
+ '?URL=' + escape(document.URL)
+ "&HTTP_REFERER=" + escape(document.referrer)
+ '"></script>');

//

このスクリプトでは、HTML文書のURL (document.URL) とその参照元 (document.referrer) をパラメータとして付けて "visitor_world8.cgi" というCGIを呼び出すタグを作ります。 たとえば、こんなタグが生成されます。

<script type="text/javascript" charset="utf-8" src="http://noritan.org/cgi/visitor_world8.cgi?URL=http://noritan.org/&HTTP_REFERER=http://noritan-micon.blog.so-net.ne.jp/"></script>

これら二つのパラメータは、CGIを呼び出すと書き換えられて失われてしまいます。 そのため、JavaScriptで拾い上げておく必要があったのです。

スクリプトが出した出力に従って、スクリプトが動くという仕組みは、非常に奇妙ですが、できるみたいです。 無限連鎖しないように気をつける必要があると思います。

他にもCGIを呼び出すタグはあります。 画像を呼び出す"IMG"タグもその一つです。 "IMG"の使用も考えましたが、CGIから正しい画像ファイルを返すプログラムを作るのが面倒だったため、テキストを返すだけで済む"SCRIPT"を使用しています。

CGI : visitor_world8.cgi

"JavaScript"から呼び出された"CGI"は、以下の"JavaScript"記述を返します。

document.writeln('<div>OK</div>')

もちろん、これは表面上の話で、裏では、アクセス記録を残すプログラムが動いています。

#!/usr/local/bin/python
#
#   Access counter in a log file
#
# $Id: visitor_world8.cgi,v 1.1 2009/06/16 20:06:41 noritan Exp $

import os
import sys
import cgi
import urllib
import time
import cgitb

# Show error as a page description.
sys.stderr = sys.stdout
cgitb.enable()

# Get a POST data.
form = cgi.FieldStorage()

# Construct a parameter table
#   All from the parameters
#   Some from the environment variables
data = {}
for name in form:
    data[name] = str(form.getvalue(name))
for name in ('REMOTE_ADDR', 'REMOTE_HOST'):
    data[name] = str(os.environ[name])

# Show HTTP response
print """Content-type: text/javascript; charset="utf-8"

document.writeln('<div>OK</div>')
"""

# Record in a log file
f = open("XXXXXXXXXX.txt", "a+")

try:
    now = time.time()
    asctime = time.strftime(
        "%Y-%m-%d (%A) %H:%M:%S",
        time.localtime(now)
    )
    f.write("%s\n" % asctime)
    for name in data:
        f.write('%s : %s\n' % (name, data[name]))
    f.write("\n")
finally:
    # Abort if failed.
    f.close()

#

このバージョンでは、アクセス記録をログファイルに残していきます。 ただし、ログファイル名は、諸般の事情により伏字になっています。

ログファイルに記録する内容は、時刻、文書URL、参照元URL、クライアントのIPアドレス、クライアントのホスト名です。 ログファイルには、こんな感じの記述が並びます。

2009-06-17 (Wednesday) 05:58:36
URL : http://noritan-micon.blog.so-net.ne.jp/
REMOTE_HOST : xxxxxxxxxx.xxxxxxxxxx.net
HTTP_REFERER : http://noritan.org/
REMOTE_ADDR : xxx.xxx.xxx.xxx

クライアントに関する情報は、伏字としました。

運用してみたら

このアクセスカウンタを設置するには、BLOGに以下のタグを埋め込みます。 現在は、右側のカラムの"POWERED BY"の下に埋め込んでいます。 地味に表示されている"OK"がこのスクリプトを実行した結果です。

<script type='text/javascript' charset='utf-8' src='http://noritan.org/cgi/visitor_world8.js'></script>

ほぼ、丸一日、当BLOGにこのスクリプトを仕掛けておいたところ、LOGファイルは200Kバイトを超えました。 このままでは、巨大なファイルが出来上がってしまうので、早いとこ情報をデータベースに放り込むプログラムを作らなくては。

参考文献

JavaScript – The Definitive Guide 5e

JavaScript – The Definitive Guide 5e

  • 作者: D Flanagan
  • 出版社/メーカー: Pragma
  • 発売日: 2006/08/25
  • メディア: ペーパーバック
Learning Python 3e (Learning)

Learning Python 3e (Learning)

  • 作者: M Lutz
  • 出版社/メーカー: Pragma
  • 発売日: 2007/11/06
  • メディア: ペーパーバック

前の20件 | - プログラム三昧 ブログトップ

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。