ファイルをアップロードするための Python CGI スクリプト
2007.10.12 (鈴)- 1. はじめに
- 2. ソース解説
- 3. 準備
- 4. 手軽な実験
- 5. インストール
- 6. おわりに
- 次回 ファイルをアップロードするための Python CGI スクリプト - 2 2008.9.5 - 2008.9.9
- upload-191010.tar.bz2 md5: 3da5e606c8ff20e737c5fd266bed4bdc : スクリプトとそのライセンス (MIT/X11)
1. はじめに
- L: ねえ,ちょっと,Rei,これ見て御覧なさいな。
- R: Ling 先輩,これは?
- L: Mac の Python でファイルをアップロードする CGI なんだけど, はじめて書いたにしてはよくできたみたいだから :^)。
- R: ええっと,CGI ってあの Web サーバから Perl かなんかのプログラムを起動して Web ページを作らせる,あの CGI?
- L: そう,Common Gateway Interface って大げさな名前がついている,その CGI!
- R: 先輩は今まで書かれたことなかったんですか?
- L: ええ,とんでもない富豪的アプローチだと思っていたから…。 でもインストールが簡単であちこちですぐ動かせるし,結構手軽で便利… どのみち今はマシンも速くて,さしあたり動かすには全然問題ないようです。
本稿は,サーバ上のアップロード用フォルダの内容をリスティングし, ユーザからのファイルのアップロードを受け付ける Python CGI スクリプトを解説する。 下記のソース・リスティングとあわせて読み進めるとよい。
- upload: ファイルをアップロードする Python CGI スクリプト,82 行,Python 2.3.5 以降, Mac OS X ほか一般の Unix 用。
ブラウザからの利用例を下図に示す。図のブラウザは Mac OS X 10.4.10 上の Camino 1.5.1 多言語版である。 Web サーバと Python は同 Mac OS X 標準装備の Apache 1.3.33 と Python 2.3.5 である。
ブラウザで http://localhost/cgi-bin/upload を開くと, ファイル名の入力フィールドと,UPLOAD と書かれた送信ボタン, およびアップロード用フォルダの現在の一覧が表示される。 図はそこで ピクチャ 2.png というファイル名を入力して送信した場面である。 「received: ピクチャ 2.png」 という表示は, ピクチャ 2.png をサーバが受け取ったことを意味する。 アップロード用フォルダの一覧では,分かりやすいように, 受け取ったばかりのファイルが背景色を白色にして表示される。
- R: (ブラウザ画面の Szonáta szóló をみて) ええっと,スゾナタ・スゾロ…?
- L: ソナータ・ソーローのこと?
- R: ソナタ・ソロ… 独奏ソナタ?
- L: 後ろのゴルドンカーラ (gordonka+ra, チェロ+のための) とあわせて,独奏チェロソナタ。 ハンガリーの作曲家,コダーイ (Kodály) の傑作の一つです。 風変わりなファイル名でも大丈夫かどうか試すために, Hungaroton の…ええっと HCD 32198 の CD から iTunes に入れたファイルを試したんだけど…。
- R: "sz" 2文字で普通の "s"?
- L: そう,同時期に活躍したバルトークの作品なんかだと Sz. 40 みたいに番号がついているけど, あれは二文字で「エス」,またはナンバーを意味する「サーム」(szám) と読むわけ。 見たことあるでしょ?
- R: だったら,ええと,謎だったんですけど, もしかしたら,OpenLaszlo っていうのも…?
- L: ラースロー (László) なら,よくある名前だけど?
- R: ラースロー…。少し失礼します… (しばらく検索して) その読み方だと思って検索すると, OpenLaszlo の名前の由来で説明されているのって,結局,どれも「ラースロー」みたいです。 これ とか これ とか。
- L: (しばらく読んで) カメラマンのラズロ・コーバックス… Laszlo Kovacs… László Kovács …ラースロー・コヴァーチ…なるほどね。 探せば間違っていてもそれなりに証拠が見つかるし, 引用した中に正解へのヒントがあっても,その気がなければ,引用した人自身それに気付かない。 今はたまたまそれを垣間見たけど…,きっといろんなことについて, みんな気付かずにそんな風にすごしているに違いなくて…それが世界の真の姿だとしたら…。
- R: あ…あの…先輩?
2. ソース解説
スクリプトの先頭には #! と,スクリプトを解釈するインタープリタのパス名と,
高々1個のオプションを書く。
ここでは (効果はそれほど明白ではないが) 最適化オプション -O を与えている。
別解として,#!/usr/bin/env python と書く方法がある。
これは,どのインタープリタを使うか
(/usr/bin/python か,/usr/local/bin/python か,
それともその他のどれかか) を,
Web サーバが CGI スクリプトを実行するときの環境変数 PATH の値から決定する。
二行目のコメント内にある -*- mode: python -*- は,
emacs でこのスクリプトを編集するとき,
自動的に python モードにするための宣言である。
普通,スクリプトに接尾辞 .py があるときは不要である。
#!/usr/bin/python -O # -*- mode: python -*- H19.10/10 import cgi, ntpath, os, shutil, stat, sys, time
次の3行は,時と場合により書き換える必要がある。
FOLDER はアップロードしたファイルの置き場所にする。
CHARSET は (ファイルの内容ではなく)
ファイルの名前に使われる文字エンコーディングと一致させる。
cgi.logfile はこの CGI スクリプトのためのログファイル名にする。
(本来の趣旨からいえば,CHARSET は sys.getfilesystemencoding() の値にすべきである。
実際,Mac ではそうしても問題ない。
しかし,Unix によっては,これは必ずしも期待どおりには機能しない。
例えば,日本語 locale ではファイル名に EUC-JP が使われているが,
Web サーバ自身は C locale で動いている場合である。
その場合,暫定的に CHARSET = 'EUC-JP' とすればよい)
FOLDER = '/tmp/uploads' # to be configured CHARSET = 'UTF-8' # to be configured cgi.logfile = '/tmp/logfile' # to be configured
その次の if 文は Darwin
プラットフォーム (オペレーティングシステム本体としての Mac OS X) のためにある。
Darwin はファイル名を格納するとき,(半)濁音をかな本体と(半)濁点に分解する
Normalization Form D (NFD) への正規化を行う。
しかし,この形式は,ファイル名を入力値と比較したり,一般のブラウザに表示するとき,都合が悪い。
そこで,normalize という関数を用意して,
ファイルシステムから読み込んだ NFD の UTF-8 つまり UTF-8-MAC によるファイル名を,
一般的な Normalization Form C (NFC) の UTF-8 へ正規化する。
この変換処理を実現するため,/usr/bin/iconv -f UTF-8-MAC -t UTF-8 を子プロセスで実行する。
この処理が不要の一般プラットフォームに対しては, 何も変換せず,引数値をそのまま戻り値とするダミーの関数を用意する。
if sys.platform == 'darwin': import popen2 def normalize(s): (rf, wf) = popen2.popen2('/usr/bin/iconv -f UTF-8-MAC -t UTF-8') wf.write(s) wf.close() return rf.read() else: def normalize(s): return s
その次の main 関数が CGI スクリプトの本体である。 トップレベルにじかに文を置いてもよいが,上記 normalize 関数の単体テストを 行うときの便宜のため,いわゆる実行文をすべてこの関数の中に置くことにする。
cgi.log は,引数の書式化出力を cgi.logfile に指定されたファイル名に追記する。 ここでは現在時刻とクライアントのネットワーク・アドレス (REMOTE_ADDR) を出力している。 CGI スクリプトとして起動されたとき,REMOTE_ADDR のほか, どのような環境変数を利用できるかについては, RFC 3875 "The Common Gateway Interface (CGI) Version 1.1" の 4.1 節を参照されたい。
def main(): cgi.log('%s: host: %s', time.ctime(), os.environ['REMOTE_ADDR'])
クライアントからのフォーム (form) 入力を解釈する処理は cgi.FieldStorage クラスにカプセル化されている。 Python による CGI スクリプトでは, このクラスのインスタンス (ここでは form) を構築し,それを辞書のように扱う。
form = cgi.FieldStorage()
後述するように HTML の中でファイル名の入力フィールドは
<input type="FILE" name="file" size="70" />
となっている。
name="file" だから,アップロードされたファイルは
form からキー 'file' で参照できる。
しかし,初回アクセスで,
フォームによらず直接 CGI スクリプトのページを (HTTP GET で) 表示したときは,キーは存在しない。
また,入力フィールドのファイル名を空のままにしてフォームを送信したとき,
form['file'] の filename は空である。
この二つの場合を二重の if 文でチェックする。
処理をスキップしたとき,あとで HTML テキストの作成に困らないように,
事前に変数 wf_name と msg の初期値を与えておく。
wf_name = None
msg = '---'
if form.has_key('file'):
f = form['file']
if f.filename:
与えられたファイル名 f.filename に os.path.basename を適用して,
ディレクトリ部分を取り除く。
これは悪意ある入力で予期しないファイルが上書きされないようにするために必要である。
"../../some/important/file" のような入力を想像されたい。
実験によれば,Windows XP SP2 をクライアントとしたとき,
Firefox 2.0.0.7 では問題ないが,IE 6 ではファイル名として Windows 形式の完全パス名がサーバに与えられる。
C:\Documents and Settings\… のような名前のファイルがフォルダ上に作られることを防ぐため,
ファイル名にさらに ntpath.basename を適用する。
FOLDER とファイル名 wf_name を
os.join で結合して,ファイルシステムに書き込むパス名 wf_pname
(write file path name) を得る。
try:
wf_name = os.path.basename(f.filename)
wf_name = ntpath.basename(wf_name) # for Windows IE 6
wf_pname = os.path.join(FOLDER, wf_name)
バイナリ書き込みモード 'wb' で file オブジェクト wf を作る
アップロードされたファイルの内容は f.file で読み取ることができる。
shutil.copyfileobj(f.file, wf) を使って,
内容をファイルシステムに書き込む。
内部では一定の大きさのバッファを使って複写が行われるから,
巨大なファイルをアップロードした場合でも,メモリは浪費されない。
CGI スクリプトは,普通,専用のアカウント (Mac OS X の場合は www) で実行される。 アップロードの結果として得られたファイルを サーバ上のユーザが自由に処理できるように,os.chmod でパーミッションを 0666 (つまり -rw-rw-rw-) にしておく。
出力メッセージを msg にセットする。ファイル名により HTML が壊されないように cgi.escape で <, >, & をそれぞれ <, >, & に変換する。 また,ログにもファイル名を記録する。
wf = file(wf_pname, 'wb')
try:
shutil.copyfileobj(f.file, wf)
finally:
wf.close()
os.chmod(wf_pname, 0666)
msg = 'received: <b>%s</b>' % cgi.escape(wf_name)
cgi.log('%s: received: %s',
time.ctime(), wf_pname)
- L: ここではコンストラクタ…というか型オブジェクト file を利用して, ファイル・オブジェクト wf を作ってます。 これは具体的なクラスを指定した抽象度の低い方法です。 一般には,open 関数を使った方が, どのようなクラスでファイル・オブジェクトを実装するのか, 将来,Python の処理系に任せる余地があるわけなんだけど…。
- R: CGI だとなるべく動作を確定させたほうが安心だから,file にされたんですよね?
- L: まあ,そうです…というか単に慣れです。統一感もあるし。 でも,これで本当に良いかどうか,本当はよく分かりません。
- R: ところで f.file からの読み取りって,HTTP リクエストをじかに読んでるんですか?
- L: ええっと…違ったと思うけど,cgi.py のソースを見てみて。
- R: FieldStorage クラスの… make_file メソッドで一時ファイルを作って,そこにいったん格納してるようです。 ひょっとして,派生クラスでここを上書きすれば,あとでコピーしなくてすんだり, どれだけアップロードできたか見れるようにできるんではないでしょうか?
- L: あ,そうね。なんか良さそう。よかったら作ってみてくれない?
例外時には,ログに例外を記録するとともに,エラー発生の旨をメッセージ用の変数にセットする。
except Exception, ex:
cgi.log('%s: exception: %s',
time.ctime(), ex)
msg = '<font color="red">ERROR OCCURRED</font>: %s' % (
cgi.escape(wf_name))
以上でアップロードの処理が終わったので,関数本体の残りではレスポンスを作成する。 CGI スクリプトだから,標準出力へ書き込んだ内容が,クライアントへのレスポンスになる。
HTTP ヘッダでは,本文が HTML テキスト (Content-type: text/html) であり,
その文字エンコーディングが CHARSET の値であること,
キャッシュ保存をしないようにすること (Cache-control: no-store) を指示する。
HTTP ヘッダと本文は改行だけの行で区切られる。
print 'Content-type: text/html; charset=%s' % CHARSET
print 'Cache-control: no-store'
print
HTML テキストでは,タイトルと見出しとして upload と書く。 本文の地の色を silver (明るい灰色) にする。 変数 msg に格納されたメッセージを書く。
print '<html>'
print '<head><title>upload</title></head>'
print '<body bgcolor="silver">'
print '<h2>upload</h2>'
print '<p><big>%s</big></p>' % msg
次にアップロードのためのフォームを書く。 フォームの送信先を再びこのスクリプトにするために, action 値として環境変数 SCRIPT_NAME の値を使う。 こうすると,内容を変更せずにスクリプトの名前を変えることができる。
print '<form action="%s" method="POST" enctype="multipart/form-data">' % (
os.environ['SCRIPT_NAME'])
print ' <input type="FILE" name="file" size="70" />'
print ' <input type="SUBMIT" value="UPLOAD" />'
print '</form>'
水平の線で区切って,アップロード用フォルダの一覧を表示する。
ファイルシステムから os.listdir(FOLDER) で読み取ったファイル名を,
fname = normalize(fname) と正規化する。
さらに,HTML を壊さないように,cgi.escape(fname) とエスケープする。
print '<hr />'
print '<table cellpadding="7">'
print '<tr><th> name </th><th> size </th><th> date </th></tr>'
for fname in os.listdir(FOLDER):
fname = normalize(fname)
if fname == wf_name:
print '<tr bgcolor="white">',
else:
print '<tr>',
print '<td><tt>', cgi.escape(fname), '</tt></td>',
pname = os.path.join(FOLDER, fname)
st = os.stat(pname)
print '<td align="right">', st[stat.ST_SIZE], '</td>',
print '<td>', time.ctime(st[stat.ST_MTIME]), '</td>',
print '</tr>'
print '</table>'
print '<hr />'
print '</body>'
print '</html>'
このファイルが,モジュールではなく,スクリプトとして実行されたとき,main() を実行する。
if __name__ == '__main__': main()
3. 準備
スクリプト中の FOLDER (既定値は '/tmp/uploads') に相当するフォルダを作成し,
CGI スクリプトから移動・読み・書きを可能にする。
スクリプト中の cgi.logfile (既定値は '/tmp/logfile') に相当する空のファイルを作成し,
CGI スクリプトから追記可能にする。
$ mkdir /tmp/uploads $ chmod 777 /tmp/uploads $ touch /tmp/logfile $ chmod 666 /tmp/uploads
4. 手軽な実験
Python は CGI 可能な簡易 HTTP サーバを標準ライブラリにもっており, いわゆる "本物の" Web サーバにインストールする前の手軽な実験に使うことができる。 任意の場所で,下記のように cgi-bin フォルダを作り, そこに実行パーミッションを与えた CGI スクリプトを置いて CGIHTTPServer.py を実行する。
$ ls -F cgi-bin upload* $ python /usr/lib/python2.3/CGIHTTPServer.py Serving HTTP on 0.0.0.0 port 8000 ...
ブラウザから http://localhost:8000/cgi-bin/upload とアクセスする。 ポート番号 8000 に対しファイアウォール等で防止されていない限り, 他のマシンからもアクセスできることに注意されたい。
Python 2.4 以降ならば,ライブラリ・ファイルのパス名を指定するかわりに, -m オプションにモジュール名を与えることができる。
$ python -m CGIHTTPServer Serving HTTP on 0.0.0.0 port 8000 ...
5. インストール
Mac OS X では,アプリケーション → システム環境設定 → 共有 → サービス で, 「パーソナル Web 共有」にチェックを入れて Apache を開始できる。 いったんチェックを入れれば,後で OS X を再起動したときも自動的に Apache が開始される。 /Library/WebServer/CGI-Executables/ に CGI スクリプト upload を 実行パーミッションを付けて置く。
一般の Unix では,/var/www/cgi-bin/ や /usr/local/apache2/cgi-bin/ など,そのシステムの CGI スクリプトの置き場に upload を実行パーミッションを付けて置く。 Web サーバの設定によっては,各ユーザの ~/public_html/ 以下の任意の場所で upload.cgi のように所定の接尾辞を与えて CGI スクリプトを置くことができる。 /etc/httpd/conf/httpd.conf や /usr/local/apache2/conf/httpd.conf など,そのシステムの Web サーバの設定を確認されたい。
6. おわりに
本稿は,ファイルのアップロードとフォルダの一覧という, それほどトリビアルではない CGI スクリプトの Python による実現例を解説した。 これには Mac OS X 特有のファイル名の NFD - NFC 間の変換処理も含まれるが, 他の Unix への移植性は保たれている。
主観的な判断をいえば, スクリプトは処理の複雑さに対し十分に簡明であり, この種の問題解決に対する Python の高い適性を示していると言える。 低スループットが許容されるが, 高いコード品質と短い工期が要求される状況で, Python による CGI スクリプトは良い選択肢になると考えられる。
- R: そもそも,どうしてアップロード用 CGI を作ろうと考えられたのですか,先輩?
- L: 実は先生から,課題提出用の Web アプリがほしいと言われたんだけど…それが, なんというか,ライバル心をたきつける? ために,誰がいつどれだけのものを提出したかだけ, みんなから見えるようにしてくれって言われて… あと,ごちゃごちゃ入れられるのはイヤだからインストールは最低限にしろとも…。
- R: それがこれ,というわけですか?
- L: いいえ,これはその習作というか,プロトタイプです。 ネットワーク・アドレスは提出者ごとに固定ということだから, アップロードのファイル名の頭にそれを付けるとかが必要になってくると思うけど…, そこまで作り込む前に,これはこれで完成だから,こうして一度,君に見せたわけ。
- R: 勉強になります。
- L: それでね,きっと先生のことだから,いったんできると, 上位 10 名と本人の分しか一覧できないようにしてくれ,とか注文してくると思うから, そういったことが簡単にできそうな Python を選んだわけです。
次回へ
姉妹編: Tomcat 上の JRuby サーブレットへ


