5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python3 で URL エンコードするとチルダもエンコードされてしまう("~" → "%7E")

Last updated at Posted at 2018-12-20

チルダ(tilde, ~)を Python3 の urllib.parse.quote() で URL エンコードすると、チルダ記号まで %7E とエンコードされてしまうRFC-3986 に準拠していないのかしら。

Python3.6
import urllib.parse

str_raw = '-._~'
str_enc = urllib.parse.quote(str_raw)
print(str_enc)
出力結果(%7Eに注目)
-._%7E
期待する出力結果(~に注目)
-._~

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 イメージの、コンテナ上で実行したところです。同じソースでも、ちゃんとチルダがアンエスケープ(~ のまま表示)されています。

Python3.7.1(最終行の~に注目)
$ # 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 パラメーターに「~」を追加して回避できます。

Python3.6(safeパラメーター付き)
>>> 
>>> import urllib.parse
>>> 
>>> str_raw = '-._~'
>>> str_enc = urllib.parse.quote(str_raw, safe='~')
>>> print(str_enc)
-._~

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.4.2(チルダが%7Eにエスケープされている)
$ python3
>>> import urllib.parse
>>> print(urllib.parse.quote('Matilda~Amuro'))
Matilda%7EAmuro
PHP7.1.12
$ php -a
php > print_r(rawurlencode('Matilda~Amuro'));
Matilda~Amuro
Node.js11.3.0
$ node
> encodeURIComponent('Matilda~Amuro')
'Matilda~Amuro'

あなたはエスケープかもしれない

どうやらチルダが %7E にエスケープされてしまっています。どちらもデコードすれば結果は同じですし、「問題のある文字がエンコードされないならいざ知らず、エンコードしてんだし良いじゃん」と思います。

しかし、とある UnitTest の1つが「RFC-3986 に準拠してエンコードしているか」のテストであったため、パスさせるために何とかしないといけなくなりました。(正規表現でマッチさせればいいのに…( ・´ω・`)ボソッ)

まぁ、urllib.parse.quotesafe パラメーターにチルダを指定するだけでエスケープの除外対象に追加されるので、現象自体は簡単に解決できます。

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-2396RFC-3986

(2018/12/23 日本語のドキュメントはまだ翻訳されていないようです。RCが取れたら公開されるのかもしれません。)

マ?チルダがアンジャン Issue に

確かに Docker で Python3.7.1 のイメージでコンテナ上で検証したところ、Python3.7 では ~ はエスケープされませんでした。RFC-3986 の仕様通りの動きです。

Python3.7.1
$ 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

5
2
0

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
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?