Hatena::ブログ(Diary)

スコトプリゴニエフスク通信 このページをアンテナに追加 RSSフィード

2012-02-02

[][][]Jasyプロジェクトを作成する

前提条件

Python 3.2とpip、それにJasy 0.4.6がインストールされているものとします。

$ pip install https://github.com/wpbasti/jasy/zipball/0.4.6

ディレクトリ構成

最初にプロジェクトのトップディレクトリを作る。ここでは設定ファイルに記載するプロジェクト名にあわせてmyprojとした。git initは任意だが、coreがないとJasyアプリケーションは実行できないので、git submoduleとして参照しておくと便利。もちろんダウンロードして自分のレポジトリ内に含めてしまったり、hg等の別のVCSの方が好みならばgit submoduleに相当する手段で参照しても可。

$ mkdir myproj
$ cd myproj
$ git init

sourceというディレクトリを作り、さらにその下にclassとassetというディレクトリを作る。class以下がJavaScriptファイルの置き場所で、assetが画像ファイルやCSSファイルの置き場所。ディレクトリ名はsource/class, source/assetそのままを推奨。これはこのディレクトリ名がJasyが期待しているディレクトリ名であるということによる。

$ mkdir -p source/{class,asset}

最初に述べたようにCoreに依存しているので、external/coreというディレクトリにセバスチャンのgithubレポジトリへの参照を張った。このexternal/coreというパスに関しては後に述べるjasyscript.pyの中の設定を変えれば変更可。

$ git submodule add git://github.com/wpbasti/core.git external/core
$ cd external/core
$ git checkout 0.3.1 # 重要!!

重要な点としてはcoreのmasterは今のところ動かないので、0.3.1というタグが打ってあるバージョンを使うこと。submoduleではなくて0.3.1のzipを拾ってきたほうがよいかも。

次に、jasyproject.jsonというJSONファイルをプロジェクトのトップディレクトリに作り、nameにプロジェクト名を記述する。ここではmyprojとした。この名称は後々JavaScriptモジュール、クラスを定義していく際に、myproj.module1.Modelのような名前空間に使うので、org.example.my.great.appみたいな長い名称を付けると、コーディングの手間になるばかりかJavaScript実行時のパフォーマンスにも影響が出ると思う。短めでドットを含まないものが良いと思う。

    "name": "myproj"
}

ここまでのディレクトリ構成を示す。

.
├── external
│  └── core
├── jasyproject.json
└── sou
    ├── asset
    └── class

HTML

source/index.htmlに以下のようなHTMLを追加。

<!DOCTYPE HTML>
<html>
<head>
  <title>My First Jasy App</title>
</head>
<body>
  <h1>Hello Jasy</h1>
  <div id="content"></div>
  <script type="text/javascript" src="loader.js"></script>
  <script type="text/javascript">core.io.Script.load("myproj-" + core.Env.CHECKSUM + ".js");</script>
</body>
</html>

何の変哲もないHTMLファイルだが、scriptの部分だけに関してポイントを述べると、

  • loader.jsというのがアプリ起動に最小限必要なbootstrapコード
    • loader.jsはJasyで自動生成される。
    • 後に述べるjasyscript.pyの中で変更すればloader.jsでなくても可。
  • core.io.Script.loadでアプリケーションの実体のスクリプトを読み込む。
    • この実体スクリプトはJasyで自動生成される。

JavaScript

source/class/App.jsに以下のようなJavaScriptを追加。

core.Class('myproj.App', {
    construct: function() {
        alert("Hello Jasy");
    }
});

jasyscript.py

プロジェクトのトップディレクトリにjasyscript.pyという名前で以下のようなPython3スクリプトを作る。

#!/usr/bin/env jasy

def getSession():
    session = Session()

    # 依存しているJasyプロジェクトのディレクトリを指定
    session.addProject(Project("external/core/"))
    session.addProject(Project("."))

    return session

@task
def clean():
    logging.info("Clearing cache...")
    session = getSession()
    session.clearCache()
    session.close()

