引越しのおしらせ
こちらのはてな日記の内容は、 http://www.gembook.org に 移転しました。
2012-05-03
Zipファイル一個で実行可能なPythonアプリケーションを作ってみる
python, pkg_resources, flask, jinja2
アプリケーションを作成して配布するとき、配布するのは複数のファイルやディレクトリではなく、ファイル一つだけで済ませることができたらそれに越したことはないだろう。Pythonには cx_Freeze や py2exe のような、プラットフォーム固有な実行可能ファイルを開発するための環境も揃っているが、ここではもうちょっとライトに、Pythonの実行環境があれば実行できる zip ファイルの作り方を紹介したい。例として、flaskを使った簡単なWebアプリケーションを作ってみよう。
尚、ここで作成したファイルは https://github.com/atsuoishimoto/demo_pkgrsrc に置いてあるのでご参照いただきたい。
リソースファイルの使い方
普通、アプリケーションにはスクリプトファイル以外にいろんなファイルが含まれる。この例のようなWebアプリケーションではHTMLやCSSが必要だし、Flask では jinja2 のテンプレートファイルも必要となる。 Flask では、こういったファイルをアプリケーションのパッケージディレクトリ内に格納するようになっている。
<Pythonパッケージディレクトリ>
|- __init__.py
|- app.py
|
|-<template>
| |- index.html
| |- app.html
|
|-<static>
|- fav.ico
|- style.css
このような配置にしておけば、Flask が自動的に必要なファイルを読み込んでくれるのだが、アプリケーションを zipファイルに固めてしまうとその面倒を見てくれなくなってしまい、自分でなんとかしなければならない。
このようにパッケージを zipファイルとして作成する場合、データファイルを参照するときには Distribute (または Setuptools) のpkg_resources を使うと、パッケージがzipでもディレクトリでも、どちらでも正しくファイルを読み込むことができるようになる。
例えば、my_application パッケージ内の、static/default.cssというファイルが必要なら、
pkg_resources.resource_string("my_application", "static/default.css")
でzipファイルでもファイルシステム上のディレクトリでも取得することができる。
flaskでリソースファイルを使う
デフォルトでは、Flask はzipファイルからはスタティックファイルを読み込んでくれないので、ちょっとした細工をする必要がある。普通のFlaskアプリケーションでは
from flask import Flask app = Flask(__name__) @app.route('/hello/<name>') def hello(name=None): return render_template('hello.html', name=name) if __name__ == "__main__": app.run()
のようにFlaskオブジェクトを作成するが、ここではzipファイルからの読み込みをサポートするために send_static_file() メソッドを置き換えて pkg_resources のリソースAPIを利用するようにカスタマイズする必要がある。
import pkg_resources from flask import Flask, helpers class ZippedFlask(Flask): PACKAGENAME = "demo_pkgrsrc" def send_static_file(self, filename): fname = helpers.safe_join(self._static_folder, filename) f = pkg_resources.resource_stream(self.PACKAGENAME, fname) return helpers.send_file(f, attachment_filename=filename) app = ZippedFlask(__name__) if __name__ == "__main__": app.run()
jinja2でリソースファイルを使う
jinja2を使用する場合は、jinja2のテンプレートファイルもリソースAPIを利用して読み込むようにしなければならない。jinja2にはこの為のjinja2.loaders.PackageLoader() が用意されている。
import pkg_resources from flask import Flask, helpers class ZippedFlask(Flask): PACKAGENAME = "demo_pkgrsrc" @helpers.locked_cached_property def jinja_loader(self): return loaders.PackageLoader(self.PACKAGENAME) app = ZippedFlask(__name__) @app.route('/hello/<name>') def hello(name=None): return render_template('hello.html', name=name) if __name__ == "__main__": app.run()
zipファイルを使って実行する
パッケージディレクトリを圧縮してzipファイルを作ってしまえば、環境変数PYTHONPATHへの指定などからsys.pathに登録して、Pythonからインポートできるようになる。
$ zip -r myapp.zip demo_pkgrsrc $ export PYTHONPATH=myapp.zip $ python -m demo_pkgrsrc.demo
もうちょっとだけ便利なzipファイルにする
Python2.5以降では、__main__.pyというファイルを含むディレクトリやzipファイルはPythonコマンドにスクリプトファイルとして指定できるようになっている。
以下のような__main__.pyファイルを作成し
import demo_pkgrsrc.demo
demo_pkgrsrc.demo.app.run()
zipファイルのトップディレクトリに含めておけば、 python myapp.zip で実行可能なzipファイルとなる。
$ zip -r myapp.zip __main__.py demo_pkgrsrc $ python myapp.zip
Windows環境では、このzipファイルの拡張子を .py や .pyw などの、Pythonに関連付けられた拡張子に変更すれば、エクスプローラからのダブルクリックだけで起動できるようになる。
Unixで実行可能なファイルにする
さらに、Unix環境ではshebangを指定して直接実行可能なファイルとすることもできる。
$ echo '#!/usr/bin/env python' > myapp $ cat myapp.zip >>myapp $ chmod +x myapp $ ./myapp
- 72 http://b.hatena.ne.jp/hotentry/it
- 61 https://www.google.co.jp/
- 53 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CH4QFjAA&url=http://d.hatena.ne.jp/atsuoishimoto/20120503/1336050988&ei=0tCnT-SSJKj-mAWE2MThBA&usg=AFQjCNGUFcXmN1MjWTqbVfAcC6DNitSICQ
- 51 http://bit.ly/IYsDCR
- 45 http://b.hatena.ne.jp/
- 43 http://reader.livedoor.com/reader/
- 34 http://t.co/4WXd9xP2
- 33 http://longurl.org
- 32 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&ved=0CFwQFjAB&url=http://d.hatena.ne.jp/atsuoishimoto/&ei=Z2SkT_WuBuSTiAeh-_SyAw&usg=AFQjCNHPpY_zvlEQz0NrRDjAjtfdKZr7Sg&sig2=fWAadBbEwc3MABaLmrgVgQ
- 26 http://d.hatena.ne.jp/nullpobug/20120509/1336490491