JavaScriptの値をローカルファイルに保存する方法について調べた

はじめに

Webブラウザーからローカルファイルを読み込みたいときはFile APIを使えば良さそうだけど、逆にデータをローカルファイルに保存したいときはどうしたら良いんだろう?ってことで、ちょっと調べてみました。

方法1

<a>タグのhrefに、保存したいデータを流し込む方法がありました。以下のHTMLファイルをブラウザー上で開き、何かメッセージを入力して保存ボタンをクリックして下さい。すると、ファイル保存を促すリンクが現れるので、右クリックでリンク先を保存して下さい。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>Sample</title>
        <script type="text/javascript">
            function save () {
                var data = document.getElementById("text").value;
                if (data.length) {
                    var obj = document.getElementById("anchor");
                    obj.href = "data:application/octet-stream," +
                               encodeURIComponent(data);
                    obj.innerHTML = "右クリックでリンク先を保存して下さい";
                }
            }
        </script>
    </head>
    <body>
        <textarea id="text" cols="30" rows="5" wrap="soft"></textarea><br>
        <button onclick="save();">save</button>
        <a id="anchor" href=""></a>
    </body>
</html>

以下のように、メッセージを入力して保存ボタンを押すとリンクが表示されます。

表示されたリンクを右クリックして「リンク先を別名で保存」を選ぶと以下のようなダイアログが表示され、ローカルファイルとしてメッセージを保存することができました。ですが、ファイル名は指定できないみたいです。

方法2

方法1だと右クリックから保存を選ばせたり、ファイル名が指定できなかったりと、いろいろ不便なので別の方法を調べてみました。
ActionScriptのExternalInterfaceではJavaScriptActionScriptが連携でき、ActionScriptのFileReference.save()ではActionScriptのデータをローカルファイルに保存できるということが分かりました。ASは一度も触ったことがなかったので、勉強代わりに「ASを経由してJSのデータを保存するプログラム」を書いてみました。
先に結果だけ言うと、以下のサンプルは動かないです(^_^;)

まずは、以下のActionScriptをWriteFile.asという名前で保存します。

  • WriteFile.as
package {
    import flash.display.Sprite;
    import flash.events.MouseEvent;
    import flash.external.ExternalInterface;
    import flash.net.FileReference;

    public class WriteFile extends Sprite {
        private var fileBody:String = "";
        private var fileName:String = "";

        function WriteFile () {
            // JSのwrite()が呼び出されるとASのbridge()を呼び出すように登録する
            ExternalInterface.addCallback("write", bridge);
            // マウスクリックイベントを検出するとdoWrite()を呼び出すように登録する
            this.addEventListener(MouseEvent.CLICK, doWrite);
        }

        private function bridge (body:String, name:String):void {
            // 保存するデータとファイル名を変数に保持する
            fileBody = body;
            fileName = name;
            // マウスクリックイベントを発生させる
            this.dispatchEvent(new MouseEvent(MouseEvent.CLICK));
        }

        private function doWrite (eventObj:MouseEvent):void {
            var fr:FileReference = new FileReference();
            try {
                // ローカルファイルに保存できるか試みる
                fr.save(fileBody, fileName);
            } catch (error:Error) {
                // 保存できなければアラートを表示する
                ExternalInterface.call("function(){javascript:alert('だが断る " + error.message + "')}");
            }
        }
    }
}

このWriteFile.asでは、

  1. JSのwrite()メソッドが呼び出されるとASのbridge()メソッドを呼び出す
  2. 保存したいファイルの名前と内容をASの変数にコピーする
  3. マウスクリックイベントを発生させる
  4. マウスクリックイベントを検出してASのdoWrite()メソッドを呼び出す
  5. JSのwrite()メソッドから受け取った内容をローカルファイルに保存する

という流れでローカルファイルにデータを保存しています。3, 4の処理は無駄なように見えますが、FileReference.save()メソッドは

マウスクリックまたはキー押下イベントのイベントハンドラなど、ユーザイベントに応答する形でのみ実行してください。それ以外の場合はエラーが発生します。

書かれていたので3, 4の処理を行っています。


次に、WriteFile.asをコンパイルします。

 % mxmlc WriteFile.as