@task
def build():
    session = getSession()

    # プロジェクトのビルド設定
    session.setField("es5", True)
    session.permutateField("debug")

    # Asset(画像, CSS等)の設定
    resolver = Resolver(session.getProjects())
    resolver.addClassName("myproj.App")
    assets = Asset(session, resolver.getIncludedClasses()).exportBuild()

    # 起動スクリプトをbuild/loader.jsに出力
    includedByKernel = storeKernel("build/loader.js", session, assets=assets)

    # Asset以外に必要な静的ファイルをビルドディレクトリにコピーする
    for staticFile in ["index.html"]:
        updateFile("source/%s" % staticFile, "build/%s" % staticFile)

    # 最適化オプションを指定
    optimization = Optimization("variables", "declarations", "blocks", "privates")
    formatting = Formatting()

    # 起動後に最初に実行されるスクリプトを指定
    bootCode = "new myproj.App();"

    # 設定ごとにbuild/myproj-{{ hexdiget }}.jsというファイルを出力する
    for permutation in session.getPermutations():

        # 依存しているクラスを解決
        resolver = Resolver(session.getProjects(), permutation)
        resolver.addClassName("myproj.App")
        resolver.excludeClasses(includedByKernel)

        # 必要なクラスのみ圧縮して書き出し
        classes = Sorter(resolver, permutation).getSortedClasses()
        compressedCode = storeCompressed("build/myproj-%s.js" % permutation.getChecksum(), classes,
            permutation=permutation, optimization=optimization, formatting=formatting, bootCode=bootCode)

    session.close()

作成したら実行権限を付与してビルドを実行。

$ chmod +x jasyscript.py
$ ./jasyscript.py build

もしくは、Jasyをインストールしたときに同時にインストールされるjasyコマンドで、

$ jasy jasyscript.py build

でもOK。以下のようなビルドのログは出力されたあと、buildディレクトリに生成物ができているはず。

% ./jasyscript.py build
>>> Jasy 0.4.6
>>> Initialized project core
>>> Initialized project myproj
>>> Detecting dependencies...
>>> Publishing files...
>>> Updated 0/0 files
>>> Detecting dependencies...
>>> Sorting classes...
>>> Compressing 13 classes...
>>> Detected 2 possible permutations
>>> Detecting dependencies...
>>> Sorting classes...
>>> Compressing 11 classes...
>>> Detecting dependencies...
>>> Sorting classes...
>>> Compressing 6 classes...
>>> Closing session...

build/index.htmlをブラウザで開けば、App.jsに書いたスクリプトが実行される。最終的なディレクトリ構成は以下のとおり。

.
├── build
│├── index.html
│├── loader.js
│├── myproj-b490a0744.js
│└── myproj-b4f8b078f.js
├── external
│└── core
├── jasycache
├── jasyproject.json
├── jasyscript.py
└── source
    ├── asset
    ├── class
    └── inde.h

よくある質問と回答

jasycacheって何ですか

JasyがJavaScriptのソースを解析するときに作るキャッシュファイルです。レポジトリに含める必要はありません。.gitignoreやsvn:ignoreでレポジトリからは外しましょう。

jasyscript.py buildで"Invalid indention in doc string at line 25"のようなエラーが出ます

アルファ版のJasy 0.5を使っているためです。しばらくJasy 0.4.6を使う方が無難です。

どうしても最新版を使いたい人は治るのを気長に待ちましょう。もしくはPush Requestを送りましょう。

shelveの"dbm.error: db type could not be determined"というエラーが出ます

Macのgdbmのバージョンの問題だと考えられます。Python 3.2の次のリリースで修正されると思いますが、それを待てない人は以下のエントリを参照してください。

[][][]Jasyを使ってJavaScriptアプリケーションを作成する - 最適化

Jasy/Coreの使い方をなんとなく語るシリーズの第一回。順番が間違っている気がするが、第一回はJasyのJavaScript最適化機能について。

解説の順番が間違っている気がするが、Python3.2とpipをインストールした上で、

