JavaScript でリサジュー [プログラム三昧]
お久しぶりでございます。 休載していた間に JavaScript がずいぶん変わってしまったみたいで。 ためしに JavaScript でリサージュ(リサジュー)を描いてみよう。
onclik が無くなって、 var が無くなって、かなり変わりましたね。
scilab で、 LFSR の実験 [プログラム三昧]
Linear Feedback Shift Register (LFSR) って、ご存知ですか? シフトレジスタにちょっとしたフィードバック論理を加えて、ビット列を出力させる仕組みの事です。 この記事では、 scilab で LFSR のシミュレーションを行います。 scilab を使うのも、久々だねぇ。
LFSR って、何だろう
フリップフロップを何段も接続したものをシフトレジスタと呼びます。 シフトレジスタは、 D から入ってきた入力が、その段数分のクロックを与えると出力に出てきます。
ここで、個々のフリップフロップの出力にある演算をほどこした結果を D 入力に与えてやると、シフトレジスタの出力からは、自動的にビット列が出力されます。
このとき、演算式を工夫すると、疑似乱数と呼ばれる出力を得られるようになります。 ここでは、 scilab を使って、疑似乱数の生成と解析を行います。
LFSR のシミュレーション
LFSR の実現方法には、フィボナッチとガロアの二種類の実装が知られています。 ここでは、ガロアの実装にしたがって、実装を行います。 XOR 演算を使用してフィードバックを実装します。
ガロアの実装を表記するためにタップの位置をリストしたものを使います。 例えば、このような配列で表記します。
配列の最初がシフトレジスタの段数に相当します。 残りは、 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 で作成される疑似乱数は、その特徴として、1の数が 2**(lfsr_stage-1) となります。 この例では、 sum(lsfr)=32 となります。
y=abs(fft(lfsr)); scf(2);clf;plot((0:lfsr_period-1),y)
さらに、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 で実現する事もできます。 ある一定の数のパルスを得たいのであれば、これでも十分です。 しかし、周波数特性は大きく違います。
y=abs(fft(pwm)); scf(2);clf;plot((0:lfsr_period-1),y)
このように、ある特定の周波数で大きなエネルギーが出ている事がわかります。 この信号が原因でノイズが出る場合、突出してエネルギーが出ている特定周波数でのノイズを減らさなくてはなりません。 疑似乱数を使うと、ノイズが各周波数にまんべんなく広がり、エネルギーの突出が無くなります。 このため、ノイズ対策も行いやすくなります。
オーバーサンプリングしてみたら
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 波形は、インパルスを使って表現していました。 実際には、各パルスが幅を持つことになります。 そこで、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)
同様に、周波数特性を計算させました。 このグラフの縦軸は、 DC 成分を 1/2 (-6dB) としたデシベル表示で表現しています。 また、 log10() 関数でエラーが起こらないように、 -100dB を下限とする制限も設けています。
見た所、これまでとは少々異なっている様子です。 各周波数に広くエネルギーが分散しているのは同じですが、低い周波数の方がエネルギーが高く見えています。 LFSR を単体で解析した時には、すべての AC 成分が -24dB にそろっていたのですが、この解析結果では、 AC 成分の最大値が -24dB になっており、他の成分はすべて -24dB を下回っています。 また、 LFSR のクロック周波数の幅で、山が出来ています。
関連文献
ARM アセンブラで 32ビット定数をロードする [プログラム三昧]
ARM Cortex-M プロセッサでは、もちろんアセンブラでプログラムを書くことが出来ます。 基本的な動作として、32ビットの定数をレジスタに格納する操作について調べてみました。
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-M4 と Cortex-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 を使ったら
しかしながら、 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 をインストールする。 [プログラム三昧]
FX3 のソフトウェア開発キット (Software Development Kit; SDK) には、 eclipse が同梱されています。 せっかくソフトウェアを開発するのなら、どこかにリポジトリを立てて、共有しましょう。 リポジトリは、最近はやりの github を使ってみたいな。
github を使うには、 git クライアントが必要
FX3 SDK に同梱されている eclipse GALILEO には、 CVS クライアントがインストールされています。 そのため、そのままで、 CVS サーバのリポジトリに対してデータのやり取りを行うことができます。
ところが、この eclipse には、 git クライアントはインストールされていません。 必要であれば、自分でインストールする必要があります。 もちろん、 eclipse の他に git クライアントをインストールすれば、使えないことはありませんが、せっかくの統合開発環境ですから、 eclipse にインストールして使いたいところです。
eclipse 向けの git クライアントとして、 EGit というのが、あるので、これをインストールしてみます。
インストールするソフトウェアの場所を設定する
まず、最初に、 EGit が格納されている場所を指定します。 新たにソフトウェアをインストールするときには、メニューバーから "Help" → "Install New Software..." を選択します。
すると、ダイアログボックス "Install" が開きます。
"Work with:" テキストフィールドの隣にある "Add..." ボタンをクリックします。 すると、さらに "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 が再起動されるのを待ちます。
eclipse が再起動したら、 git インターフェイスが使用できるようになっています。 git の機能は、 "Project Explorer" のコンテキストメニューから、使用することができるようになっています。
RSA キーの場所を指定する
git のリポジトリに使おうとしている github は、 SSH2 の機能を利用して、ファイルのやり取りをしています。 SSH2 には、暗号に使われる秘密鍵と公開鍵が必要です。 これらの鍵の置き場所を指定するには、メニューバーの "Window" → "Preference" を選択し、 "Preferences" ダイアログボックスを開きます。
鍵の場所を指定するためには、 "Preferences" ダイアログボックスで、 "General" → "Network Connection" → "SSH2" を選択して、さらに "General" タブをクリックして、設定画面を呼び出します。
鍵のありかは、 "SSH2 home:" テキストフィールドで指定します。
デフォルトの状態では、ホームディレクトリの "ssh
" ディレクトリが選択されていますが、今回は、ほかの git クライアントも使っている ".ssh
" を指定します。
これで、鍵の置き場所が確保できましたので、公開鍵を github の設定画面で指定すると、リポジトリへのアクセスが簡単になります。
さいごに
ここで述べた手順は、新しい FX3 SDK が発行されるたびに必要で、リリースされるたびに行う作業となっています。 そのため、記録しておく必要に迫られたというのが、この文書の目的の一つです。 FX3 SDK で EGit を使いたいという方のお役に立てれば、幸いです。
関連文献
関連文献を並べてみましたが、私自身は、 WEB の情報をたよりに使っているので、文献を参照してはいません。 ほんの、ご参考までに。
Quartus II を Ubuntu 10.04 にインストールするお話 [プログラム三昧]
先ごろ,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 (アスキームック)
- 作者:
- 出版社/メーカー: アスキー・メディアワークス
- 発売日: 2010/05/31
- メディア: 大型本
"Twitter Friends" つくりました. [プログラム三昧]
Twitter でフォローしてきた ID .突如として他の人のつぶやきに現れた ID .「この方は,どのようなクラスタの方なのかしら.」という疑問の解決に少しでも役立てようと, "Twitter Friends" なる CGI を作成しました.
ID という個人に近い情報を表示するプログラムなので,使用例は掲載していません.ご自由にお試しください.
二つのアカウントのつながりを列挙したい
この CGI は,「自分とアノ人」などのように,二つの ID をつないでくれる ID を列挙してくれます.二つの ID の間に共通のフォロー関係が存在していれば,どのようなクラスタに属する人なのかをある程度は判断することができます.使い方は,簡単です.二つの Textbox のそれぞれに ID を入力して, Submit ボタンをクリックします.しばらくすると, follow/follower 関係ごとに二つの ID ともにつながりのある ID が表示されます.
参考文献
Windows で tar ファイルを展開する法 [プログラム三昧]
先日, python-twitter というモジュールを Windows で使っている Python に入れようと思い立ちました. python-twitter は, http://code.google.com/p/python-twitter/ で配布されている, Python で Twitter API を使用するためのラッパーライブラリです.
tar ファイルを展開せよ
このページの中に,インストール手順が記述されています.
- simplejson をインストールする
simplejson は,私が使っている Python 2.6 には,インストールされているらしいので,OK.
- ライブラリファイルをダウンロードする
リストに並んでいる, python-twitter-0.6-tar.gz をダウンロードしました.
- 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()
以上で,展開終了です.サフィックスから判断して,適切に扱ってくれました.
参考文献
この本によると, tarfile.TarFile.open() を使うと書かれていますが,オンラインマニュアルによると, tarfile.open() の方が推奨されています.おそらく, Python のバージョンの違いによるものだと思われます.
Android SDK で,ゲームを作る (3) [プログラム三昧]
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) [プログラム三昧]
前回は,ひとつのファイルに何もかもゴチャマゼに入れてしまいました.今回は, 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) [プログラム三昧]
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 を指定すると子どもが大きくなる
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>
余った部分いっぱいに子部品を広げたいこともあるでしょう.そんな時には, 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>
ちなみに, 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 の数値は,子供の取り分を表す
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 を宣言した場合
さて,ここまでは,マニュアル通りに動作することがわかりました.この時,すべての小部品には, 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>
今度は,すべての子部品の 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>
そこで,四つの子部品に "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 CGI で 掲示板みたいなものを作る~sqlite3モジュール編~ [プログラム三昧]
Python CGI で 掲示板みたいなものを作る~Ajax編~では、Ajaxという仕組みを利用してページ遷移を起こさない掲示板システムを作成しました。 しかし、SQLiteとのインターフェイスは、相変わらず"system"関数によるコマンドを呼び出しで、標準入力を通じてSQLコマンドを送り込み、標準出力から結果を受け取る方式になっていました。 Python CGI で作るアクセスカウンタ~sqlite3モジュール編~でsqlite3モジュールが使えるようになったので、掲示板もsqlite3モジュールを使用するように変更してみました。
データベースの構成
今までの掲示板は、一つのデータベースを使いまわしてきましたが、今回は別のデータベースを作成しました。 その理由は、文字エンコーディングの扱いが異なってきたからです。
カラム名 | タイプ | 内容 |
---|---|---|
time | NUMBER | 記録時刻を表す数値です。 |
description | TEXT | メッセージの内容をユニコードで表現した文字列です。 |
このテーブルに "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("&"); str = str.split("<").join("<"); str = str.split(">").join(">"); str = str.split('"').join("""); str = str.split("{").join("{"); str = str.split("}").join("}"); str = str.split("'").join("'"); 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"モジュールが標準で装備されたため、プログラムが楽にはなりましたが、マニュアルは、必要です。
参考文献
「チッターぽい」解説します [プログラム三昧]
「チッターぽい」作りましたで、作成したプログラムですが、忙しさにかまけて、全く整備していませんでした。 認証なんか装備していないにもかかわらず、いままで、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の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"と呼んでいるようです。
参考文献
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 とは認めていません。 そのため、 Python に BOM 付きのファイルを読ませようとすると、「ユニコードでは無い文字があるぞ。」と言って受け付けてくれません。
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ファイル版」のものと同じです。 これらをテキストファイルではなく、データベースの「行」として記録していきます。
カラム名 | タイプ | 内容 |
---|---|---|
time | REAL | 記録時刻を表す数値です。 |
url | TEXT | アクセス先のURL (document.URL) を記録します。 |
referrer | TEXT | 参照元のURL (document.referrer) を記録します。参照元が不明な場合には、 "None" が入ります。 |
r_addr | TEXT | ページの表示を要求したクライアントのIPアドレス (REMOTE_ADDR) です。 |
r_host | TEXT | クライアントが申告したホスト名 (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 CGI で作る「秘密鍵・公開鍵」発行システムの構想 [プログラム三昧]
アクセス・カウンタを作ろうと、ログ・ファイルを作成するところまで、たどり着きました。 でも、ログ・ファイルをそのまま公開するのは危なそうです。
お見せしたいのは、やまやまなれど
ログファイル版アクセス記録を作成しました。 そこで、みなさまに、どんな情報が収集できたかお見せしたいのですが、どうも公開すると差し障りのありそうな内容になってしまいそうです。 特に HTTP_REFERRER には、検索エンジンに与えたキーワードが含まれているので、「○○○社が○○○というキーワードで検索をかけてきて、○○○という情報に興味を示した。」という事までわかってしまいます。 場合によっては、「売れる情報」です。
そのため、すべての情報を公開するようなまねはしません。 ただ、このアクセス・カウンタを使ってみたいと思われる方もいらっしゃると思いますので、公開できる情報だけ表示するシステムを考えてみました。
需要1 : ログを見てみたいだけ
興味だけで、どんなログが表示されるか見たい方むけには、見に来た方に関連した情報だけをお出しします。 具体的には、「見に来た方のIPアドレス」と「アクセス記録のIPアドレス」が一致する情報だけ抜き出して表示するページを考えています。 もっとも、「抜き出す」作業が必要なので、データベース版を作った後の話ですが。 当アクセス記録に参加している BLOG に出たり入ったりして、記録のされ方を確認することが出来ます。 あっ、出たほうは記録に残らないか。
この方法にも、問題点が無いわけではありません。 それは、一般的なプロバイダでは DHCP (Dynamic Host Configuration Protocol) を使っているので、「見に来た方のIPアドレス」は決して固定では無いという事です。 そのため、ネットワークが物理的に近い人のアクセス記録がたまたま見えてしまう可能性もあります。 また、LANにゲートウェイを設置している企業などの場合にも、大域IPアドレスが数個に限定されると思いますので、社内の多くの端末からのアクセス記録が見えてしまいます。
まあ、DHCPがコロコロ変更される状況は、そうそう起こらないだろうし、社内からのアクセス記録が社内の他の人に見えると困るという状況(それはそれで、別の問題がありそう。)も考えにくいと思います。 データベース版を作成したら、最初にこのタイプのアクセス記録表示ページを作成しようと思っていますので、懸念のある方は、あらかじめお知らせください。
需要2 : 自分のWEBページ(BLOG)のアクセス記録を取りたい
このアクセス・カウンタの本来の目的は、こっちです。 もちろん、自前のレンタル・サーバを用意すれば、やりたい放題ですが、そこまでコストをかけずに、"noritan.org"のデータベースを共同利用する方法もあります。 この場合、「見に来た人がオーナになっているWEBページに関連する情報だけ提供する」というのが、正しい姿だと思います。 いくつかアクセス記録参照の際の問題点を考えました。
- アクセス記録を参照する方法
あるWEBページのアクセス記録を参照する場合を考えます。 性善説に基づけば、「WEBのホスト名(と上位ディレクトリ)を入れてね。」だけで済むのですが、最近のインターネットは善人ばかりじゃないらしいので、この方法は使えません。
そこで、WEBページのオーナしか知らないアクセス・キー(秘密鍵と名づけます)を指定してアクセス記録を参照するようにします。 秘密鍵は、アクセス記録参照ページの URL に query を入れるだけで十分でしょう。 盗聴までは、考えないことにします。
- 秘密鍵を発行する方法
秘密鍵を発行する時、それほど、重いセキュリティが必要だとも思えないので、「あなたのWEBページに対応する秘密鍵は、コレです。」というメールを送れば十分です。 この方法は、簡単なのですが、「メール・アドレスの受け渡しをどうするか」「メール・アドレスが本人のものなのか」という問題が解決できません。
そこで、「秘密鍵発行ページ」を用意し、秘密鍵を発行すると同時に"noritan.org"の鍵サーバに登録する作業を自動で行います。 これで、WEBページのオーナしか知らない秘密鍵ができました。
- WEBページのオーナであることを証明する方法
WEBページのオーナが秘密鍵を受け取ることが出来ましたが、これだけでは、秘密鍵をどのWEBページに紐付けすればよいかが判断できません。 「秘密鍵発行ページ」で「WEBのホスト名(と上位ディレクトリ)を入れてね。」とやってしまうと、誰でも他人のWEBページのアクセス記録を見ることが出来ます。
「私が持っている秘密鍵はコレです。」という記事をWEBページに貼ってもらうと、簡単に確認できますが、秘密鍵を公開してしまうことになるので、意味がありません。 そこで、「秘密鍵発行ページ」で「秘密鍵」と同時に「公開鍵」を発行し、「公開鍵」の方をWEBページに貼ってもらいます。 「秘密鍵」と「公開鍵」を対にして鍵サーバに登録すれば、WEBページのオーナが持っている「秘密鍵」がどれなのかを判断することが出来ます。
- 他人が「公開鍵」を貼るのを防ぐ方法
「公開鍵」をWEBページに貼る方法には、欠点があります。 それは、『WEBページに貼られた「公開鍵」を別のWEBページにコピーすることができる。』ということです。
こういった場合、「本物は誰だ!!」と問われても、判断できません。 そこで、「公開鍵」を貼ると同時に当BLOGのコメントまたはトラックバックで「ここに貼りました。」と通知してもらいます。 これなら、もし、別の人が「公開鍵」をコピーしたとしても、コメントまたはトラックバックの到着順から本物を見分けることが出来ます。 つまり、「早い者勝ち」システムです。
もっとも、「公開鍵」をコピーした人は、「秘密鍵」を持っていないため、情報にアクセスすることはできません。 コピーした人に嫌がらせ以外の利益は無いはずなので、こういう事件は発生しないと思います。
- 「秘密鍵」と「WEBページ」の紐付け方法
ここは、手動で行います。 当BLOGオーナがコメントまたはトラックバックを確認して、紐付け情報を鍵サーバに登録します。 ここだけは、自動化しない方がきっと無難でしょう。
というわけで、人間も大いに介在するシステムが出来上がりました。
アクセス記録を参照するまでの手順
色々と問題点を並べましたが、すべて解決できそうなので、システム全体のおさらいです。
- ユーザが「鍵発行ページ」から鍵の発行を申請をする (1)
- 自動的に「公開鍵」と「秘密鍵」が鍵サーバに登録される (2)
- ユーザが表示された「公開鍵」と「秘密鍵」をメモする (3)
- ユーザが紐付けを希望するWEBページに「公開鍵」を記入し公開する (4)
- ユーザがDBオーナに「公開鍵」の公開をコメントまたはトラックバックで通知する (5)
- DBオーナが鍵サーバに「WEBページ情報」を登録する (6)
- ユーザが「秘密鍵」を使ってアクセス記録を参照する (7)
- アクセス記録参照ページが「秘密鍵」を認証し記録を表示する (8)
これ以外に「ユーザが自身のWEBページにJavaScriptを仕込む」というステップがありますが、これはいつでもかまいません。
以上の手順で安全にアクセス記録を参照できるようになると思います。 抜けは、ありませんかね。
参考文献
昔、この本で読んだエキスが少しは入っていると思う。
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
- 作者: D Flanagan
- 出版社/メーカー: Pragma
- 発売日: 2006/08/25
- メディア: ペーパーバック