cgi --- CGI (ゲートウェイインタフェース規格) のサポート

ソースコード: Lib/cgi.py


ゲートウェイインタフェース規格 (CGI) に準拠したスクリプトをサポートするためのモジュールです。

このモジュールでは、Python で CGI スクリプトを書く際に使える様々なユーティリティを定義しています。

はじめに

CGI スクリプトは、HTTP サーバによって起動され、通常は HTML の <FORM> または <ISINDEX> エレメントを通じてユーザが入力した内容を処理します。

ほとんどの場合、CGI スクリプトはサーバ上の特殊なディレクトリ cgi-bin の下に置きます。HTTP サーバは、まずスクリプトを駆動するためのシェルの環境変数に、リクエストの全ての情報 (クライアントのホスト名、リクエストされている URL、クエリ文字列、その他諸々) を設定し、スクリプトを実行した後、スクリプトの出力をクライアントに送信します。

スクリプトの入力端もクライアントに接続されていて、この経路を通じてフォームデータを読み込むこともあります。それ以外の場合には、フォームデータは URL の一部分である「クエリ文字列」を介して渡されます。このモジュールでは、上記のケースの違いに注意しつつ、Python スクリプトに対しては単純なインタフェースを提供しています。このモジュールではまた、スクリプトをデバッグするためのユーティリティも多数提供しています。また、最近はフォームを経由したファイルのアップロードをサポートしています (ブラウザ側がサポートしていればです)。

CGI スクリプトの出力は 2 つのセクションからなり、空行で分割されています。最初のセクションは複数のヘッダからなり、後続するデータがどのようなものかをクライアントに通知します。最小のヘッダセクションを生成するための Python のコードは以下のようなものです:

print("Content-Type: text/html")    # HTML is following
print()                             # blank line, end of headers

二つ目のセクションは通常、ヘッダやインラインイメージ等の付属したテキストをうまくフォーマットして表示できるようにした HTML です。以下に単純な HTML を出力する Python コードを示します:

print("<TITLE>CGI script output</TITLE>")
print("<H1>This is my first CGI script</H1>")
print("Hello, world!")

cgi モジュールを使う

import cgi と記述して開始します。

新たにスクリプトを書く際には、以下の行を付加するかどうか検討してください:

import cgitb
cgitb.enable()

これによって、特別な例外処理が有効にされ、エラーが発生した際にブラウザ上に詳細なレポートを出力するようになります。ユーザにスクリプトの内部を見せたくないのなら、以下のようにしてレポートをファイルに保存できます:

import cgitb
cgitb.enable(display=0, logdir="/path/to/logdir")

スクリプトを開発する際には、この機能はとても役に立ちます。 cgitb が生成する報告はバグを追跡するためにかかる時間を大幅に減らせるような情報を提供してくれます。スクリプトをテストし終わり、正確に動作することを確認したら、いつでも cgitb の行を削除できます。

入力されたフォームデータを取得するには、 FieldStorage クラスを使います。フォームが非 ASCII 文字を含んでいる場合は、 encoding キーワードパラメータを使用してドキュメントに対して定義されたエンコーディングの値を設定してください。それは、通常 HTML ドキュメントの HEAD セクション中の META タグ、あるいは Content-Type ヘッダーに含まれています。これは、標準入力または環境変数からフォームの内容を読み出します (どちらから読み出すかは、複数の環境変数の値が CGI 標準に従ってどのように設定されているかで決まります)。インスタンスが標準入力を使うかもしれないので、インスタンス生成を行うのは一度だけにしなければなりません。

FieldStorage のインスタンスは Python の辞書型のように添え字アクセスが可能です。 in を使用することによって要素が含まれているかの判定も出来ますし、標準の辞書メソッド keys() 及び組み込み関数 len() もサポートしています。空の文字列を含むフォーム要素は無視され、辞書には現れません。そのような値を保持するには、FieldStorage のインスタンス作成の際にオプションのキーワードパラメータ keep_blank_values に true を指定してください。

例えば、以下のコード (Content-Type ヘッダと空行はすでに出力された後とします) は name および addr フィールドが両方とも空の文字列に設定されていないか調べます:

form = cgi.FieldStorage()
if "name" not in form or "addr" not in form:
    print("<H1>Error</H1>")
    print("Please fill in the name and addr fields.")
    return
