Python CGIでGET/POST等のデータ受け取りについて

文字コードPython str は utf-8 一方 Javascript は utf-16

FieldStorageクラスを使って cgi.FieldStorage() と書くのが基本。このクラスを通せば文字コードは変換してくれる。

但しこのままでは普通の配列ではないので使いにくい。

form = cgi.FieldStorage() 
orm_dict={}
for key in form.keys():
    form_dict[key] = form.getvalue(key)

そこでこのように普通の dictionary としてリストを作っておく。

=+?を含んだデータのやり取りをどうするか

簡単な解決法が HTML 側の encodeURIComponent である。ところが下記場合にトラブルが発生。

<a href="https://exqmple.com/%E6%97%A5%E6%9C%AC%E8%AA%9E
">https://exqmple.com/日本語</a>

これをNode.jsのようなJavascriptで受けるのは問題ない。

decodeURIComponent(encodeURIComponent('<a href="https://exqmple.com/%E6%97%A5%E6%9C%AC%E8%AA%9E">https://exqmple.com/日本語</a>'))
"<a  ref="https://exqmple.com/%E6%97%A5%E6%9C%AC%E8%AA%9E">https://exqmple.com/日本語</a>"

Pythonの問題 +と%20

時代によってスペースがURLエンコードで+になるか%20になるかの違いがある。それを受け取った時に対応できないが、先人の知恵で「urllib.parse.unquote_plus」を使えば解決する。

>>> import urllib.parse
>>> urllib.parse.unquote("space%20space+space")
'space space+space'
>>> urllib.parse.unquote_plus("space%20space+space")
'space space space'
>>>

RFC1738 (1994年)+RFC1808→RFC2396 (1998年)→RFC3986 (2005年)と時代が update されている中、未だにURLと言ってしまうのも年がバレる。

この辺りの話は、「encodeURIComponentが世界基準だと誤解してた話」が詳しい。

encodeURIComponent を使ってみる

encodeURIComponent , decodeURIComponent はおそらくWebでのやり取りをするために作られたものである。HTMLで問題のあるURLを教えてくださいというフォームを作り、「送信」ボタンで「http://claim.world/?http://issue.get/?why=send」GETしたら、受け取り側はURLを分解してしまう。

そもそもURIエンコード表は以下の通り。

RFC3986(2005年)RFC2396(1998年)
0-9 a-z A-Z自由自由
– . _ ~自由自由
!'()*[]予約自由
#$&+,/:;=?@予約予約
RFC 3986 URI Generic Syntax January 2005
reserved = gen-delims / sub-delims
gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 
sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
RFC 2396
reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
unreserved = alphanum | mark
mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"

昔はhttp://xxx.xx/photo?re=1951(195)なんてリクエストもみかけたな。

そこでencodeURIcomponent には予約文字もエンコードする事になっているはずだ。

18.2.6.4encodeURI ( uri )
The encodeURI function computes a new version of a UTF-16 encoded (6.1.4) URI in which each instance of certain code points is replaced by one, two, three, or four escape sequences representing the UTF-8 encoding of the code points.
The encodeURI function is the %encodeURI% intrinsic object. When the encodeURI function is called with one argument uri, the following steps are taken:
Let uriString be ? ToString(uri).
Let unescapedURISet be a String containing one instance of each code unit valid in uriReserved and uriUnescaped plus “#”.
Return ? Encode(uriString, unescapedURISet).
NOTE
The code unit “#” is not encoded to an escape sequence even though it is not a reserved or unescaped URI code point.

18.2.6.5encodeURIComponent ( uriComponent )
The encodeURIComponent function computes a new version of a UTF-16 encoded (6.1.4) URI in which each instance of certain code points is replaced by one, two, three, or four escape sequences representing the UTF-8 encoding of the code point.

https://www.ecma-international.org/ecma-262/10.0/index.html#sec-encodeuricomponent-uricomponent

ところがというのには理由がある。Firefox も Chrome も RFC3986 に対応していないのだ。

