cgi — Common Gateway Interfaceのサポート—Pythonドキュメント

提供:Dev Guides
< PythonPython/docs/3.9/library/cgi
移動先:案内検索

cgi — Common GatewayInterfaceのサポート

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



Common Gateway Interface(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

2番目のセクションは通常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()

これにより、エラーが発生した場合にWebブラウザに詳細レポートを表示する特別な例外ハンドラがアクティブになります。 スクリプトのユーザーにプログラムの本質を見せたくない場合は、次のようなコードを使用して、代わりにレポートをファイルに保存することができます。

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

スクリプト開発中にこの機能を使用すると非常に役立ちます。 cgitb によって作成されたレポートは、バグの追跡にかかる時間を大幅に節約できる情報を提供します。 スクリプトをテストし、正しく機能することを確認したら、後でいつでもcgitb行を削除できます。

送信されたフォームデータを取得するには、FieldStorageクラスを使用します。 フォームに非ASCII文字が含まれている場合は、ドキュメントに定義されているエンコーディングの値に設定された encoding キーワードパラメータを使用します。 これは通常、HTMLドキュメントのHEADセクションのMETAタグ、または Content-Type ヘッダーに含まれています。 これは、標準入力または環境からフォームの内容を読み取ります(CGI標準に従って設定されたさまざまな環境変数の値に応じて)。 標準の入力を消費する可能性があるため、インスタンス化する必要があるのは1回だけです。

FieldStorageインスタンスは、Python辞書のようにインデックスを付けることができます。 in 演算子を使用したメンバーシップテストが可能であり、標準の辞書メソッド keys()と組み込み関数 len()もサポートしています。 空の文字列を含むフォームフィールドは無視され、辞書に表示されません。 このような値を保持するには、FieldStorageインスタンスを作成するときに、オプションの keep_blank_values キーワードパラメーターにtrue値を指定します。

たとえば、次のコード( Content-Type ヘッダーと空白行がすでに印刷されていることを前提としています)は、フィールドnameaddrの両方がに設定されていることを確認します。空でない文字列:

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()メソッドは、この文字列値を直接返します。 また、要求されたキーが存在しない場合に返すデフォルトとして、オプションの2番目の引数を受け入れます。

送信されたフォームデータに同じ名前のフィールドが複数含まれている場合、form[key]によって取得されるオブジェクトはFieldStorageまたはMiniFieldStorageインスタンスではなく、そのようなインスタンスのリストです。 同様に、この状況では、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 ステートメントでの使用もサポートしており、完了すると自動的に閉じます。

アップロードされたファイルの内容を取得するときにエラーが発生した場合(たとえば、ユーザーが[戻る]または[キャンセル]ボタンをクリックしてフォームの送信を中断した場合)、フィールドのオブジェクトのdone属性が設定されます。値-1に。

ファイルアップロードドラフト標準は、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クラスにコンテキスト管理プロトコルのサポートが追加されました。


より高いレベルのインターフェース

前のセクションでは、FieldStorageクラスを使用してCGIフォームデータを読み取る方法について説明しました。 このセクションでは、このクラスに追加された高レベルのインターフェイスについて説明します。これにより、より読みやすく直感的な方法でそれを実行できるようになります。 このインターフェースは、前のセクションで説明した手法を廃止するものではありません。たとえば、ファイルのアップロードを効率的に処理するのに役立ちます。

インターフェイスは2つの簡単な方法で構成されています。 メソッドを使用すると、1つの名前で1つ以上の値のみが投稿されたかどうかを心配することなく、一般的な方法でフォームデータを処理できます。

前のセクションでは、ユーザーが1つの名前で複数の値を投稿することを期待するときはいつでも次のコードを書くことを学びました。

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" />

ただし、ほとんどの場合、フォーム内に特定の名前を持つフォームコントロールは1つしかないため、この名前に関連付けられた値は1つだけである必要があります。 したがって、たとえば次のコードを含むスクリプトを記述します。

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 にそのようなフォームフィールドまたは値が存在しない場合、メソッドは空のリストを返します。 そのような値が1つしかない場合は、1つの項目で構成されるリストを返します。

これらのメソッドを使用すると、優れたコンパクトなコードを記述できます。

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.parse(fp=None, environ=os.environ, keep_blank_values=False, strict_parsing=False, separator='&')
環境内またはファイルからのクエリを解析します(ファイルのデフォルトはsys.stdinです)。 keep_blank_valuesstrict_parsing 、および Separator パラメーターは、変更されずに urllib.parse.parse_qs()に渡されます。
cgi.parse_multipart(fp, pdict, encoding='utf-8', errors='replace', separator='&')

タイプ multipart / form-data の入力を解析します(ファイルのアップロード用)。 引数は、入力ファイルの場合は fpContent-Type ヘッダーに他のパラメーターを含む辞書の場合は pdictencoding はエンコーディングをリクエストします。

urllib.parse.parse_qs()と同じように辞書を返します。キーはフィールド名であり、各値はそのフィールドの値のリストです。 ファイル以外のフィールドの場合、値は文字列のリストです。

これは使いやすいですが、メガバイトがアップロードされることを期待している場合はあまり良くありません。その場合は、代わりにFieldStorageクラスを使用してください。これははるかに柔軟性があります。

バージョン3.7で変更: encoding および errors パラメーターが追加されました。 ファイル以外のフィールドの場合、値はバイトではなく文字列のリストになりました。

バージョン3.9.2で変更: セパレーターパラメーターが追加されました。

cgi.parse_header(string)
MIMEヘッダー( Content-Type など)をメイン値とパラメーターのディクショナリに解析します。
cgi.test()
メインプログラムとして使用できる堅牢なテストCGIスクリプト。 最小限のHTTPヘッダーを書き込み、スクリプトに提供されるすべての情報をHTML形式でフォーマットします。
cgi.print_environ()
シェル環境をHTMLでフォーマットします。
cgi.print_form(form)
フォームをHTMLでフォーマットします。
cgi.print_directory()
現在のディレクトリをHTMLでフォーマットします。
cgi.print_environ_usage()
有用な(CGIで使用される)環境変数のリストをHTMLで出力します。


セキュリティへの配慮

重要なルールが1つあります。外部プログラムを呼び出す場合( os.system()os.popen()、または同様の機能を持つ他の関数を介して)、必ず実行しないでください。 tクライアントから受信した任意の文字列をシェルに渡します。 これはよく知られたセキュリティホールであり、Web上のあらゆる場所にいる巧妙なハッカーが、騙されやすいCGIスクリプトを悪用して任意のシェルコマンドを呼び出すことができます。 リクエストはフォームから送信する必要がないため、URLやフィールド名の一部でさえ信頼できません。

安全のために、フォームから取得した文字列をシェルコマンドに渡す必要がある場合は、文字列に英数字、ダッシュ、アンダースコア、およびピリオドのみが含まれていることを確認する必要があります。


UnixシステムへのCGIスクリプトのインストール

HTTPサーバーのドキュメントを読み、ローカルシステム管理者に確認して、CGIスクリプトをインストールする必要のあるディレクトリを見つけてください。 通常、これはサーバーツリーのディレクトリcgi-binにあります。

スクリプトが「他の人」によって読み取り可能で実行可能であることを確認してください。 Unixファイルモードは0o755 8進数である必要があります(chmod 0755 filenameを使用)。 スクリプトの最初の行に、列1で始まりPythonインタープリターのパス名が続く#!が含まれていることを確認してください。次に例を示します。

#!/usr/local/bin/python

Pythonインタープリターが存在し、「その他」によって実行可能であることを確認してください。

スクリプトが読み取りまたは書き込みを行う必要のあるファイルは、それぞれ「その他」によって読み取り可能または書き込み可能であることを確認してください。モードは、読み取り可能の場合は0o644、書き込み可能の場合は0o666である必要があります。 これは、セキュリティ上の理由から、HTTPサーバーが特別な特権なしでユーザー「nobody」としてスクリプトを実行するためです。 誰もが読み取る(書き込み、実行)ことができるファイルのみを読み取る(書き込み、実行)ことができます。 実行時の現在のディレクトリも異なり(通常はサーバーのcgi-binディレクトリです)、環境変数のセットもログイン時に取得するものとは異なります。 特に、実行可能ファイルのシェルの検索パス( 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スクリプトは通常、コマンドラインから実行しても実行されません。また、コマンドラインから完全に機能するスクリプトは、サーバーから実行すると不思議なことに失敗する可能性があります。 コマンドラインからスクリプトをテストする必要がある理由が1つあります。構文エラーが含まれている場合、Pythonインタープリターはスクリプトをまったく実行せず、HTTPサーバーがクライアントに不可解なエラーを送信する可能性があります。

スクリプトに構文エラーがなくても機能しないと仮定すると、次のセクションを読むしかありません。


CGIスクリプトのデバッグ

まず、些細なインストールエラーがないか確認します。CGIスクリプトのインストールに関する上記のセクションを注意深く読むと、時間を大幅に節約できます。 インストール手順を正しく理解したかどうか疑問に思われる場合は、このモジュールファイル(cgi.py)のコピーをCGIスクリプトとしてインストールしてみてください。 スクリプトとして呼び出されると、ファイルはその環境とフォームの内容をHTML形式でダンプします。 適切なモードなどを指定して、リクエストを送信します。 標準のcgi-binディレクトリにインストールされている場合は、次の形式のブラウザにURLを入力してリクエストを送信できるはずです。

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

これでタイプ404のエラーが発生した場合、サーバーはスクリプトを見つけることができません。おそらく、別のディレクトリにインストールする必要があります。 別のエラーが発生する場合は、インストールの問題があり、先に進む前に修正する必要があります。 環境とフォームコンテンツの適切にフォーマットされたリストを取得した場合(この例では、フィールドは値「AtHome」の「addr」および値「JoeBlow」の「name」としてリストされている必要があります)、 [ X194X]スクリプトが正しくインストールされました。 独自のスクリプトに対して同じ手順を実行すると、スクリプトをデバッグできるようになります。

次のステップは、スクリプトから cgi モジュールの test()関数を呼び出すことです。メインコードを単一のステートメントに置き換えます。

cgi.test()

これにより、cgi.pyファイル自体をインストールした場合と同じ結果が得られるはずです。

通常のPythonスクリプトが未処理の例外を発生させると(何らかの理由で:モジュール名のタイプミス、開くことができないファイルなど)、Pythonインタープリターは適切なトレースバックを出力して終了します。 Pythonインタープリターは、CGIスクリプトで例外が発生した場合でもこれを実行しますが、トレースバックがHTTPサーバーのログファイルの1つに記録されるか、完全に破棄される可能性があります。

幸い、スクリプトで some コードを実行できるようになったら、 cgitb モジュールを使用してトレースバックをWebブラウザーに簡単に送信できます。 まだ行っていない場合は、次の行を追加するだけです。

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スクリプトが実行されるユーザーIDで読み取りまたは書き込みができることを確認してください。これは通常、Webサーバーが実行されるユーザーID、またはWebサーバーの[ X260X] 機能。
  • CGIスクリプトにset-uidモードを与えようとしないでください。 これはほとんどのシステムでは機能せず、セキュリティ上の責任もあります。

脚注

1
HTML仕様の最近のバージョンの中には、フィールド値を指定する順序を示しているものもありますが、要求が準拠するブラウザーから受信されたか、ブラウザーから受信されたかを知ることは、面倒でエラーが発生しやすいことに注意してください。