設定ファイル "/usr/local/flex_sdk_4.1/frameworks/flex-config.xml" をロードしています
/Library/WebServer/Documents/writefile/WriteFile.swf (946 バイト)


そして、コンパイルしてできたWriteFile.swfファイルと同じ階層に以下のHTMLファイルを置いてWebブラウザーからアクセスして下さい。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <script type="text/javascript">
            function save () {
                var body = document.getElementById("fbody").value;
                var name = document.getElementById("fname").value;
                if (!body || !name) return false;
                var wf = document["wf"];
                wf.write(body, name);
            }
        </script>
    </head>
    <body>
        <embed src="WriteFile.swf" name="wf" allowScriptAccess="always" width="0" height="0" />
        File Name : <input id="fname" type="text"><br>
        <textarea id="fbody" cols="30" rows="5" wrap="soft"></textarea><br>
        <button onclick="save();">save</button>
    </body>
</html>


以下のように、適当にファイル名とファイルに保存したいメッセージを書いてsaveボタンを押して下さい。

すると、ファイル保存ダイアログが表示されるはずですが…

WriteFile.as内のFileReference.save()で例外が投げられので、上のようなアラートが表示されてファイル保存に失敗しました。アラートを表示する際、Errorクラスのmessageプロパティも一緒に表示するようにしています。上の画像の『Error #2176』の部分です。このページによると、エラーコード2176というのは、

ポップアップウィンドウを表示するアクションなどの特定のアクションは、マウスをクリックしたりボタンを押したりするユーザー操作によってのみ呼び出すことができます。

と、言う事らしいです。これは「dispatchEvent()メソッドで発生させたマウスクリックイベントではFileReference.save()が実行できない」という意味だと思います。

おわりに

方法1だと"一応"ローカルファイルに保存することができました。方法2だとActionScriptと連携しているため、実行時に制限がありローカルファイルに保存することができませんでした。次は、ちゃんとしたマウスクリックイベント発生させ、ローカルファイルにデータを保存するプログラムを書いてみようと思います。

hg commitを少しだけ楽しくする方法

以下のページを見て、gitでこんなことができるならMercurialでもできるのでは?っと思ったのでパクって試してみました!
コミットが楽しくなるgitハック『Happy Git Commits』がなんとも素敵な件 | IDEA*IDEA


上記のgitの方法では音楽ファイルの再生にafplayコマンドを使用していますが、私はMac以外でも使えるmpg123コマンドを使用します。

コミット時に再生したい音楽ファイルを用意し、Mercurialの設定ファイルであるhgrcに以下の内容を追加してください。リポジトリごとに再生するファイルを変えたいのであれば(各リポジトリへのパス)/.hg/hgrcに、すべてのリポジトリで同じファイルを再生したい場合は$HOME/.hgrcを編集して下さい。

[hooks]
# mpg123がインストール済みなら
commit = `mpg123 -q ~/Music/hg/fanfare.mp3 > /dev/null &`
# mpg123がインストールされていないなら
#commit = `afplay ~/Music/hg/fanfare.mp3 > /dev/null &`

作業はこれだけです。あとはコミットするたびに(・∀・)ニヤニヤできるか確認して下さい。
ちなみに、私はFF7のファンファーレの最初の数秒間を切り出したファイルを再生して(・∀・)ニヤニヤしてますw

WebSocket Draft 76でechoサーバーを作ってみた

もう3ヵ月近く前ですが、WebSocketプロトコルのDraft76が公開されました。このDraft76では、それまでのDraft75とハンドシェイクの内容が変わっていました。
draft-hixie-thewebsocketprotocol-76 - The WebSocket protocol

ということで、新しい仕様とPythonの勉強がてらにechoサーバーを書いてみました!実行環境は以下の通りです。

