チルダ(tilde
, ~
)を Python3 の urllib.parse.quote()
で URL エンコードすると、チルダ記号まで %7E
とエンコードされてしまう。RFC-3986 に準拠していないのかしら。
import urllib.parse
str_raw = '-._~'
str_enc = urllib.parse.quote(str_raw)
print(str_enc)
-._%7E
-._~
- オンラインで事象確認する @ paiza.IO
TL;DR(概要)
Python 3.6 までの仕様です。 Python 3.7 で修正されています。
Python 3.6 までは RFC-2396 に準拠しており RFC-3986 に準拠していません。そのため、チルダもエンコードするのは Python <= 3.6 までの仕様です。Python 3.7(issue #16285)で修正されています。
-
urllib.parse.quote()
は- Python <= 3.6 → RFC 2396
- Python >= 3.7 → RFC 3986
- Python 3.6 以前のバージョンでは、
safe
パラメーターに「~
」を追加して暫定回避できますstr_enc = urllib.parse.quote(str_raw, safe='~')
以下は Python3.7 の Docker イメージの、コンテナ上で実行したところです。同じソースでも、ちゃんとチルダがアンエスケープ(~
のまま表示)されています。
$ # Python 3.7 の Docker イメージをダウンロード
$ sudo docker pull python:3.7
...(略)...
$ # コンテナを作成 & Bash で操作
$ docker run -it python:3.7 bash
root@xxxxx:/#
root@xxxxx:/# python -V
Python 3.7.1
root@xxxxx:/#
root@xxxxx:/# python
Python 3.7.1 (default, Nov 16 2018, 21:59:43)
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> import urllib.parse
>>>
>>> str_raw = '-._~'
>>> str_enc = urllib.parse.quote(str_raw)
>>> print(str_enc)
-._~
Python 3.7 より前のバージョンでは、safe
パラメーターに「~
」を追加して回避できます。
>>>
>>> import urllib.parse
>>>
>>> str_raw = '-._~'
>>> str_enc = urllib.parse.quote(str_raw, safe='~')
>>> print(str_enc)
-._~
- オンラインで動作確認する @ paiza.IO
TS;DR(詳細)
URL エンコードの正しい仕様
URL エンコーディングとは、URI で使うためにパーセントエンコーディングすることです。
現在の URI の標準規格(STD66)では、URL エンコードは RFC-3986 と呼ばれる標準で規格が定義されています。(2018/12/19 現在)
そして、この RFC-3986 の 2.3 の項目で「[a-zA-Z]
[0-9]
-
.
_
~
」はエスケープしない仕様として定義されています。つまり、これらは "Unreserved Characters
"(非予約文字扱い)というわけです。
RFC-3986-2.3 原文と日本語訳
2.3. Unreserved Characters
Characters that are allowed in a URI but do not have a reserved purpose are called unreserved. These include uppercase and lowercase letters, decimal digits, hyphen, period, underscore, and tilde.
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
(Uniform Resource Identifier (URI): Generic Syntax @ IETF より)
2.3. 非予約文字
URI 内に含む事が認められており、予約目的のない文字は、予約されていない (unreserved) と呼ばれる。 これは、大文字と小文字のアルファベット、数字、ハイフン、ピリオド、アンダースコア、チルダが含まれる。
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
(RFC3986 日本語訳(橋本英彦 氏)の複製 | @ GitHub より)
マ?チルダ注意!
問題は、これら非予約文字のうちの「チルダ」(~
)です。
Python3 の urllib.parse.quote()
は RFC-3986 に準拠しているっぽいことがネットで散見されるものの、PHP7 の rawurlencode()
関数 など他の言語のエンコードと比較した時に結果が異なることに気づきました。
$ python3
>>> import urllib.parse
>>> print(urllib.parse.quote('Matilda~Amuro'))
Matilda%7EAmuro
$ php -a
php > print_r(rawurlencode('Matilda~Amuro'));
Matilda~Amuro
$ node
> encodeURIComponent('Matilda~Amuro')
'Matilda~Amuro'
あなたはエスケープかもしれない
どうやらチルダが %7E
にエスケープされてしまっています。どちらもデコードすれば結果は同じですし、「問題のある文字がエンコードされないならいざ知らず、エンコードしてんだし良いじゃん」と思います。
しかし、とある UnitTest の1つが「RFC-3986 に準拠してエンコードしているか」のテストであったため、パスさせるために何とかしないといけなくなりました。(正規表現でマッチさせればいいのに…( ・´ω・`)ボソッ)
まぁ、urllib.parse.quote
の safe
パラメーターにチルダを指定するだけでエスケープの除外対象に追加されるので、現象自体は簡単に解決できます。
$ python3
>>> import urllib.parse
>>> s = 'Matilda~Amuro'
>>> print(urllib.parse.quote(s, safe='~'))
Matilda~Amuro
しかし、URL のエンコードはセキュリティにも関わってくるらしいので、本当に標準で RFC-3986 に準拠しているのか気になりました。
ドキュメントはお嫌?
何はともあれ、まずは公式ドキュメントの確認です。
Python 3.6 のドキュメントを見たところ、チルダさんがおりません。マジですか。つまり、RFC-3986 に準拠はしていないものの、Python の仕様通りの動きではあったわけです。
urllib.parse.quote (string, safe='/', encoding=None, errors=None) (原文)
string 内の特殊文字を %xx を使用してエスケープします。文字、数字、および '_.-' はクオートされません。デフォルトでは、この関数は URL のパス部分のクオートのために用意されています。任意のパラメータ safe を指定すると、指定した ASCII 文字もクオートされません。デフォルトは '/' です。
( 21.8.4. URL Quoting | 21.8. urllib.parse @ Python3 日本語公式ドキュメントより)
ところが! Python3.7.2rc1 や Python3.8-dev の英語版の最新ドキュメントを見ると、なんとチルダさんがいらっしゃるのです。
urllib.parse.quote (string, safe='/', encoding=None, errors=None)
Replace special characters in string using the %xx escape. Letters, digits, and the characters '_.-~' are never quoted. By default, this function is intended for quoting the path section of URL. The optional safe parameter specifies additional ASCII characters that should not be quoted — its default value is '/'.
しかも、チルダが追加されたことについても明記されていました。
Changed in version 3.7: Moved from RFC 2396 to RFC 3986 for quoting URL strings. “~” is now included in the set of reserved characters.
(URL Quoting | urllib.parse — Parse URLs into components」@ Python3.7 公式ドキュメントより)
つまり、今までは(Python3.6 までは)、URL エンコードは RFC 2396 に準拠していたが、Python3.7 から RFC 3986 に準拠するようになったということです。(RFC-2396
→ RFC-3986
)
(2018/12/23 日本語のドキュメントはまだ翻訳されていないようです。RCが取れたら公開されるのかもしれません。)
マ?チルダがアンジャン Issue に
確かに Docker で Python3.7.1 のイメージでコンテナ上で検証したところ、Python3.7 では ~
はエスケープされませんでした。RFC-3986 の仕様通りの動きです。
$ sudo docker pull python:3.7
(略)
$ docker run -it python:3.7 bash
root@xxxxxxxxxxxx:/# python -V
Python 3.7.1
root@xxxxxxxxxxxx:/# python
>>> import urllib.parse
>>> s = 'Matilda~Amuro'
>>> print(urllib.parse.quote(s))
Matilda~Amuro
どういう経緯で準拠したのか興味を持ったところ、2012/10/19 には Issue は上がっていたのですが、4年半の月日を経て「このご時世に RFC 3986 に準拠していないのはいかがなもんか」と思ったのか、2017/03/24 にやっと修正されたようです。
I think
~
should be added to the safe characters of quote function
- 「Update urllib quoting to RFC 3986」| Issue Tracker @ Python.org