はじめに
Webフレームワークが嫌いな弊社
APIもDjangoやFlaskは使わずにCGI化したPythonで作ってる
(ホントに今時の自社サービス系の会社か?)
ブラウザから何かしら受け取る時って辞書に加工したい時、cgiモジュールのFieldStorage使ってるけど
input = cgi.FieldStorage()
diction = {n: input.getvalue(n) for n in input.keys()}
こいつがPython3.13から廃止になるらしいのでなんとかしてみた
コード
画面はこんな感じ、とりあえずGETとPOSTができる作りならなんでもいい
(HTMLとJSなんも知らんから適当に書いてる)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>テスト</title>
</head>
<body>
<p>POST値がそのまま返ってきます</p>
<p>
<label><input type="text" id="id" placeholder="id"></label>
<label><input type="text" id="name" placeholder="氏名"></label>
<label><input type="text" id="age" placeholder="年齢"></label>
<label><input type="text" id="bikou" placeholder="なんでも"></label>
<input type="button" value="POST" id="PostButton" onclick="ButtonClick(this)">
</p>
<p>URLパラメタがそのまま返ってきます</p>
<p>
<input type="button" value="GET" id="GetButton" onclick="ButtonClick(this)">
</p>
<p id="msg"></p>
<script type="text/javascript">
function ButtonClick(button){
let formData = new FormData();
formData.append("id", encodeURIComponent(document.getElementById('id').value));
formData.append("name", encodeURIComponent(document.getElementById('name').value));
formData.append("age", encodeURIComponent(document.getElementById('age').value));
formData.append("bikou", encodeURIComponent(document.getElementById('bikou').value));
let xhl = new XMLHttpRequest();
if (button.id == 'PostButton') {
xhl.open('POST', 'test.py', false);
xhl.setRequestHeader( 'Content-Type', 'Multipart/form-data' );
xhl.send(formData);
}
if (button.id == 'GetButton') {
//hoge=huga foo=baa piyo=baz qux=netagire をパラメタにしてGETする
xhl.open('GET', 'test.py?hoge=huga&foo=baa&piyo=baz&qux=netagire', false);
xhl.setRequestHeader( 'Content-Type', 'Multipart/form-data' );
xhl.send();
}
document.getElementById('msg').innerText = xhl.response;
}
</script>
</body>
</html>
できあがった画面がこれ
バカデカい余白にPOSTデータ、GETパラメタがそのまま表示される
バックエンドのPython
#!/usr/local/bin/python3.9
import os
import json
import sys
import urllib.parse
print('Status: 200 OK')
print('Content-Type: application/json\n')
if os.environ['REQUEST_METHOD'] == 'GET' :
#環境変数QUERY_STRINGから取り出したURLパラメタを辞書にパース
url_dict = urllib.parse.parse_qs(os.environ['QUERY_STRING'])
'''
url_dict = {'hoge': ['huga'], 'foo': ['baa'], 'piyo': ['baz'], 'qux': ['netagire']}
辞書のバリュー部が長さ1のリストなので、頭だけ抽出すれば
{'hoge': 'huga', 'foo': 'baa', 'piyo': 'baz', 'qux': 'netagire'} みたいな辞書ができるね
'''
diction = {n: url_dict[n][0] for n in url_dict.keys()}
if os.environ['REQUEST_METHOD'] == 'POST' :
#標準入力を環境変数CONTENT_LENGTHだけ読み込み、splitlines()で改行文字を''にする
stdin = sys.stdin.read(int(os.environ['CONTENT_LENGTH'])).splitlines()
'''
このときのstdinの中身は
------WebKitFormBoundaryBJHNmZMBUlxGg07O
Content-Disposition: form-data; name="id"
POSTデータ
------WebKitFormBoundaryBJHNmZMBUlxGg07O
.
.
.
の繰り返し
こいつを4行ずつ区切ったものを重ねて奥行きを持たせる(この程度ならNumPy使わんでいいか)
'''
stdin = [stdin[n:n+4] for n in range(len(stdin)) if ((n == 0 or n % 4 ==0))]
'''
このときのstdinの中身は
[
['------WebKitFormBoundaryWVRIhGMy4qjlrgQA', 'Content-Disposition: form-data; name="フォームid"', '', 'POSTデータ'],
['------WebKitFormBoundaryWVRIhGMy4qjlrgQA', 'Content-Disposition: form-data; name="フォームid"', '', 'POSTデータ'],
(繰り返し)
]
なお末尾はPOST値が入ってないので削除
'''
stdin.pop(-1)
#各リストの2番目がキー、4番目が値になる辞書を作成
#2番目の内、Content-Disposition...は固定なのでトリミング
diction = {n[1][38:-1] : n[3] for n in stdin}
print(json.dumps(diction))
クラスのプロパティとして扱いたいならこんな記述になるのかな
#!/usr/local/bin/python3.9
import os
import json
import sys
import urllib.parse
class Hoge :
#selfって打つのがめんどくさいからthisのtにしちゃう
def __init__(t) :
if os.environ['REQUEST_METHOD'] == 'GET' :
#同様にURLパラメタを辞書にパース
url_dict = urllib.parse.parse_qs(os.environ['QUERY_STRING'])
#各プロパティを辞書のキーで命名、辞書の各値の最初の要素を代入
for n in url_dict.keys() :
setattr(t,n,url_dict[n][0])
#exec('t.' + n + '= url_dict[n][0]') でもいいかも?
#安直にt.n = url_dict[n][0]とかやっちゃうとキーで命名されないので注意(tのnになっちゃう)
if os.environ['REQUEST_METHOD'] == 'POST' :
#標準入力を読み込んで2次元のリストにする→4行区切りで重ねる→末尾を削除 までは同様
stdin = sys.stdin.read(int(os.environ['CONTENT_LENGTH'])).splitlines()
stdin = [stdin[n:n+4] for n in range(len(stdin)) if ((n == 0 or n % 4 ==0))]
stdin.pop(-1)
#各プロパティをリストの2番目の要素で命名、4番目の値を代入
for n in stdin :
setattr(t,n[1][38:-1],n[3])
#同様にexec('t.' + n[1][38:-1] + ' = n[3]') でも良さそうに見える
#ここでもt.n[1][38:-1] = n[3] とか目論んではいけない
#コンストラクタ内で出力メソッド呼ぶ
t.out()
def out(t) :
print('Status: 200 OK')
print('Content-Type: application/json\n')
#以下、拘りがない人は print(json.dumps(vars(t))) で終わり
#プロパティを辞書に加工、値部分をデコード
diction = {n: urllib.parse.unquote(vars(t)[n]) for n in vars(t).keys()}
#吐き出す時もensure_ascii=False渡してエンコード禁止
print(json.dumps(diction,ensure_ascii=False))
exit()
#インスタンス化
Hoge()
ボタン押してPOSTデータが返ってきたことを確かめてみよう
GETの時もちゃんと取れてる
とりあえずこいつをたたき台にしていろんなPOST、GETを試してみる
10/14追記
POSTデータに番号がついてた場合にリストで認識させたいので改良
まずはPOSTする画面を作ってみる
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>テスト</title>
</head>
<body>
<p>POST値がそのまま返ってきます</p>
<p>
<label><input type="text" id="id" placeholder="id"></label>
<label><input type="text" id="name[0]" placeholder="姓"></label>
<label><input type="text" id="name[1]" placeholder="名"></label>
<label><input type="text" id="name[2]" placeholder="ミドルネーム(あれば)"></label>
<label><input type="text" id="age" placeholder="年齢"></label>
<label><input type="text" id="bikou[0]" placeholder="備考1"></label>
<label><input type="text" id="bikou[1]" placeholder="備考2"></label>
<input type="button" value="POST" id="PostButton" onclick="ButtonClick(this)">
</p>
<p>URLパラメタがそのまま返ってきます</p>
<p>
<input type="button" value="GET" id="GetButton" onclick="ButtonClick(this)">
</p>
<p id="msg"></p>
<script type="text/javascript">
function ButtonClick(button){
let formData = new FormData();
formData.append("id", encodeURIComponent(document.getElementById('id').value));
formData.append("name[0]", encodeURIComponent(document.getElementById('name[0]').value));
formData.append("name[1]", encodeURIComponent(document.getElementById('name[1]').value));
formData.append("name[2]", encodeURIComponent(document.getElementById('name[2]').value));
formData.append("age", encodeURIComponent(document.getElementById('age').value));
formData.append("bikou[0]", encodeURIComponent(document.getElementById('bikou[0]').value));
formData.append("bikou[1]", encodeURIComponent(document.getElementById('bikou[1]').value));
let xhl = new XMLHttpRequest();
if (button.id == 'PostButton') {
xhl.open('POST', 'test.py', false);
xhl.setRequestHeader( 'Content-Type', 'Multipart/form-data' );
xhl.send(formData);
}
if (button.id == 'GetButton') {
//hoge=huga foo=baa piyo=baz qux=netagire をパラメタにしてGETする
xhl.open('GET', 'test.py?hoge=huga&foo=baa&piyo=baz&qux=netagire', false);
xhl.setRequestHeader( 'Content-Type', 'Multipart/form-data' );
xhl.send();
}
document.getElementById('msg').innerText = xhl.response;
}
</script>
</body>
</html>
姓,名,ミドルネームは長さ3、備考は長さ2
尤も名称に番号がついてるだけで厳密には配列じゃないけど...
バックエンドのPython(改良版)はこれ
ついでにデコードの辺りもちょっと変更
#!/usr/local/bin/python3.9
import os
import json
import sys
import re
import urllib.parse
class Hoge :
#selfって打つのがめんどくさいからthisのtにしちゃう
def __init__(t) :
if os.environ['REQUEST_METHOD'] == 'GET' :
#同様にURLパラメタを辞書にパース
url_dict = urllib.parse.parse_qs(os.environ['QUERY_STRING'])
#各プロパティを辞書のキーで命名、辞書の各値の最初の要素を代入
for n in url_dict.keys() :
setattr(t,n,url_dict[n][0])
#exec('t.' + n + '= url_dict[n][0]') でもいいかも?
#安直にt.n = url_dict[n][0]とかやっちゃうとキーで命名されないので注意(tのnになっちゃう)
if os.environ['REQUEST_METHOD'] == 'POST' :
#標準入力を読み込んで2次元のリストにする→4行区切りで重ねる→末尾を削除 までは同様
stdin = sys.stdin.read(int(os.environ['CONTENT_LENGTH'])).splitlines()
stdin = [stdin[n:n+4] for n in range(len(stdin)) if ((n == 0 or n % 4 ==0))]
stdin.pop(-1)
#多次元配列を辞書に変換、2番目のContent-Disposition...をトリミングしたものと4番目のペアとする
dict_stdin = {n[1][38:-1] : n[3] for n in stdin}
for n in dict_stdin.keys() :
#辞書のキーの最後の文字が']'に引っかかった場合、POSTデータを配列と見なす
if n.endswith(']'):
#要素番号の開始位置、つまりは'['があるところの位置引数を引っ張り出す
start = re.search(r'\[', n).start()
#キーの要素番号をトリミングしたもの([0:start]だね)で引っかけて1つのリストにまとめる
keys = [m for m in dict_stdin.keys() if re.match(r"\b(?=\w)" + re.escape(n[0:start]) + r"\b(?!\w)", m) is not None]
#今作ったリストで引っかけて値だけ1つのリストにまとめる、ついでにデコードもする
values = [urllib.parse.unquote(dict_stdin[m]) for m in keys]
#属性名はキーを[0:start]でトリミングしたもの、値はリストでプロパティにセット
setattr(t,n[0:start],values)
else :
#リストで入ってこなかった場合はセット時にデコードする
setattr(t,n,urllib.parse.unquote(dict_stdin[n]))
#コンストラクタ内で出力メソッド呼ぶ
t.out()
def out(t) :
print('Status: 200 OK')
print('Content-Type: application/json\n')
print(json.dumps(vars(t),ensure_ascii=False))
exit()
#インスタンス化
Hoge()





Comments
初めまして!
有益な投稿をありがとうございます。
当方、PythonでWebアプリを作成したく、ググって情報を調べておりました。しかし肝心のCGIモジュールが廃止となっていて、どうにかならないかと再度ググって今回の貴殿の投稿を見つけました。
質問したいことがあります。
お忙しいところすみませんけれども、興味を持っていただけましたら、お時間のある時に返信をお願いできませんか?
質問1) CGIモジュール自体はまだPythonのGithub上にあるので、これをコピペして、自分のローカルで使用することはできないですかね?
質問2) Pythonの仮想環境上で動くPythonプログラムをブラウザ上で動かすことは可能ですか?
Let's comment your feelings that are more than good