Why not login to Qiita and try out its useful features?

We'll deliver articles that match you.

You can read useful information later.

1
1

(Python)CGIモジュール廃止への抗い

Last updated at Posted at 2023-09-16

はじめに

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パラメタがそのまま表示される
スクリーンショット 2023-09-16 190306.png

バックエンドの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データが返ってきたことを確かめてみよう
スクリーンショット 2023-09-16 222044.png
GETの時もちゃんと取れてる
スクリーンショット 2023-09-16 222156.png

とりあえずこいつをたたき台にしていろんな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
尤も名称に番号がついてるだけで厳密には配列じゃないけど...
スクリーンショット 2023-10-14 180813.png

バックエンドの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()

うまくいったぽい
スクリーンショット 2023-10-14 180813.png
画像データとCSVのPOST方法も考えた方がいいかもしれない

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Comments

@nomad_nagoya(ナゴヤ ノマド)

初めまして!

有益な投稿をありがとうございます。

当方、PythonでWebアプリを作成したく、ググって情報を調べておりました。しかし肝心のCGIモジュールが廃止となっていて、どうにかならないかと再度ググって今回の貴殿の投稿を見つけました。

質問したいことがあります。
お忙しいところすみませんけれども、興味を持っていただけましたら、お時間のある時に返信をお願いできませんか?

質問1) CGIモジュール自体はまだPythonのGithub上にあるので、これをコピペして、自分のローカルで使用することはできないですかね?

質問2) Pythonの仮想環境上で動くPythonプログラムをブラウザ上で動かすことは可能ですか?

0

Let's comment your feelings that are more than good

Being held Article posting campaign

paiza×Qiita記事投稿キャンペーン「プログラミング問題をやってみて書いたコードを投稿しよう!」

~
View details
1
1

Login to continue?

Login or Sign up with social account

Login or Sign up with your email address