print("<p>name:", form["name"].value)
print("<p>addr:", form["addr"].value)
...further form processing here...

ここで、 form[key] で参照される各フィールドはそれ自体が FieldStorage (または MiniFieldStorage 。フォームのエンコードによって変わります) のインスタンスです。インスタンスの属性 value の内容は対応するフィールドの値で、文字列になります。 getvalue() メソッドはこの文字列値を直接返します。 getvalue() の 2 つめの引数にオプションの値を与えると、リクエストされたキーが存在しない場合に返すデフォルトの値になります。

入力されたフォームデータに同じ名前のフィールドが二つ以上あれば、 form[key] で得られるオブジェクトは FieldStorageMiniFieldStorage のインスタンスではなく、そうしたインスタンスのリストになります。この場合、 form.getvalue(key) も同様に、文字列からなるリストを返します。もしこうした状況が起きうると思うなら (HTML のフォームに同じ名前をもったフィールドが複数含まれているのなら) 、 getlist() メソッドを使ってください。これは常に値のリストを返します (単一要素のケースを特別扱いする必要はありません)。例えば、以下のコードは任意の数のユーザ名フィールドを結合し、コンマで分割された文字列にします:

value = form.getlist("username")
usernames = ",".join(value)

フィールドがアップロードされたファイルを表している場合、 value 属性や getvalue() メソッドを使ってフィールドの値にアクセスすると、ファイルの内容をすべてメモリ上にバイト列として読み込みます。これは場合によっては望ましい動作ではないかもしれません。アップロードされたファイルがあるかどうかは filename 属性および file 属性のいずれかで調べられます。そして、 FieldStorage インスタンスのガベージコレクションの一部として自動的に閉じられるまでの間に、 file 属性から以下のようにデータを読み込むことができます (read() および readline() メソッドはバイト列を返します):

fileitem = form["userfile"]
if fileitem.file:
    # It's an uploaded file; count lines
    linecount = 0
    while True:
        line = fileitem.file.readline()
        if not line: break
        linecount = linecount + 1

FieldStorage オブジェクトは with 文での使用にも対応しています。 with 文を使用した場合、オブジェクトは終了時に自動的に閉じられます。

アップロードされたファイルの内容を取得している間にエラーが発生した場合 (例えば、ユーザーが戻るボタンやキャンセルボタンで submit を中断した場合)、そのフィールドのオブジェクトの done 属性には -1 が設定されます。

