URLエンコードされた(?)JSONとpythonのワンライナー

はじめに

あるサービスでページの状態をURLとして共有するときに以下の様な形で表現されていた。

http%253A//example.net%253Bstate%253D%257B%2522x%253A%2522%253A%252010%252C%2520%2522y%2522%253A%252020%252C%2520%2522name%2522%253A%2520%2522hello%2522%257D

URL中にJSONとして状態が埋め込まれているのだけれど。。この状態を取り出したい。

pythonでURLエンコードのdecode

URLエンコードされているのかな?と思い試してみる。

過去に何度も調べていて嫌になっていたので、この辺りについては昔記事に書いた。

要約すると、URLlib.parse.unquote() (あるいは URLlib.parse.unquote_plus() )を使えば大丈夫。なのだけれど。わざわざpythonインタプリタも立ち上げたくないし。ワンライナーで済ませたい。

dictknife --unescape オプション

これを元に自分の作っているutility commandに機能を追加していた。めんどくさかったし。

以下の様にして使える。

$ echo "%7B%22name%22%3A+%22foo%22%2C+%22age%22%3A+20%7D" | dictknife --unescape URL cat -f json
{
  "name": "foo",
  "age": 20
}

あー、なるほどねという形で渡してみる。

$ echo http%253A//example.net%253Bstate%253D%257B%2522x%253A%2522%253A%252010%252C%2520%2522y%2522%253A%252020%252C%2520%2522name%2522%253A%2520%2522hello%2522%257D | dictknife --unescape URL
http%3A//example.net%3Bstate%3D%7B%22x%3A%22%3A%2010%2C%20%22y%22%3A%2020%2C%20%22name%22%3A%20%22hello%22%7D

期待とは違っていた。なんでだろうと思いよーく見てみる。

よく見ると4桁になっていた。ところで、末尾はおそらく } なのだけれど、URLエンコードされてると見做すと、}%7D になる。しかし%257D だった。そして、 %25 はURLエンコードされると %%25% で置き換えると上手く動くようになる。

import URLlib.parse as p

s = "%253Bstate%253D%257B%2522x%253A%2522%253A%252010%252C%2520%2522y%2522%253A%252020%252C%2520%2522name%2522%253A%2520%2522hello%2522%257D"

print(p.unquote(s.replace("%25", "%")))
# => ';state={"x:": 10, "y": 20, "name": "hello"}'

やっぱりワンライナーで済ませたい。

考えてみるとURLエンコードが2回かかっているだけだった。なので直接2回かければ大丈夫ではある。

$ echo "%253Bstate%253D%257B%2522x%253A%2522%253A%252010%252C%2520%2522y%2522%253A%252020%252C%2520%2522name%2522%253A%2520%2522hello%2522%257D" | dictknife --unescape url cat -f raw | dictknife --unescape url cat -f raw
;state={"x:": 10, "y": 20, "name": "hello"}

一応pythonだけで済ませられるけれど。これはこれでめんどくさい。

$ echo "%253Bstate%253D%257B%2522x%253A%2522%253A%252010%252C%2520%2522y%2522%253A%252020%252C%2520%2522name%2522%253A%2520%2522hello%2522%257D" | python -c 'import urllib.parse as p; import sys; print(p.unquote(p.unquote(sys.stdin.read().strip())))'
;state={"x:": 10, "y": 20, "name": "hello"}

もう少し楽に

--unescape でサポートされているのは、unicodeとurlだけだった。せっかくpythonは動的にコードを渡せるのでこれを良い感じにカスタマイズできるようにしたい(幸い今回はurlencodeが2回掛かっただけだったけれど。fでencodeしてgでencodeという状況のときに手軽に処理を差し込みたい)。

$ dictknife -h
usage: dictknife [-h] [--log {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}] [-q]
                 [--debug] [--compact] [--flatten] [--unescape {unicode,url}]
                 {cat,concat,transform,diff,shape,mkdict} ...

positional arguments:
  {cat,concat,transform,diff,shape,mkdict}

optional arguments:
  -h, --help            show this help message and exit
  --log {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}
  -q, --quiet
  --debug
  --compact
  --flatten
  --unescape {unicode,url}

テキトーに調べてもこれはツライ。

どうすると良いんだろうなー。