$ pip install https://github.com/wpbasti/jasy/zipball/0.4.6
$ git clone git://github.com/csakatoku/my-jasy-sandbox.git
$ cd my-jasy/sandbox/helloworld
$ jasy jasyscript.py build

ビルドを行なっていただければ、Jasyがどんなものか何となく分かると思う。


Jasyは"variables", "declarations", "blocks", "privates"の4つの最適化オプションを提供している*1。それらがどのようなものかをざっと説明する。僕は"variables"と"privates"オプションが気に入っていて、あえて地雷を踏みに行くJasyを使うメリットの一つだと思っている。

なお以下の例にあるJasy適用後のJavaScriptコードは著者によって読みやすいようにインデントして、コメントを追加してあります。実際の出力は無駄なホワイトスペースとコメントはすべて除去されます。

privates

__で始まるプロパティやメソッドを、以下の__fRv76ように一意でランダムなプロパティ名に書き換えてくれる。ちなみに、Jasyでは__で始まるプロパティやメソッドはプライベート扱いになり、これらのプロパティ、メソッドに外からアクセスしようとするとビルド時にエラーになる。

最適化前。

core.Class("p.helloworld.App", {
  construct: function(settings) {
    $("#content").html(this.sayHello());
  },

  members: {
    __name : 'Jasy',

    sayHello: function() {
        return "Hello, " + this.__name + "!";
    }
  }
});

Jasy適用後。

core.Class("p.helloworld.App", {
  construct: function() {
    $("#content").html(this.sayHello())
  },
  members: {
    __fRv76: "Jasy", // プロパティ名がランダムでユニークな文字列になっている
    sayHello: function() {
      return "Hello, " + this.__fRv76 + "!"
    }
  }
});

blocks

ifブロックを二項演算子を使って短縮してくれる機能。

適用前。

core.Class("p.helloworld.App", {
  construct: function(settings) {
    var __spam = "spam";
    var __egg = "egg";

    if (__spam) {
      console.log(__spam);
    } else {
      console.log(__egg);
    }
  }
});

適用後。

core.Class("p.helloworld.App", {
  construct: function() {
    var __spam = "spam";
    var __egg = "egg";
    __spam ? console.log(__spam) : console.log(__egg) // ifが消えた
  }
});

variables

ローカル変数を短縮してくれる機能。

適用前。

core.Class("p.helloworld.App", {
  construct: function(settings) {
    var __spam = "spam";
    var __egg = "egg";

    if (__spam) {
      console.log(__spam);
    } else {
       console.log(__egg);
    }
  }
});

適用後。

core.Class("p.helloworld.App", {
  construct: function() {
    var a = "spam"; // 長い変数名がaに変わっている
    var b = "egg";
    if (a) {
      console.log(a)
    } else {
      console.log(b)
    }
}});

declarations

変数宣言を短縮してくれる機能。

core.Class("p.helloworld.App", {
  construct: function(settings) {
    var __spam = "spam";
    var __egg = "egg";

    if (__spam) {
      console.log(__spam);
    } else {
      console.log(__egg);
    }
  }
});

適用後。

core.Class("p.helloworld.App", {
  construct: function() {
    var __spam = "spam", __egg = "egg"; // varがひとつ減っている
    if (__spam) {
      console.log(__spam)
    } else {
      console.log(__egg)
    }
  }
});

全部適用

全部の最適化を適用すると元の236バイトのスクリプトが170バイトになった。

core.Class("p.helloworld.App",{construct:function(){$("#content").html(this.sayHello())},members:{__fRv76:"Jasy",sayHello:function(){return"Hello, "+this.__fRv76+"!"}}});

Jasyで最適化できないこと

デッドコード削除は現状できていないし、ロードマップにあるのかも分からない。例えば、少しトリビアルな例だが、

function f() {
  if (0) {
    return "spam";
  } else {
    return "egg";
  }
}

は、if(0)が絶対成立しないので、

function f() { return "egg" }

と同等であるがJasyはこの部分を除去してくれない。