現在ドラフトとなっているファイルアップロードの標準仕様では、一つのフィールドから (再帰的な multipart/* エンコーディングを使って) 複数のファイルがアップロードされる可能性を受け入れています。この場合、アイテムは辞書形式の FieldStorage アイテムとなります。複数ファイルかどうかは type 属性が multipart/form-data (または multipart/* にマッチする他の MIME 型) になっているかどうかを調べれば判別できます。この場合、トップレベルのフォームオブジェクトと同様にして再帰的に個別処理できます。

フォームが「古い」形式で入力された場合 (クエリ文字列または単一の application/x-www-form-urlencoded データで入力された場合)、データ要素の実体は MiniFieldStorage クラスのインスタンスになります。この場合、 listfile 、および filename 属性は常に None になります。

フォームがPOSTによって送信され、クエリー文字列も持っていた場合、 FieldStorageMiniFieldStorage の両方が含まれます。

バージョン 3.4 で変更: file 属性は、それを作成した FieldStorage インスタンスのガベージコレクションによって自動的に閉じられます。

バージョン 3.5 で変更: FieldStorage クラスにコンテキスト管理プロトコルのサポートが追加されました。

高水準インタフェース

前節では CGI フォームデータを FieldStorage クラスを使って読み出す方法について解説しました。この節では、フォームデータを分かりやすく直感的な方法で読み出せるようにするために追加された、より高水準のインタフェースについて記述します。このインタフェースは前節で説明した技術を撤廃するものではありません --- 例えば、前節の技術は依然としてファイルのアップロードを効率的に行う上で便利です。

このインタフェースは 2 つの単純なメソッドからなります。このメソッドを使えば、一般的な方法でフォームデータを処理でき、ある名前のフィールドに入力された値が一つなのかそれ以上なのかを心配する必要がなくなります。

前節では、一つのフィールド名に対して二つ以上の値が入力されるかもしれない場合には、常に以下のようなコードを書くよう学びました:

item = form.getvalue("item")
if isinstance(item, list):
    # The user is requesting more than one item.
else:
    # The user is requesting only one item.

こういった状況は、例えば以下のように、同じ名前を持った複数のチェックボックスからなるグループがフォームに入っているような場合によく起きます:

<input type="checkbox" name="item" value="1" />
<input type="checkbox" name="item" value="2" />

しかしながら、ほとんどの場合、あるフォーム中で特定の名前を持ったコントロールはただ一つしかないので、その名前に関連付けられた値はただ一つしかないはずだと考えるでしょう。そこで、スクリプトには例えば以下のようなコードを書くでしょう:

user = form.getvalue("user").upper()

このコードの問題点は、クライアントがスクリプトにとって常に有効な入力を提供するとは期待できないところにあります。例えば、もし好奇心旺盛なユーザがもう一つの user=foo ペアをクエリ文字列に追加したら、 getvalue("user") メソッドは文字列ではなくリストを返すため、このスクリプトはクラッシュするでしょう。リストに対して upper() メソッドを呼び出すと、引数が有効でない (リスト型はその名前のメソッドを持っていない) ため、例外 AttributeError を送出します。

従って、フォームデータの値を読み出しには、得られた値が単一の値なのか値のリストなのかを常に調べるコードを使うのが適切でした。これでは煩わしく、より読みにくいスクリプトになってしまいます。

ここで述べる高水準のインタフェースで提供している getfirst()getlist() メソッドを使うと、もっと便利にアプローチできます。

FieldStorage.getfirst(name, default=None)

フォームフィールド name に関連付けられた値をつねに一つだけ返す軽量メソッドです。同じ名前で 1 つ以上の値がポストされている場合、このメソッドは最初の値だけを返します。フォームから値を受信する際の値の並び順はブラウザ間で異なる可能性があり、特定の順番であるとは期待できないので注意してください。1 指定したフォームフィールドや値がない場合、このメソッドはオプションの引数 default を返します。このパラメタを指定しない場合、標準の値は None に設定されます。

FieldStorage.getlist(name)

このメソッドはフォームフィールド name に関連付けられた値を常にリストにして返します。name に指定したフォームフィールドや値が存在しない場合、このメソッドは空のリストを返します。値が一つだけ存在する場合、要素を一つだけ含むリストを返します。

これらのメソッドを使うことで、以下のようにナイスでコンパクトにコードを書けます:

import cgi
form = cgi.FieldStorage()
user = form.getfirst("user", "").upper()    # This way it's safe.
for item in form.getlist("item"):
    do_something(item)

関数

より細かく CGI をコントロールしたり、このモジュールで実装されているアルゴリズムを他の状況で利用したい場合には、以下の関数が便利です。

cgi.parse(fp=None, environ=os.environ, keep_blank_values=False, strict_parsing=False, separator="&")

環境変数、またはファイルからからクエリを解釈します (ファイルは標準で sys.stdin になります) keep_blank_values, strict_parsing, separator 引数はそのまま urllib.parse.parse_qs() に渡されます。

バージョン 3.8.8 で変更: Added the separator parameter.

cgi.parse_multipart(fp, pdict, encoding="utf-8", errors="replace", separator="&")

Parse input of type multipart/form-data (for file uploads). Arguments are fp for the input file, pdict for a dictionary containing other parameters in the Content-Type header, and encoding, the request encoding.

Returns a dictionary just like urllib.parse.parse_qs(): keys are the field names, each value is a list of values for that field. For non-file fields, the value is a list of strings.

この関数は簡単に使えますが、数メガバイトのデータがアップロードされると考えられる場合にはあまり適していません --- その場合、より柔軟性のある FieldStorage を代りに使ってください。

バージョン 3.7 で変更: Added the encoding and errors parameters. For non-file fields, the value is now a list of strings, not bytes.

バージョン 3.8.8 で変更: Added the separator parameter.

cgi.parse_header(string)

(Content-Type のような) MIME ヘッダを解釈し、ヘッダの主要値と各パラメタからなる辞書にします。

cgi.test()

メインプログラムから利用できる堅牢性テストを行う CGI スクリプトです。最小の HTTP ヘッダと、HTML フォームからスクリプトに供給された全ての情報を書式化して出力します。

cgi.print_environ()

シェル変数を HTML に書式化して出力します。

cgi.print_form(form)

フォームを HTML に初期化して出力します。

cgi.print_directory()

現在のディレクトリを HTML に書式化して出力します。

cgi.print_environ_usage()

意味のある (CGI の使う) 環境変数を HTML で出力します。

セキュリティへの配慮

重要なルールが一つあります: (関数 os.system() または os.popen() 、またはその他の同様の機能によって) 外部プログラムを呼び出すなら、クライアントから受信した任意の文字列をシェルに渡していないことをよく確かめてください。これはよく知られているセキュリティホールであり、これによって Web のどこかにいる悪賢いハッカーが、だまされやすい CGI スクリプトに任意のシェルコマンドを実行させてしまえます。URL の一部やフィールド名でさえも信用してはいけません。CGI へのリクエストはあなたの作ったフォームから送信されるとは限らないからです!

安全な方法をとるために、フォームから入力された文字をシェルに渡す場合、文字列に入っているのが英数文字、ダッシュ、アンダースコア、およびピリオドだけかどうかを確認してください。

CGI スクリプトを Unix システムにインストールする

あなたの使っている HTTP サーバのドキュメントを読んでください。そしてローカルシステムの管理者と一緒にどのディレクトリに CGI スクリプトをインストールすべきかを調べてください; 通常これはサーバのファイルシステムツリー内の cgi-bin ディレクトリです。

あなたのスクリプトが "others" によって読み取り可能および実行可能であることを確認してください; Unix ファイルモードは 8 進表記で 0o755 です (chmod 0755 filename を使ってください)。スクリプトの最初の行の 1 カラム目が、#! で開始し、その後に Python インタプリタへのパス名が続いていることを確認してください。例えば:

#!/usr/local/bin/python

Python インタプリタが存在し、"others" によって実行可能であることを確かめてください。

あなたのスクリプトが読み書きしなければならないファイルが全て "others" によって読み出しや書き込み可能であることを確かめてください --- 読み出し可能のファイルモードは 0o644 で、書き込み可能のファイルモードは 0o666 になるはずです。これは、セキュリティ上の理由から、 HTTP サーバがあなたのスクリプトを特権を全く持たないユーザ "nobody" の権限で実行するからです。この権限下では、誰でもが読める (書ける、実行できる) ファイルしか読み出し (書き込み、実行) できません。スクリプト実行時のディレクトリや環境変数のセットもあなたがログインしたときの設定と異なります。特に、実行ファイルに対するシェルの検索パス (PATH) や Python のモジュール検索パス (PYTHONPATH)が何らかの値に設定されていると期待してはいけません。

モジュールを Python の標準設定におけるモジュール検索パス上にないディレクトリからロードする必要がある場合、他のモジュールを取り込む前にスクリプト内で検索パスを変更できます。例えば:

import sys
sys.path.insert(0, "/usr/home/joe/lib/python")
sys.path.insert(0, "/usr/local/lib/python")

(この方法では、最後に挿入されたディレクトリが最初に検索されます!)

非 Unix システムにおける説明は変わるでしょう; あなたの使っている HTTP サーバのドキュメントを調べてください (普通は CGI スクリプトに関する節があります)。

CGI スクリプトをテストする

残念ながら、CGI スクリプトは普通、コマンドラインから起動しようとしても動きません。また、コマンドラインから起動した場合には完璧に動作するスクリプトが、不思議なことにサーバからの起動では失敗することがあります。しかし、スクリプトをコマンドラインから実行してみなければならない理由が一つあります: もしスクリプトが文法エラーを含んでいれば、Python インタプリタはそのプログラムを全く実行しないため、HTTP サーバはほとんどの場合クライアントに謎めいたエラーを送信するからです。

スクリプトが構文エラーを含まないのにうまく動作しないなら、次の節に読み進むしかありません。

CGI スクリプトをデバッグする

何よりもまず、些細なインストール関連のエラーでないか確認してください --- 上の CGI スクリプトのインストールに関する節を注意深く読めば時間を大いに節約できます。もしインストールの手続きを正しく理解しているか不安なら、このモジュールのファイル (cgi.py) をコピーして、CGI スクリプトとしてインストールしてみてください。このファイルはスクリプトとして呼び出すと、スクリプトの実行環境とフォームの内容を HTML 形式で出力します。ファイルに正しいモードを設定するなどして、リクエストを送ってみてください。標準的な cgi-bin ディレクトリにインストールされていれば、以下のような URL をブラウザに入力してリクエストを送信できるはずです:

http://yourhostname/cgi-bin/cgi.py?name=Joe+Blow&addr=At+Home

もしタイプ 404 のエラーになるなら、サーバはスクリプトを発見できないでいます -- おそらくあなたはスクリプトを別のディレクトリに入れる必要があるのでしょう。他のエラーになるなら、先に進む前に解決しなければならないインストール上の問題があります。もし実行環境の情報とフォーム内容 (この例では、各フィールドはフィールド名 "addr" に対して値 "At Home"、およびフィールド名 "name" に対して "Joe Blow") が綺麗にフォーマットされて表示されるなら、 cgi.py スクリプトは正しくインストールされています。同じ操作をあなたの自作スクリプトに対して行えば、スクリプトをデバッグできるようになるはずです。

次のステップでは cgi モジュールの test() 関数を呼び出すことになります: メインプログラムコードを以下の 1 文と置き換えてください

cgi.test()

この操作で cgi.py ファイル自体をインストールした時と同じ結果を出力するはずです。

通常の Python スクリプトが例外を処理しきれずに送出した場合 (様々な理由: モジュール名のタイプミス、ファイルが開けなかった、など)、Python インタプリタはナイスなトレースバックを出力して終了します。Python インタプリタはあなたの CGI スクリプトが例外を送出した場合にも同様に振舞うので、トレースバックは大抵HTTP サーバのいずれかのログファイルに残るかまったく無視されるかです。

幸運なことに、あなたが自作のスクリプトで 何らかの コードを実行できるようになったら、 cgitb モジュールを使って簡単にトレースバックをブラウザに送信できます。まだそうでないなら、以下の2行:

import cgitb
cgitb.enable()

をスクリプトの先頭に追加してください。そしてスクリプトを再度走らせます; 問題が発生すれば、クラッシュの原因を見出せるような詳細な報告を読めます。

cgitb モジュールのインポートに問題がありそうだと思うなら、(組み込みモジュールだけを使った) もっと堅牢なアプローチを取れます:

import sys
sys.stderr = sys.stdout
print("Content-Type: text/plain")
print()
...your code here...

このコードは Python インタプリタがトレースバックを出力することに依存しています。出力のコンテント型はプレーンテキストに設定されており、全ての HTML 処理を無効にしています。スクリプトがうまく動作する場合、生の HTML コードがクライアントに表示されます。スクリプトが例外を送出する場合、最初の 2 行が出力された後、トレースバックが表示されます。HTML の解釈は行われないので、トレースバックを読めるはずです。

よくある問題と解決法

  • ほとんどの HTTP サーバはスクリプトの実行が完了するまで CGI からの出力をバッファします。このことは、スクリプトの実行中にクライアントが進捗状況報告を表示できないことを意味します。

  • 上のインストールに関する説明を調べましょう。

  • HTTP サーバのログファイルを調べましょう。(別のウィンドウで tail -f logfile を実行すると便利かもしれません!)

  • 常に python script.py などとして、スクリプトが構文エラーでないか調べましょう。

  • スクリプトに構文エラーがないなら、import cgitb; cgitb.enable() をスクリプトの先頭に追加してみましょう。

  • 外部プログラムを起動するときには、スクリプトがそのプログラムを見つけられるようにしましょう。これは通常、絶対パス名を使うことを意味します --- PATH は普通、あまり CGI スクリプトにとって便利でない値に設定されています。

  • 外部のファイルを読み書きする際には、CGI スクリプトを動作させるときに使われる userid でファイルを読み書きできるようになっているか確認しましょう: userid は通常、Web サーバを動作させている userid か、Web サーバの suexec 機能で明示的に指定している userid になります。

  • CGI スクリプトを set-uid モードにしてはいけません。これはほとんどのシステムで動作せず、セキュリティ上の信頼性もありません。

脚注

1

最近のバージョンの HTML 仕様ではフィールドの値を供給する順番を取り決めてはいますが、ある HTTP リクエストがその取り決めに準拠したブラウザから受信したものかどうか、そもそもブラウザから送信されたものかどうかの判別は退屈で間違いやすいので注意してください。