ソースコードで学ぶWebプログラミング

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)