Docker + Alpine3.5 + Apache2.4 + Python3.6で、フォームのデータを標準モジュールcgiで受け取ってみた

前回、フォームのデータをCGIPythonスクリプトsys.stdin.read()os.environを使って受け取りました。
Dockerで、Alpine3.5 + Apache2.4 + Python3.6の環境を作って、フォームのデータをCGIで受け取ってみた - メモ的な思考的な

 
今回は標準モジュールcgiにあるFieldStorage使ってデータを受け取ってみます。

 
目次

 

環境

  • Mac OS X 10.11.6
  • Docker for Mac 17.03.1-ce-mac5
  • Alpine3.5 + Apache2.4.25 + Python 3.6.1

なお、Dockerfileは前回のものを流用します。

HTMLファイルは前回のものを流用し、actionだけを対象のPythonスクリプトに差し替えます。

Dockerまわりのコマンドは以下の通りです。

# Docker上のApacheでCGIとして動かすために、Dockerと共有するローカルファイルのパーミッションを変更
$ chmod 755 cgi_module.py 
$ chmod 755 cgi_file_upload.py
$ chmod 755 cgi_file_upload_with.py

# Dockerイメージのビルド
$ docker image build -t alpine:python3_httpd24_cgi_module .

# Dockerコンテナの起動
$ docker container run -p 8081:80 --name cgi_module -v `pwd`/htdocs/:/usr/local/apache2/htdocs -v `pwd`/cgi/:/usr/local/apache2/cgi-bin/ alpine:python3_httpd24_cgi_module

 

cgi.FieldStorageを使ったフォームデータの受け取り

データの読み込み

cgi.FieldStorageインスタンスを生成することで、標準入力などからデータを読み込めます。

cgi_module.py

#!/usr/bin/python3
import cgi

# HTTPレスポンスヘッダ
print('Content-Type: text/plain;charset=utf-8\n')
print('')

form = cgi.FieldStorage()
print(form)
# => FieldStorage(None, None, [MiniFieldStorage('quantity', '1個'), 
#                              MiniFieldStorage('hidden_valude', '隠しデータ')])

 
また、フォームに入力していない要素も取得したい場合は、

form = cgi.FieldStorage(keep_blank_values=True)
print(form)
# => FieldStorage(None, None, [MiniFieldStorage('subject', ''), 
#                              MiniFieldStorage('quantity', '1個'), 
#                              MiniFieldStorage('hidden_valude', '隠しデータ'), 
#                              MiniFieldStorage('memo', '')])

と、FieldStorageのインスタンス生成時の引数としてkeep_blank_values=Trueを指定します。

ただ、上記を見る限り、未選択のcheckboxやradio button、select multipleは取得できないようです。

 
なお、FieldStorageのインスタンス生成については、公式ドキュメントにある

標準入力または環境変数からフォームの内容を読み出します (どちらから読み出すかは、複数の環境変数の値が CGI 標準に従ってどのように設定されているかで決まります)。インスタンスが標準入力を使うかもしれないので、インスタンス生成を行うのは一度だけにしなければなりません。

21.2. cgi — CGI (ゲートウェイインタフェース規格) のサポート — Python 3.6.1 ドキュメント

という点に注意します。

 
また、FieldStorageインスタンス生成後にsys.stdin.read()を使うと、

form = cgi.FieldStorage()

print('stdin:\n{}'.format(sys.stdin.read()))
# => stdin:

と、何も取得できません。

 
一方(今回のケースだけかもしれませんが)、os.environについては

form = cgi.FieldStorage()

print('os.environ:\n')
for k, v in os.environ.items():
    print('{}: {}'.format(k, v))
# =>
# HTTP_HOST: localhost:8081
# HTTP_CONNECTION: keep-alive
# ...

と、環境変数の値が取得できました。

 

FieldStorageの属性やメソッド

属性やメソッドを使ってみると、

form = cgi.FieldStorage()

print(form.type)
# => application/x-www-form-urlencoded

print(form.headers)
# => {'content-type': 'application/x-www-form-urlencoded', 'content-length': '94'}

print(form.keys())
# => ['quantity', 'memo', 'hidden_valude', 'subject']

となりました。

 

フォームのフィールド値の取得

例えば、「自分で持ち帰る」チェックボックスにチェックを入れた場合、

form = cgi.FieldStorage()

print(form['takeout'])
# => MiniFieldStorage('takeout', '自分で持ち帰る')

print(form['takeout'].value)
# => 自分で持ち帰る

と、辞書の添字アクセスと同じようにして、フィールドの値を取得できます。

 
なお、添字アクセスで存在しないキーを指定した場合、

form = cgi.FieldStorage()

try:
    print(form['foo'])