以下、サーバー側のソースコード(WSP76_echo.py)です。githubとか使えよ!って感じはしますが、ただのサンプルなので面倒くさ(ry

#!/usr/bin/env python
#-*- coding:UTF-8 -*-

from optparse import OptionParser
import SocketServer
import struct
import hashlib

class MyTCPHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        '''クライアントとTCP接続したときに呼び出される
        '''
        self.ishandshake = False
        self.CLIENT_TERMINATED = False
        while True:
            self.data = self.request.recv(1024)
            if self.ishandshake:  # 既にハンドシェイクに成功している場合
                if len(self.data):
                    self.checkDataFrame()
                else:   # Chrome 6用の処理
                    self.CLIENT_TERMINATED = True
                if self.CLIENT_TERMINATED:
                    print '/client terminated/フラグがセットされたのでコネクションを切断します'
                    self.closeConnection()
                    break
                else:
                    print '[%s:%s] %s' % (self.client_address[0], self.client_address[1], self.RAW_DATA)
                    if len(self.RAW_DATA) != 0:
                        self.request.send('\x00' + self.RAW_DATA + '\xFF')
                        print self.RAW_DATA
                        self.RAW_DATA = ''
            else:  # まだハンドシェイクに成功していない場合
                if self.checkHandshake() and self.sendHandshake():  # クライアントとハンドシェイクを試みる
                    self.ishandshake = True
                    print '%s:%s とのハンドシェイクに成功' % (self.client_address[0], self.client_address[1])
                else:
                    print '%s:%s とのハンドシェイクに失敗' % (self.client_address[0], self.client_address[1])
                    self.data = ''
                    self.closeConnection()


    def checkDataFrame(self):
        '''ハンドシェイク後に受信したデータを解釈する関数

        受信したデータフレームの/type/を調べtext-frameかbinary-frame, closing-frameを
        判断してデータをRAW_DATAに入れる。
        '''
        self.TYPE = self.data[0]
        if ord(self.TYPE) & 0x80:  # /type/の上位ビットがセットされている場合
            if self.TYPE != '\xFF':
                self.CLIENT_TERMINATED = True
            else:  # /type/が0xFFの場合
                self.LENGTH = 0
                tmp = 1  # 既に読み込んだバイト数を保持する変数
                for self.B in self.data[1:]:  # 最初の1バイトは/type/に入れたので無視する
                    tmp += 1
                    if self.B != '\x00':  # binary-frameのLengthを求める
                        self.CLIENT_TERMINATED = True
                        self.B_V = ord(self.B) & 0x7F
                        self.B_V += self.LENGTH / 128
                        self.LENGTH = self.B_V
                        if self.B & 0x80:
                            continue
                        else:
                            self.RAW_DATA = self.data[tmp:tmp+self.LENGTH]
                            print '%dバイトのデータを読み込んだ' % len(self.RAW_DATA)
                            break
                    elif self.LENGTH == 0:  # 0xFF 0x00のclosing-frameを受信したとき
                        print '%s:%s からclosing-frameを受信' % (self.client_address[0], self.client_address[1])
                        self.CLIENT_TERMINATED = True
                        break
        elif self.TYPE != '\x00':
            self.CLIENT_TERMINATED = True
        else:  # text-frameの場合
            self.RAW_DATA = ''
            for self.B in self.data[1:]:
                if ord(self.B) != 0xFF:  # text-frameの終わりでなければ
                    self.RAW_DATA += self.B
                else:  # text-frameの終わりなら
                    break


    def closeConnection(self):
        '''クライアントに対してclosing-frameを送信する関数
        '''
        print '%s:%s にclosing-frameを送信' % (self.client_address[0], self.client_address[1])
        self.request.send('\xFF\x00')
        self.finish()

                    
    def sendHandshake(self):
        '''クライアントにハンドシェイクを送信する関数
        '''
        # Sec-WebSocket-Locationフィールドの値を作成
        self.LOCATION = 'wss://' if SECURE_FLAG else 'ws://'
        self.LOCATION += HOST
        self.LOCATION += ':' + str(PORT) if PORT else ''
        self.LOCATION += RESOURCE_NAME

        # チャレンジ・レスポンス方式
        tmp = ''
        for c in self.KEY_1:
            tmp += c if c.isdigit() else ''
        self.KEY_NUMBER_1 = int(tmp)  # /key_1/内の[0-9]だけを取り出して/key-number_1/に入れる
        tmp = ''
        for c in self.KEY_2:
            tmp += c if c.isdigit() else ''
        self.KEY_NUMBER_2 = int(tmp)  # /key_2/内の[0-9]だけを取り出して/key-number_2/に入れる
        self.SPACES_1 = self.KEY_1.count(' ')  # /key_1/内の空白文字の数を/spaces_1/に入れる
        self.SPACES_2 = self.KEY_2.count(' ')  # /key_2/内の空白文字の数を/spaces_2/に入れる
        if self.SPACES_1 == 0 or self.SPACES_2 == 0:
            print 'Sec-WebSocket-Keyフィールドの値に空白文字が無かった'
            return False
        # /key-number_1/を/spaces_1/で除算したものを/part_1/に入れる
        self.PART_1, tmp = divmod(self.KEY_NUMBER_1, self.SPACES_1)
        if tmp != 0:  # 割りきれなかった場合
            print 'Sec-WebSocket-Key1フィールドの値が不適切'
            return False
        # /key-number_2/を/spaces_2/で除算したものを/part_2/に入れる
        self.PART_2, tmp = divmod(self.KEY_NUMBER_2, self.SPACES_2)
        if tmp != 0:  # 割りきれなかった場合
            print 'Sec-WebSocket-Key2フィールドの値が不適切'
            return False
        # /part_1/, /part_2/, /key_3/をくっつけたものを/chalenge/に入れる
        self.CHALLENGE = struct.pack('>I', self.PART_1)  # 値を32bitのビッグエンディアンのバイナリーにする
        self.CHALLENGE += struct.pack('>I', self.PART_2) # 値を32bitのビッグエンディアンのバイナリーにする
        self.CHALLENGE += self.KEY_3
        self.RESPONSE = hashlib.md5(self.CHALLENGE).digest() # /chalenge/のMD5 fingerprintを/response/に入れる

        # 送信するハンドシェイクデータの作成
        handshakeData = 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
        handshakeData += 'Upgrade: WebSocket\r\n'
        handshakeData += 'Connection: Upgrade\r\n'
        handshakeData += 'Sec-WebSocket-Origin: ' + ORIGIN + '\r\n'
        handshakeData += 'Sec-WebSocket-Location: ' + self.LOCATION + '\r\n'
        if PROTOCOL:
            handshakeData += 'Sec-WebSocket-Protocol: ' + PROTOCOL + '\r\n'
        handshakeData += '\r\n' + self.RESPONSE
        self.request.send(handshakeData)  # ハンドシェイクデータの送信

        print '>' * 10, '%s:%s にハンドシェイクデータを送信した' % (self.client_address[0], self.client_address[1])
        print handshakeData
        print '>' * 40

        return True
            
    def checkHandshake(self):
        '''クライアントから受信したハンドシェイクデータが正しいか調べる関数
        '''
        print '<' * 10, '%s:%s からハンドシェイクデータを受信した' % (self.client_address[0], self.client_address[1])
        print self.data
        print '<' * 40

        # ハンドシェイクの1行目を調べる
        fields = self.data.split('\r\n')
        field = fields[0].split()
        if field[0] != 'GET':
            print 'ハンドシェイクの1行目が GET から始まっていないので不適切'
            return False
        if field[1] != RESOURCE_NAME:
            print 'ハンドシェイクの1行目(/resource name/)が不適切'
            return False
        # 2行目以降の連続したフィールドをバラす
        blankLine = False
        d = dict()
        for field in fields[1:]:
            if field == '':
                blankLine = True
                continue
            if not blankLine:
                fieldName, fieldValue = field.split(': ', 1)
                d[fieldName.lower()] = fieldValue  # フィールド名は大文字と小文字を区別しない
            else:
                self.KEY_3 = field
                break
        # それぞれのフィールドを調べる
        if not 'upgrade' in d.keys() or d['upgrade'].lower() != 'websocket':
            print 'ハンドシェイク内のUpgradeフィールドが不適切'
            return False
        if not 'connection' in d.keys() or d['connection'].lower() != 'upgrade':
            print 'ハンドシェイク内のConnectionフィールドが不適切'
            return False
        tmp = HOST + ':' + str(PORT) if PORT != 80 else HOST
        if not 'host' in d.keys() or not d['host'] == tmp:
            print 'ハンドシェイク内のHostフィールドが不適切'
            return False
        if not 'origin' in d.keys() or d['origin'].lower() != ORIGIN:
            print 'ハンドシェイク内のOriginフィールドが不適切'
            return False
        if PROTOCOL:
            if not 'sec-websocket-protocol' in d.keys() or d['sec-websocket-protocol'] != PROTOCOL:
                print 'ハンドシェイク内のSec-WebSocket-Protocolが不適切'
                return False
        if not 'sec-websocket-key1' in d.keys() or not d['sec-websocket-key1']:
            print 'ハンドシェイク内のSec-WebSocket-Key1が不適切'
            return False
        else:
            self.KEY_1 = d['sec-websocket-key1']
        if not 'sec-websocket-key2' in d.keys():
            print 'ハンドシェイク内のSec-WebSocket-Key2が不適切'
            return False
        else:
            self.KEY_2 = d['sec-websocket-key2']

        return True


if __name__ == "__main__":
    usage = u'%prog [-p ポート番号] オリジン [-s サブプロトコル名] [-r リソース]'
    parser = OptionParser(usage=usage)
    parser.add_option('-p', '--port', dest='port', type='int', default=8080,help=u'ポート番号(デフォルトは8080)')
    parser.add_option('-s', '--subprotocol', dest='subprotocol', help=u'サブプロトコル名')
    parser.add_option('-r', '--resource', dest='resource', default='/', help=u'リソース')
    options, args = parser.parse_args()
    if len(args) < 1:
        parser.error('引数を1つ入力してください')
    elif len(args) > 1:
        parser.error('引数が多いです')

    HOST = 'localhost'
    PORT = options.port
    ORIGIN = args[0]
    PROTOCOL = options.subprotocol
    RESOURCE_NAME = options.resource
    SECURE_FLAG = False

    server = SocketServer.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
    print 'Ctrl-cで終了します'
    server.serve_forever()

次に、クライアント側のソースコード(clientSample.html)です。

<html>
    <head>
        <style type="text/css">
            .log {
                color: red;
            }
        </style>
        <script>
            ws = new WebSocket("ws://localhost:8080");
            ws.onopen = function (e) {
                var resultAreaObj = document.getElementById('result');
                resultAreaObj.innerHTML += '<span class="log">onopen</span>' + '<br>'
            };
            ws.onclose = function (e) {
                var resultAreaObj = document.getElementById('result');
                resultAreaObj.innerHTML += '<span class="log">onclose</span>' + '<br>'
            };
            ws.onmessage = function (e) {
                var resultAreaObj = document.getElementById('result');
                resultAreaObj.innerHTML += e.data + '<br>'
            };
            ws.onerror = function () {
                var resultAreaObj = document.getElementById('result');
                resultAreaObj.innerHTML += '<span class="log">onerror</span>' + '<br>'
            };
            send = function () {
                var textFieldObj = document.getElementById('textField');
                var data = textFieldObj.value;
                if (data) {
                    ws.send(data);
                    textFieldObj.value = '';
                }
            };
        </script>
    </head>
    <body>
        <input type='text' id='textField'/>
        <button onclick='send();'>send</button><br>
        <button onclick='ws.close();'>close</button>
        <hr>
        <div id='result'></div>
    </body>
</html>

これらのサンプルを動かすにはWebサーバーを立てて行う方法と、Webサーバーを立てずに行う2つの方法があります。

Webサーバーを立てて行う方法

まずは、上記のクライアント側のソースコードをclientSample.htmlという名前でDocumentRoot以下に置きます。そして、サーバー側のソースコードをWSP76_echo.pyという名前で保存し、以下のように実行します。第一引数にはハンドシェイク内のOriginフィールドで使用する文字列を指定します。

 % ./WSP76_echo.py "http://localhost"
Ctrl-cで終了します

echoサーバーが実行できたらWebSocket Draft76に対応したWebブラウザーからclientSample.htmlにアクセスします。

ハンドシェイクに成功すると、上のスクリーンショットのようにonopenと表示されます。

Webサーバーを立てずに行う方法

いちいちhttpd起動するの('A`)マンドクセっという方は、以下の方法でも動かすことができます。
まずはechoサーバーを起動します。Webブラウザーによって第一引数の値が変わるので注意して下さい。

Firefox 4の場合
 % ./WSP76_echo.py "file://"
Ctrl-cで終了します
Chrome 6の場合
 % ./WSP76_echo.py "null"
Ctrl-cで終了します

次に、clientSample.htmlを(ドラック&ドロップなどで)直接Webブラウザーから開いて下さい。ブラウザーのロケーションバーはfile://ファイルへのパス/clientSample.htmlという表示になっていると思います。このとき、onopenと表示されていればハンドシェイク成功です。

echoサーバーの使い方

上記の2つのどちらかの方法を行い、Webブラウザー上でonopenと表示されればハンドシェイクに成功しているので、入力フィールドに適当にメッセージを入力してsendボタンを押してみて下さい。以下のように、入力したメッセージと同じモノが表示されるはずです。また、closeボタンを押すとclose()メソッドを呼び出してWebSocketコネクションを閉じようとします。

このとき、echoサーバーを実行しているターミナルの画面は以下のような出力結果になっていると思います。

 % ./WSP76_echo.py "http://localhost"
Ctrl-cで終了します
<<<<<<<<<< 127.0.0.1:51688 からハンドシェイクデータを受信した
GET / HTTP/1.1
Host: localhost:8080
Connection: Upgrade
Sec-WebSocket-Key1: 36 7  95   56 48i9
Upgrade: WebSocket
Origin: http://localhost
Sec-WebSocket-Key2: (1wj61W97 74=- ! -&65%2

@=&#65533;&#65533;p&#65533;f[
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
>>>>>>>>>> 127.0.0.1:51688 にハンドシェイクデータを送信した
HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: http://localhost
Sec-WebSocket-Location: ws://localhost:8080/

a&#65533;{&#65533;^&x&#65533;&#65533;&#65533;K\&#65533;&#65533;V&#65533;
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
127.0.0.1:51688 とのハンドシェイクに成功
[127.0.0.1:51688] hello
hello
[127.0.0.1:51688] ハッピー
ハッピー
[127.0.0.1:51688] うれピー
うれピー
[127.0.0.1:51688] よろピクねーーーー
よろピクねーーーー
127.0.0.1:51688 からclosing-frameを受信
/client terminated/フラグがセットされたのでコネクションを切断します
127.0.0.1:51688 にclosing-frameを送信

もし、何らかの理由によりハンドシェイクが失敗するとWebブラウザーには何もメッセージが表示されませんが、echoサーバーを実行しているターミナルの画面には失敗した理由が出力されているはずです。

echoサーバー実行時のオプションについて

このechoサーバーは実行時にオプションを指定することで使用するポート番号、サブプロトコル、リソースを指定することができます。と言っても、ポート番号変更以外のオプションはハンドシェイク内の値が変わるだけで、echoサーバーとしての動作は何も変わりませんが(^^;)

 % ./WSP76_echo.py -h
Usage: WSP76_echo.py [-p ポート番号] オリジン [-s サブプロトコル名] [-r リソース]

Options:
  -h, --help            show this help message and exit
  -p PORT, --port=PORT  ポート番号(デフォルトは8080)
  -s SUBPROTOCOL, --subprotocol=SUBPROTOCOL
                        サブプロトコル名
  -r RESOURCE, --resource=RESOURCE
                        リソース

Chrome 6とFirefox 4の挙動の違いについて

echoサーバーのMyTCPHandlerクラスのhandle()メソッド内では、以下のような処理を行っています。

class MyTCPHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        '''クライアントとTCP接続したときに呼び出される
        '''
        self.ishandshake = False
        self.CLIENT_TERMINATED = False
        while True:
            self.data = self.request.recv(1024)
            if self.ishandshake:  # 既にハンドシェイクに成功している場合
                if len(self.data):
                    self.checkDataFrame()
                else:   # Chrome 6用の処理      ← ココの部分!
                    self.CLIENT_TERMINATED = True
                if self.CLIENT_TERMINATED:
                    print '/client terminated/フラグがセットされたのでコネクションを切断します'
                    self.closeConnection()
                    break
                else:
                    以下省略

この部分では、既にハンドシェイク済みのクライアントから受信したデータ(self.request.recv()メソッドの返り値)のサイズが0なら、Chrome 6用の処理としてself.CLIENT_TERMINATEDフラグを立てています。クライアントのChrome 6がWebSocketクラスのclose()メソッドを呼び出したとき、self.request.recv()メソッドは0を返します(クライアントがTCP接続を切断したって意味?)。
IETFdraft-hixie-thewebsocketprotocol-76 - The WebSocket protocolには、コネクションを閉じるときはclosing-frame(0xFFと0x00)を送信するって書いているので、クライアントがclose()メソッドを呼び出したときはrecv()メソッドの返り値のサイズが2になるはずなんですが、Chrome 6の挙動はそうじゃないので上記のような処理を行っています。ちなみに、Firefox 4ではちゃんとclosing-frameを送信してくれるので、checkDataFrame()メソッド内で/client terminated/フラグを立てることができます。

その他

今回作ったechoサーバーでは、Draft 76から追加されたチャレンジ・レスポンスの部分を以下のように書きました。

        # /part_1/, /part_2/, /key_3/をくっつけたものを/chalenge/に入れる
        self.CHALLENGE = struct.pack('>I', self.PART_1)  # 値を32bitのビッグエンディアンのバイナリーにする
        self.CHALLENGE += struct.pack('>I', self.PART_2) # 値を32bitのビッグエンディアンのバイナリーにする
        self.CHALLENGE += self.KEY_3
        self.RESPONSE = hashlib.md5(self.CHALLENGE).digest() # /chalenge/のMD5 fingerprintを/response/に入れる

/part_1/, /part_2/は"expressed as a big-endian 32 bit integer"、/response/は"the MD5 fingerprint of /challenge/ as a big-endian 128 bit string"と書かれていました。Pythonでバイナリデータの扱い方がよく分からなかったのでいろいろググった結果、上記のようなコードになりましたが、こんなコードで良いんですかね。ちゃんと環境に依存しないコードを書きたいんですが(^_^;)

Macのscreenコマンドでスクロール

個人的なメモですが久しぶりに日記を更新します。

以前使っていたMac OSX 10.4 Tigerでは.screenrcに以下の1行を加えれば、screenコマンド実行時にスクロールができて便利でした。
# いちいちscreenのコピーモードに切り替えたりするのって('A`)メンドクセ

termcapinfo xterm* ti@:te@

ですが、10.6では設定ファイルに追加してもダメでした。どうやらビルド時に--enable-color256を指定していなかったのが悪さしてたらしく、以下のページを参考にしながらcvs版のscreenをインストールしました。
Leopard で cvs版screen - bokuju とか tabe1hands の日記

ただのscreenならScreen - GNU Project - Free Software Foundationからソースを落としてきてビルドすれば良いと思いますが、どうせなら画面の縦分割が出来ると便利そうだったので、今回はcvs版のscreenをビルドしました。画面を縦分割にできると以下のようにmanページを見ながら作業したり、プログラムの実行結果を見ながらコーディングしたりできるので非常に便利です。

ですが、画面を縦分割した状態でスクロール(分割された領域の一つがスクロール)というのは出来ませんでした(-ω-;)ウムム
# 縦分割とスクロールが同時に出来たら最高なんだけどなぁ

コマンドラインから英和・和英翻訳をする

少し前に、Google翻訳サービスでインクリメント翻訳ができるようになりました。タイプするそばから翻訳されていくのは見てておもしろいし、いちいち翻訳ボタンを押さなくても良いので便利になりました。でも、ちょっと意味を確認したいだけなのに、Google翻訳のページに行くのが面倒くさい... (*゚0゚)ハッ!! Google翻訳で英和・和英翻訳するコマンドがあれば良くね?ってことで作りました。Pythonで。以下のソースをe2jというファイル名で保存してください。ちなみに実行環境はUbuntu 9.10、Python 2.6.4です。

#!/usr/bin/python
#-*- coding:utf8 -*-

import sys 
import urllib
import urllib2
import json
import pynotify

def translate(from_lang='en', to_lang='ja', word=''):
    url = 'http://ajax.googleapis.com/ajax/services/language/translate'
    values = {'v': 1.0,
              'q': word,
              'langpair': from_lang + '|' + to_lang}
    str_GET = urllib.urlencode(values)
    req = urllib2.Request(url, str_GET)
    res = urllib2.urlopen(req)
    data = json.loads(res.read())
    if data['responseStatus'] == 200:
        return data['responseData']['translatedText']
    else:
        msg = data['responseDetails'] if data['responseDetails'] else str(data['responseStatus'])
        return 'error: ' + msg 


if __name__ == '__main__':
    result = translate('en', 'ja', sys.argv[1])  # 英語を日本語に
    print sys.argv[1] + ':' + result
    pynotify.init('e2j')
    n = pynotify.Notification(sys.argv[1], result, "dialog-information")
    n.set_timeout(3000)  # 3秒だけ表示されるらしいが...
    n.show()

実行の仕方は、ファイルに実行権限を与えて

 % e2j system

と、実行すればsystemという単語の日本語訳が表示されます。このスクリプトで行っているのはGoogle翻訳APIを用いて翻訳を行い、JSON形式で返ってきた翻訳結果を表示します。ですが、結果の表示をコマンドラインに表示するだけなのはおもしろくなかったので、pynotifyというパッケージを使ってみました。Google翻訳APIについては以下のページを参考にしました。

pynotifyは /usr/share/doc/python-notify/examples の中にあったサンプルプログラムを参考にしました。
プログラムを実行すると、こんなのが画面の右上に現れるはずです。

左側のアイコンはプログラム中のpynotify.Notification()メソッドの第三引数で指定できます。具体的にどこからアイコン画像を取って来ているかわかりませんが、/usr/share/icons/gnome/scalable の中にあるactionsとかstatusemotesフォルダ内のアイコン名を指定するとアイコンが表示されます。

これで完成したと思ってたけど、set_timeout()メソッドで翻訳結果が表示される時間を指定できるらしいができてない orz
set_timeout()の値を大きくしても、小さくしても10秒間(デフォルト?)表示し続けてしまう(´・ω・`)ショボーン
10秒も表示されるのはウザいので、set_timeout(0)にした。こうすると、翻訳結果が以下のように表示されるようになる。

(´〜`)うーん、何で表示時間を指定できないのだろうか...。まぁ、とりあえずこれで完成ってことにしよう。ちなみに、和英にするにはtranslate()関数呼び出しの部分を、

result = translate('ja', 'en', sys.argv[1])  # 日本語を英語に

って書き換えるだけです。なので、仏日・日仏翻訳などもすぐに作れると思います。多分。

研究室の新PCにUbuntu 9.10をインストールした

研究室で使う用に新しいPCを購入してもらった。予算は10万ぐらいで、Webブラウジングとコーディングができれば良かったので以下の内容で組んだ。

ゲームとかするつもりはなかったのでマザボは買わなかった。だが、そのせいでUbuntuのインストールに苦労した。

Ubuntu 9.10 日本語リミックス をインストールしようとしたが画面が真っ暗になった。グラフィック周りに問題があるっぽいので、セーフティモードにしてインストールを試みた。無事にインストーラが起動してインストールが始まったが、インストール終了直前で画面が真っ暗になった\(^o^)/


9.10は諦めて9.04をインストールすることにした。9.04は問題無くインストールできたが、ちょうど良い解像度に合わなくて見にくい。/etc/X11/xorg.confをいじってみたけどダメだった orz


そこで、Ubuntuは諦めてたまたま手元にあったopenSUSE 11.2をインストールしたら最適な解像度で起動することができた!!でも、ちょっともっさり気味 orz


Ubuntuでちゃんと解像度が選べない問題をググってみたら相性(?)が悪かったみたいだ。マザボビデオチップIntel G43/G45はUbuntuのドライバ(vesa)ではサポートされてないみたいだ。Intelのオフィシャルからドライバのソースを取ってきてビルドしてインストールしようと思ったが、それもG43/G45はサポート外だった/(^o^)\


ちょいもっさりのopenSUSEで我慢しようかなって思ったが、研究室にNVIDIAのグラボ(GeForce 6600 T)が転がっているのを見つけたので差してみた。そしてNVIDIAのドライバをインストールすると最適な解像度になった!!


よかった。よかった。