ただ、開発版でのロギングといったコードの削除ならば、多少冗長なのだが、core.Env.isSet("debug") で条件分岐を作ればリリース版では除去することができる。

function f() {
  if (core.Env.isSet("debug")) {
    // 開発版だけで行う処理
    console.log("very very verbose debug log");
  }
  // 何らかの処理
  alert("hello world");
}

適用結果。

function f() {
  alert("hello world");
}

[][][]Jasyを使ってJavaScriptアプリケーションを作成する - セバスチャン現る

JasyはZyngaの俊英Sebastian WernerによるJavaScriptビルドツール(Python3製)。coreは同氏によるJavaScript Framework。

coreが大規模なJavaScriptアプリケーションを作るためのモジュール機構を提供し、jasyはcoreを使ったアプリケーションをビルドしたり、Asset(画像やCSSといったリソースファイル)を管理したりするという役割をになっている。単独でも使えるかもしれないが、そうするとあまりメリットはない。

個人的にはPython3を使った実用に耐えうるアプリケーションなんて初めて見たいので、びっくりした。

たいへん注目に値するプロジェクトなのだが、ドキュメントが本当になく、セバスチャン*2の頭の中にしかロードマップがなくて本当に泣ける。しかしながら、このZyngaで最も面白いゲームの魅力を世の中の人達に知ってほしいと思い、これからしばらく僕の苦闘を語ろうと思う。

  1. Jasyプロジェクトを作成する
  2. 最適化

苦闘は今も以下のレポジトリで続いています。

*1:4.6 stable, 5.0 alpha現在

*2:全然面識がないの勝手にファーストネームで呼んでいる

2012-01-27

[][]Selenium WebDriverでUser-Agentを変更する

しばらく前は「Webアプリケーションのテスト自動化にはSeleniumよりWatirの方がよくね?」と思っていたが、Selenium2になってWebDriverが使えるようなり、Rubyで書かなくてはならないWatirより、Pythonで書けるSeleniumの方が断然楽だと感じている。

仕事柄モバイルサイトの構築に関わることが多く、SeleniumなりWatirなりでUser-Agentをモバイルブラウザのものに変更したいと思うことが多いのだが、幸いにしてSeleniumの方はちゃんとドキュメントに書いてあった。(かなり前にWatirで同じことをやろうとして全然分からなくて挫折したのだが、今はどうなっているか調べていない。)

ドキュメントはJavaのコード例だけど、Pythonならば以下のような感じ。

from selenium import webdriver
from selenium.webdriver.firefox.firefox_profile import FirefoxProfile

profile = FirefoxProfile()
profile.set_preference("general.useragent.override",
    "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3")
driver = webdriver.Firefox(profile)

driver.get("http://t.gree.jp/")

しかし本当はSafariでUser-Agentを変更してWebDriverを使いたいんだよなぁ・・・。

2012-01-13

[][]shelveの"dbm.error: db type could not be determined"というエラー

MacPortで入れたPython3.2.2*1でshelve.openを使おうとして、"dbm.error: db type could not be determined"というエラーが出た。

 % python
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import shelve
>>> shelve.open("jasycache")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.2/lib/python3.2/shelve.py", line 232, in open
    return DbfilenameShelf(filename, flag, protocol, writeback)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.2/lib/python3.2/shelve.py", line 216, in __init__
    Shelf.__init__(self, dbm.open(filename, flag), protocol, writeback)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.2/lib/python3.2/dbm/__init__.py", line 83, in open
    raise error[0]("db type could not be determined")
dbm.error: db type could not be determined

これは、下記のURLの回答にある通り、gdbm 1.9以降がインストールされている環境で起こるPythonのバグらしい。

PythonのBTSには既に上がっており、修正もされているので、次のリリース(3.2.3)では修正されるだろう。

それを待てない場合は、修正箇所はdbm/__init__.pyの一行だけなので、手動で以下のpatchをあてれば直る。

*1:2.7や3.1でもおこるらしい

2012-01-11

[][]Misakaを使ってmarkdown文書をHTMLに変換する