except:
    print('not found: foo')
# => not found: foo

と、例外が出ます。

 
そのため、getvalue()メソッドを使うことで、

form = cgi.FieldStorage()
print(form.getvalue('foo'))
# => None
print(form.getvalue('foo', 'default_value'))
# => default_value

と、例外を回避したり、デフォルト値を返すことができます。

 

同じnameを持つ複数フィールド値の取得

チェックボックスなど、複数のフィールドで同じnameを持つ場合は

form = cgi.FieldStorage()

print(form.getfirst('purpose'))
# => 贈り物にする

print(form.getlist('purpose'))
# => ['贈り物にする', '自家用にする']

のように、getfirst()で最初のものだけ取得したり、getlist()でリストとして取得します。

 
なお、nameが一つだけの場合にgetlist()メソッドを使うと、

form = cgi.FieldStorage(keep_blank_values=True)

print(form.getlist('takeout'))
# => ['自分で持ち帰る']

と、要素数1のリストとなります。

 

cgi.FieldStorageを使ったアップロードファイルの受け取り

ファイルアップロードができるフォームを

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title></title>
</head>

<body>
    <h1>ファイルアップロードサンプル(一気に読み込み)</h1>
    <form action="/cgi-bin/cgi_file_upload.py" method="POST" enctype="multipart/form-data">
        <!--input text-->
        <label for="id_upload_file">対象ファイル</label>
        <input type="file" id="id_upload_file" name="upload_file">

        <!--submit-->
        <p>
            <input type="submit">
        </p>
    </form>
</body>

</html>

と用意した場合も、cgi.FieldStorageでアップロードしたファイルのデータを扱えます。

 
例えば、

upload_file.txt

あ
a

というファイルを扱う場合、

cgi_file_upload.py

#!/usr/bin/python3
import cgi

# HTTPレスポンスヘッダ
print('Content-Type: text/plain;charset=utf-8\n')
print('')

form = cgi.FieldStorage()
print(form)
# => FieldStorage(None, None, [FieldStorage('upload_file', 'upload_file.txt', b'\xe3\x81\x82\na')])

とすることで、FieldStorageのインスタンスとして取得できます。

 
FieldStorageインスタンスからファイルの中身を一気に読み込む場合、

form = cgi.FieldStorage()

content = form.getvalue('upload_file')

print(content.__class__)
# => <class 'bytes'>

print(content.decode('utf-8'))
# =>
# あ
# a

と、getvalue()メソッドでバイト文字列を取得し、それをdecode()にて文字列にします。

 
また、一気に読み込むのが難しい場合は、一定のバイト数で区切って読み込むこともできます。

cgi_file_upload_with.py

form = cgi.FieldStorage()
upload_file = form['upload_file']

print(upload_file)
# => FieldStorage('upload_file', 'upload_file.txt', b'\xe3\x81\x82\na')

print(dir(upload_file))
# => ['FieldStorageClass', '_FieldStorage__file', '_FieldStorage__write', '__bool__', 
#     '__class__', '__contains__', '__del__', '__delattr__', '__dict__', '__dir__', 
#     '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattr__',
#     '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__',
#     '__iter__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__',
#     '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', 
#     '__subclasshook__', '__weakref__', '_binary_file', 'bufsize', 'bytes_read', 
#     'disposition', 'disposition_options', 'done', 'encoding', 'errors', 'file', 
#     'filename', 'fp', 'getfirst', 'getlist', 'getvalue', 'headers', 'innerboundary',
#     'keep_blank_values', 'keys', 'length', 'limit', 'list', 'make_file', 'name', 
#     'outerboundary', 'qs_on_post', 'read_binary', 'read_lines', 'read_lines_to_eof', 
#     'read_lines_to_outerboundary', 'read_multi', 'read_single', 'read_urlencoded', 
#     'skip_lines', 'strict_parsing', 'type', 'type_options']

print(upload_file.name)
# => upload_file

print(upload_file.filename)
# => upload_file.txt

print(upload_file.file)
#=> <_io.BytesIO object at 0x7fe34bba03b8>

# with文にも対応しているとのことなので、試してみる
# http://docs.python.jp/3/library/cgi.html
with upload_file as f:
    content = b''
    while True:
        # 1バイトずつ読み込む場合
        byte_strings = f.file.read(1)
        print(byte_strings)
        # => b'\xe3', b'\x81', b'\x82', b'\n', b'a', b'' の順に読み込まれる

        if not byte_strings:
            break
        content += byte_strings

print(content.decode('utf-8'))
# あ
# a

 

ソースコード

GitHubに上げました。alpine_apache_python36_cgi_moduleディレクトリの中が今回のものです。
thinkAmi-sandbox/Docker_Apache-sample