encodeURIComponent("!'()*#$&+,/:;=?@[]")
"!'()*%23%24%26%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"
encodeURI("!'()*#$&+,/:;=?@[]")
"!'()*#$&+,/:;=?@%5B%5D"

どちらも結果は「!'()*」はエンコードされない。

ついでにencodeURIでも「[]」がエンコードされた。

つまり encodeURIComponent では「#$&+,/:;=?@」がエンコードされ適切にサーバーに引き渡される。

Python には decodeURIComponent に相当するデコーダが無い

エンコーダもない。つまり自作する以外ないのだ。あまりスマートではない。但し「$&+,/:;=?@」をデコードできないわけではない。

JavaScript

encodeURIComponent("#$&+,/:;=?@")
"%23%24%26%2B%2C%2F%3A%3B%3D%3F%40"

Python

Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 21:26:53) [MSC v.1916 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
import urllib.parse
urllib.parse.unquote("%23%24%26%2B%2C%2F%3A%3B%3D%3F%40")
'#$&+,/:;=?@'

Pythonでは「$&+,/:;=?@」を一発でエンコードすることが出来ないから通信することが出来ない。

時代は Node.js だよ。

Base64で

だってPythonが好きなんだもん。何年か経って思い出しやすいのが Python > Java >>>>> Perl >Basic

Basicで配列使ったマクロは自分で作っておきながら理解不能で未だに改造できずに使ってます。

Pythonでの受け取りは「base64.standard_b64decode() 」が便利。「base64.standard_b64decode(“44GT44KT44Gr44Gh44Gv”)」

と引数にstrが書ける。という事はPOSTされた文字列をそのまま渡せる。

よく例として出されているのが、「base64.b64decode(b”44GT44KT44Gr44Gh44Gv”)」だが、エスケープしなくても良い。

ところが、Javascriptの方は問題だ。btoa() がUTFを受け付けない

その事についての解決はhttps://blackninja.home.blog/javascript%e3%83%a1%e3%83%a2/javascript-base64-%e3%81%a8-utf/ のページにて。

Base64による +/=

Python のマニュアル見てて気が付いたが、urlsafe_b64encode(s)というのがあって+/の代わりに-_を使う関数があった。言われてみれば確かにURLとして危ない。但しこの関数もパディングの=については何の処理もしないらしい。良いのか。

base64.urlsafe_b64encode(b'\xff\xef')
b'_-8='

JavaScripには当然解釈できない。

atob("_-8=")
VM159:1 Uncaught DOMException: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.
at :1:1
atob("/+8=")
"ÿï"

という事はPOSTするデータをBase64するのは危険。またjavascriptでutf-16な文字をBase64で包み込んでしまうので、utf-8なPythonでデコードした時に気を使って面倒。

やっぱりURIかな

Python側に「$&+,/:;=?@」をエンコードさせる関数を足してブラウザに送るのが、正しいかも。

Python には translateという、文字列から文字を入れ替える事の出来る関数がある。(但し置換元は1文字のみ)

print("oOtTnN".translate(str.maketrans({'o': 'O', 't': 'TT', 'n': None})))
OOTTTN

これを使って、補助的にエンコードすることとする。

print("url?&=".translate(str.maketrans({\
'#':'%23',\
'$':'%24',\
'&':'%26',\
'+':'%2B',\
',':'%2C',\
'/':'%2F',\
':':'%3A',\
';':'%3B',\
'=':'%3D',\
'?':'%3F',\
'@':'%40'\
})))
url%3F%26%3D

さて一行で書くとこのような感じとなり、下にjavascriptでの結果も載せておく

print("#$&+,/:;=?@".translate(str.maketrans({'#':'%23','$':'%24','&':'%26','+':'%2B',',':'%2C','/':'%2F',':':'%3A',';':'%3B','=':'%3D','?':'%3F','@':'%40'})))
%23%24%26%2B%2C%2F%3A%3B%3D%3F%40

encodeURIComponent("#$&+,/:;=?@")
"%23%24%26%2B%2C%2F%3A%3B%3D%3F%40"