Misaka is a Python (2.7 and 3.2) and PyPy (1.6) binding for Sundown. And Sundown is a Markdown library written in C and it's really fast.

http://misaka.61924.nl/

MisakaはCで書かれたmarkdown parserであるSundownPythonバインディング。要Python2.7/3.2、PyPy1.6以上。

インストールは、

$ pip install misaka

Misakaのサイトに書いてある通り、使い方はとても簡単。

例えば、

# -*- coding: utf-8 -*-                                                                                                                                                                   
import misaka                                                                                                                                                                                                                                                                                                                                                                       
content = misaka.html(u"""
# ヘッダー

Hello, world! 

## ヘッダー2

Hello, markdown! Hello, sundown! 

- https://github.com/tanoku/sundown
- http://misaka.61924.nl/
""", extensions=misaka.EXT_AUTOLINK)

print(content)

のようなスクリプトの出力は次のようになる。

$ python hellomisaka.py
<h1>ヘッダー</h1>

<p>Hello, world!</p>

<h2>ヘッダー2</h2>

<p>Hello, markdown! Hello, sundown!</p>

<ul>
<li><a href="https://github.com/tanoku/sundown">https://github.com/tanoku/sundown</a></li>
<li><a href="http://misaka.61924.nl/">http://misaka.61924.nl/</a></li>
</ul>

SundownはUTF-8の文字列を問題なく扱えるようなので、misakaも同じくUTF-8ならば日本語も何ら問題がないようだ。

2011-12-11

[][]LZOとSnappyの圧縮効率・処理速度を比較する

LZOSnappyは共に、圧縮効率は他に劣るものの、処理速度が早いことを売りにした圧縮ライブラリ。

インストール

Pythonで使うならば、LZOに関しては python-lzo-static を使うの楽なのだろうか?

$ hg clone https://bitbucket.org/james_taylor/python-lzo-static
$ cd python-lzo-static
$ pythnon setup.py install

SnappyPyPIに登録されているので、もしSnappyがインストールされているならば*1

$ pip install python-snappy

で行ける。

検証

検証には青空文庫の芥川龍之介『トロッコ』のテキスト(10263byte)を使った。

検証スクリプト。

# -*- coding: utf-8 -*- 
import timeit
import zlib
import bz2
import lzo
import snappy

content = open('torokko.txt').read()
print 'original content length', len(content)
                                                                                                                        
for lib in (zlib, bz2, lzo, snappy):
    timer = timeit.Timer('lib.compress(content)', 'from __main__ import lib, content')
    result = timer.timeit(number=1000)

    compressed = lib.compress(content)
    print lib.__name__, result, len(compressed), ('%0.4f' % (len(compressed) / float(len(content))))

結果。

original content length 10263
zlib 0.546717882156 5270 0.5135
bz2 4.33483314514 4857 0.4733
lzo 0.130795001984 6950 0.6772
snappy 0.0607089996338 7133 0.6950

すなわち、

圧縮ライブラリ 処理時間 圧縮率
zlib 0.546717882156 0.5135
bz2 4.33483314514 0.4733
lzo 0.130795001984 0.6772
snappy 0.0607089996338 0.6950

のようになった。

timeitの使い方ってこれでいいんだっけ?という感じで自信がないのだが、もし上記計測結果が正しいならば、

  • 圧縮効率はbz2, zlib, lzo, snappyの順番で良いが、bz2とzlibの間の差、lzoとsnappyの間の差は微差。bz2, zlibのグループとlzo,snappyのグループの間の差は有意な差がある。
  • 圧縮速度はsnappy, lzo, zlib, bz2の順番で良く、それぞれの間に有意な差がある。

と言ってよいのではないだろうか。

次の課題

LZOとSnappyの処理速度の差は圧倒的に見えるが、LZOの存在意義って何だろうかを考える。こことかにLZO圧縮を使うMemcacheクライアントライブラリとかがあるが、こいつの存在意義って何だろうか考える。

*1:例えばdebianの場合は、sudo apt-get install libsnappy-dev