ファイルをアップロードするための Python CGI スクリプト

2007.10.12 (鈴)

1. はじめに

本稿は,サーバ上のアップロード用フォルダの内容をリスティングし, ユーザからのファイルのアップロードを受け付ける Python CGI スクリプトを解説する。 下記のソース・リスティングとあわせて読み進めるとよい。

ブラウザからの利用例を下図に示す。図のブラウザは 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 をサーバが受け取ったことを意味する。 アップロード用フォルダの一覧では,分かりやすいように, 受け取ったばかりのファイルが背景色を白色にして表示される。


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 スクリプトのためのログファイル名にする。 (本来の趣旨からいえば,CHARSETsys.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_namemsg の初期値を与えておく。

    wf_name = None
    msg = '---'
    if form.has_key('file'):
        f = form['file']
        if f.filename:

与えられたファイル名 f.filenameos.path.basename を適用して, ディレクトリ部分を取り除く。 これは悪意ある入力で予期しないファイルが上書きされないようにするために必要である。 "../../some/important/file" のような入力を想像されたい。

実験によれば,Windows XP SP2 をクライアントとしたとき, Firefox 2.0.0.7 では問題ないが,IE 6 ではファイル名として Windows 形式の完全パス名がサーバに与えられる。 C:\Documents and Settings\… のような名前のファイルがフォルダ上に作られることを防ぐため, ファイル名にさらに ntpath.basename を適用する。

FOLDER とファイル名 wf_nameos.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 で <, >, & をそれぞれ &lt;, &gt;, &amp; に変換する。 また,ログにもファイル名を記録する。

                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)

例外時には,ログに例外を記録するとともに,エラー発生の旨をメッセージ用の変数にセットする。

            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 スクリプトは良い選択肢になると考えられる。


次回姉妹編: Tomcat 上の JRuby サーブレット


Copyright (c) 2007 OKI Software Co., Ltd.