Pythonで実装するCGIデコード
CGIとは、Webサーバでプログラムを動作させる仕組みです。CGIプログラムは、Perl製のものが多いですが、PythonやRubyなども動作させることが出来ます。このページでは、CGIでの処理の前提となるURLエンコード(パーセントエンコーディング)を、Pythonでデコードする方法を説明します。
CGIでデコードを行なう理由、HTTPリクエストの概要につきましてはPerlで実装するCGIデコードで説明しています。重複する内容となるため、そちらをご覧下さい。
目次
Pythonでユーザーからの入力を受け取る方法
PythonでGET送信を受け取るには、環境変数のオブジェクトos.environからgetメソッドを利用します。キーはQUERY_STRINGで取得可能です。環境変数とは、Webサーバが設定した情報の連想配列(辞書)になっています。環境変数には、GET送信かPOST送信かの情報も入っているため、判定処理に利用できます。
POST送信を受け取るには、標準入力から取得します。Pythonでの標準入力はsys.stdinです。全取得read()、一行取得readline()、リストに一行ずつ格納readlines()が使えます。がこのメソッドで取得すると文字コードの問題が起こるため、今回はbufferでバイナリデータで処理します。
# モジュールのインポート
import os,sys
# GET送信のクエリーを取得
q = os.environ.get('QUERY_STRING')
# POST送信されたデータを一行分取得
s = sys.stdin.readline()
Pythonで正規表現を使う方法
受け取った文字列は、決まったルールで変換された状態になっています。これを復元するには、一定の法則で逆処理をすることになります。文字の出現法則で決まった変換が行えるコーディング方法に、正規表現があります。他に変換テーブルを作成し、通す方法がありますが、今回は正規表現でエンコードを行っていきます。
他の言語経験者への注意点として、match()は先頭からの適合を判定します。文字列内から探す場合はsearch()を利用します。
# Pythonの正規表現による置換
# 正規表現モジュールをインポート
import re
s = re.sub('[\r\n]','',s) #改行コードを除去
# 正規表現で先頭から判定
m = re.match('(\d+)','',s)
# 正規表現で検索して判定
m = re.search('(\d+)','',s)
# ()で適合した文字の取り出し 0 が全体 1から()内
s = m.group(1)
Pythonのグローバル変数とローカル変数
Pythonには、ブロック内で利用できるローカル変数と、モジュール全体で利用できるグローバル変数があります。モジュールを超えて利用できる変数を作成するには、組み込みオブジェクト__builtins__に設定する必要があります。
今回は、組み込みオブジェクトに設定し、他のモジュールからでも利用できるようにします。
# どこでも使える変数を作る
__builtins__['GET'] = {}
__builtins__['POST'] = {}
__builtins__['COOKIE'] = {}
__builtins__['FILES'] = []
PythonでGETリクエストを解析する
リクエストの種類は、os.environのgetメソッドからREQUEST_METHODで確認することが出来ます。GET送信の受け取りデータはURL情報なので、POST送信時も情報が含まれており、処理上必要になることもあるので、常に取得する構成にします。
またクッキーの処理も実装します。GETのURLには、?以降にkey1=value1&key2=value2の状態でデータが並んでいます。クッキーではkey1=value1; key2=value2; となります。「&」繋ぎと「; 」繋ぎの差があります。
# リクエストのメソッド判定
if os.environ.get('REQUEST_METHOD')=='POST':
# ここでPOST用処理
__builtins__['GET'] = self.__decode(os.environ.get('QUERY_STRING'))
__builtins__['COOKIE'] = self.__decode(os.environ.get('HTTP_COOKIE'), '; ')
selfとあるのは、クラスモジュールとして作成するためで、self.__decode()の部分がクラスのメソッドになります。メソッド名にアンダースコア__が2つ付いているものは、プライベートメソッド(内部で使うメソッド)になります。
# URLデコード処理
def __decode(self, buf, de='&'):
# 変数の初期化
h = {}
# 文字データをバイト列に
buf = bytes(buf,'utf-8')
# 正規表現をコンパイル
# keyとvalの分離用
r1 = re.compile(b'([^=]+)=([^=]*)')
# +と半角スペースの置換用
r2 = re.compile(b'+')
# 16進数表現の置換用
r3 = re.compile(b'%([a-fA-F0-9][a-fA-F0-9])')
# 区切り文字で分離して処理
for v in buf.split(bytes(de,'utf-8')):
if v==b'':
continue
# = が存在した時
m = re.search(r1,v)
if m:
(key,val) = (m.group(1),m.group(2))
else:
(key,val) = (v,b'')
# +と半角スペースの置換
key = re.sub(r2,b' ',key)
# 16進数表現を文字に復元
key = re.sub(r3,lambda x:bytes([int(x.group(1),16)]),key).decode('utf-8')
val = re.sub(r2,b' ',val)
val = re.sub(r3,lambda x:bytes([int(x.group(1),16)]),val).decode('utf-8')
# ハッシュに保存する処理
self.__setHash(key,val,h)
return h
文字データを一旦、バイト列にしてから処理しています。何度も使用する正規表現は、コンパイルしておくことが出来ます。lambdaはラムダ式と呼ばれ、本来ブロックで行なうような処理を式にしています。
URLやフォームからの送信データは、基本的にkey1=value1&key2=value2の構成で渡ってきます。ただし、例えばチェックボックスのように同じ項目を複数選択できる場合、同じkeyが重複して渡ってくる事が有ります。PHPでは、タグのname属性の末尾に[]を付けることで配列化が行えますが、今回は既にデータがある場合に配列化する構成にします。
# ハッシュに保存する処理
def __setHash(self, k, v, h):
# キーがすでにある場合、値を配列(リスト)に
if k in h:
# 値が配列(リスト)の場合、最後に追加
if isinstance(h[k], list):
h[k].append(v)
# 値が文字列の場合、配列に
else:
h[k] = [h[k],v]
else:
h[k] = v
PythonでPOSTリクエストを解析する
POST送信は、一般的に利用されているもので2つのエンコード方法が存在します。formタグのデフォルトであるapplication/x-www-form-urlencodedと、ファイルをアップロードする場合のmultipart/form-dataです。前者はGET送信と同じエンコード方法で、後者はboundaryと呼ばれる区切り文字で、フォームの各データを区切った状態で送信します。
# リクエストのメソッド判定
if os.environ.get('REQUEST_METHOD')=='POST':
# multipart/form-data を判定
m = re.search("multipart/form-data; boundary=(.+)", os.environ.get('CONTENT_TYPE'))
if m:
# 区切り文字列のboundaryを取得して渡し
__builtins__['POST'] = self.__multipart(m.group(1))
else:
# 標準入力から取り出して渡し
__builtins__['POST'] = self.__decode(sys.stdin.read())
Pythonでmultipart/form-dataを解析する
ファイルのアップロード時は、フォームタグにenctype="multipart/form-data"を指定して、他のフォーム要素と一緒に、ファイルデータも受け取ることになります。各フォーム要素を区切る文字列として、boundaryが利用されるので、これをフォームデータの終端として利用します。
フォームデータの開始行は、改行のみの行になっています。ただし、フォームデータ内に改行のみの行が存在し得るので、データが空の時のみ開始行と判定します。
# マルチパートデータを処理
def __multipart(self, bound):
# 変数の初期化
(key,fi,ty) = ('','','')
val = []
h = {}
f = []
rec = 0
# 正規表現をコンパイル
# 区切り文字判定
r1 = re.compile(bytes(bound,'utf-8'))
# 末尾の改行置換用
r2 = re.compile(b'\r\n$')
# ファイル判定
r3 = re.compile(b'Content-Disposition: form-data; name="([^"]+)"; filename="([^"]+)"')
# 通常のフォーム項目
r4 = re.compile(b'Content-Disposition: form-data; name="([^"]+)"')
# アップロードされたファイルタイプ
r5 = re.compile(b'Content-Type: (.+)')
# データ部の開始判定用
r6 = re.compile(b'^\r\n$')
# 標準入力をバイト列で処理
for li in sys.stdin.buffer:
# 区切り文字があった場合
if re.search(r1,li):
# キーがあった場合
if key!='':
# 末尾の改行を除去
val = re.sub(r2,b'',b''.join(val))
# フォームデータがファイルの場合
if fi!= '':
# 通常のフォームデータ
else:
self.__setHash(key.decode('utf-8'), val.decode('utf-8'), h)
key = ''
val = []
rec = 0
continue
# データの結合
if(rec==1):
val.append(li)
continue
# フォームのname属性とアップロードファイル名を取得
m = re.search(r3,li)
if m:
key = m.group(1)
fi = m.group(2)
continue
# 通常のフォームデータの場合
m = re.search(r4,li)
if m:
key = m.group(1)
continue
# アップロードされたファイルタイプ
m = re.search(r5,li)
if m:
ty = m.group(1).rstrip()
continue
# データ内容の開始判定
if rec==0:
m = re.search(r6,li)
if m:
rec = 1
# ファイル情報を代入
__builtins__['FILES'] = f
return h
Pythonで一時ファイルを作成する
Pythonで一時ファイルを作成するには、tempfileモジュールのNamedTemporaryFileメソッドが利用できます。標準モジュールなのでインポートするだけです。一時ファイルとは、/tmp/ディレクトリに保存されるファイルで、一定期間後に削除されます。確実に削除したい場合は、削除処理を行なう必要があります。
# モジュールのインポート
import tempfile
# フォームデータがファイルの場合
if fi!= '':
# 一時ファイルを作成
tf = tempfile.NamedTemporaryFile(delete=False)
# 一時ファイルに書込
tf.write(val)
# 保持用のファイル情報を登録
f.append({
'name':key.decode('utf-8'),
'up_name':fi.decode('utf-8'),
'type':ty.decode('utf-8'),
'tmp_name':tf.name
})
fi = ''
ty = ''
受け取った入力データの利用方法
以上でデコード面は完成しました。ファイルはモジュール化して再利用できるようにします。またデコードしたファイルデータは、独自の仕様で保持されているため、保存を簡単に行えるようにメソッド化しておきます。
# 一時ファイルから移動する
def move(self,f,name):
# ファイル指定がおかしいか、ファイル名の指定がない時
if not 'tmp_name' in f or not os.path.isfile(f['tmp_name']) or name=='':
return 0
# 一時ファイルを読み込んで新ファイルを作成
with open(name,'wb') as nf:
with open(f['tmp_name'],'rb') as tf:
data = tf.read()
nf.write(data)
os.remove(f['tmp_name'])
return name
return 0
完成したモジュールファイルはこちらで公開しています。Pythonでは、デストラクタがうまく動作しないようなので、一時ファイルを削除できるメソッドを実装しています。またpython2でも動くようにしています。
アップロードされたファイルをチェックして、利用できる位置へ移動、保存する処理は、Webアプリごとに実装されるものになります。サンプルとなるコードを掲載します。
# モジュールのインポート
import cgi_decode
# 拡張子判定用
import os.path
# インスタンス作成、デコードと変数への設定
q = cgi_decode.Set()
# GET,POST,COOKIE,FILESが利用可能
# アップロードされたファイルを順番に処理
for i,f in enumerate(FILES):
n, ext = os.path.splitext(f['up_name'])
if ext=='.csv':
new_name = q.move(f,'upfile{}{}'.format(i+1,ext))
# アップロードされた一時ファイルを削除
q.clear()
最後にCGIでのデコードが出来ているか確認できるコードを掲載します。GETやPOST、COOKIEは連想配列(辞書)なので、name属性をキーとしてvalueを取得できます。
#!/usr/bin/python3
# モジュールのインポート
import cgi_decode
# 変数の内容を整形して表示するモジュール
from pprint import pprint
# インスタンス作成、デコードと変数への設定
q = cgi_decode.Set()
print("Content-Type: text/html; charset=utf-8\n")
print('<div style="white-space: pre;">')
print('=GET')
pprint(GET)
print('=POST')
pprint(POST)
print('=COOKIE')
pprint(COOKIE)
print('=FILES')
pprint(FILES)
print('</div>')
q.clear()
h = '''
<a href="?key1=日本語&キー=値&key1=複数データ">GET確認</a>
<form method="post">
<input type="text" name="email" value="form1@example.com">
<label>
<input type="radio" name="radio1" value="ラジオ">ラジオ
</label>
<label>
<input type="radio" name="radio1" value="ボタン">ボタン
</label>
<label>
<input type="checkbox" name="check1" value="チェック">チェック
</label>
<label>
<input type="checkbox" name="check1" value="ボックス">ボックス
</label>
<textarea name="text">複数行
テキスト</textarea>
<input type="submit" value="送信">
</form>
<form method="post" enctype="multipart/form-data">
<input type="text" name="email" value="form2@example.com">
<label>
<input type="radio" name="radio1" value="ラジオ">ラジオ
</label>
<label>
<input type="radio" name="radio1" value="ボタン">ボタン
</label>
<label>
<input type="checkbox" name="check1" value="チェック">チェック
</label>
<label>
<input type="checkbox" name="check1" value="ボックス">ボックス
</label>
<textarea name="text">複数行
テキスト</textarea>
<input type="file" name="f1">
<input type="file" name="f1">
<input type="file" name="f2">
<input type="submit" value="送信">
</form>
<style>
form {
margin: 50px auto;
width: 400px;
background: #eee;
}
input[type="text"],input[type="submit"],label,textarea {
display: block;
padding: 5px;
margin: 5px 0;
width: 100%;
}
</style>
'''
print(h)