urllib.unquoteとunicode
urllib.unquoteはURLエンコードされた文字列を元に戻す関数だ。よくURLで使われている「%E6%97%A5%E6%9C%AC%E8%AA%9E」みたいな文字を元に戻す時に使う。
# Python 2.5.2 on win32 >>> a = urllib.unquote('%E6%97%A5%E6%9C%AC%E8%AA%9E') >>> print a.decode('utf-8') 日本語
URLエンコードは文字コードに関して何も規定していない。だから、デコードした結果を画面に出力する場合は適当な文字コードで変換してやる必要がある。上の例ではUTF-8を使っているが、サイトによっては、以下のようにShift_JISやEUC-JPかもしれない。
>>> a = urllib.unquote('%93%FA%96%7B%8C%EA') >>> print a.decode('shift_jis') 日本語
>>> a = urllib.unquote('%C6%FC%CB%DC%B8%EC') >>> print a.decode('euc-jp') 日本語
ここで、もしunquoteの引数をunicodeで渡したら戻り値はどうなるだろうか? 似たような他の関数から言って、unicodeを返すのが妥当のような気がするし、実際そうなっている。
>>> a = urllib.unquote(u'%E6%97%A5%E6%9C%AC%E8%AA%9E') >>> type(a) <type 'unicode'>
しかし、この動作はおかしい。unquote関数は、渡されたデータがどの文字コードで符号化されているのか知らない。知らないのにunicodeにできるはずがない。
実際、変数aの中身をprintするとエラーになる。
>>> print a Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeEncodeError: 'cp932' codec can't encode character u'\xe6' in position 0: illegal multibyte sequence
変数aの中身はこうである。
>>> a u'\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e'
どこもおかしくないように見えるが、UTF-8で符号化されたままになっているので正しいunicodeではない。
unquoteのソースはこうだ。
# lib/urllib.py より引用 _hextochr = dict(('%02x' % i, chr(i)) for i in range(256)) _hextochr.update(('%02X' % i, chr(i)) for i in range(256)) def unquote(s): """unquote('abc%20def') -> 'abc def'.""" res = s.split('%') for i in xrange(1, len(res)): item = res[i] try: res[i] = _hextochr[item[:2]] + item[2:] except KeyError: res[i] = '%' + item except UnicodeDecodeError: res[i] = unichr(int(item[:2], 16)) + item[2:] return "".join(res)
UnicodeDecodeErrorをキャッチしているので、一見するとunicodeに対応しているように見える。しかし、1バイトずつunichrをかけるという、よく意味のわかない処理が行われているので、UTF-8のままunicodeにされてしまう。
>>> a = urllib.unquote(u'%E6%97%A5%E6%9C%AC%E8%AA%9E') >>> print a.encode('raw_unicode_escape').decode('utf-8') 日本語
のように二重変換すれば上手くいくが、このコードは何をしているのがすぐ理解できない。
自前で実装(改造)してもいいかもしれない。例えばこんなふうに。
def unquote_u(uni, enc='ascii'): # 変数 uni はURLエンコードされているのだから、 # ascii以外入っていない。入っていたらおかしい。 s = uni.encode('ascii') res = s.split('%') buf = [res[0],] for i in xrange(1, len(res)): item = res[i] buf.append(chr(int(item[:2], 16))) buf.append(item[2:]) return ''.join(buf).decode(enc) print unquote_u(u'%E6%97%A5%E6%9C%AC%E8%AA%9E', 'utf-8') print unquote_u(u'%93%FA%96%7B%8C%EA', 'shift_jis') print unquote_u(u'%C6%FC%CB%DC%B8%EC', 'euc-jp')
この方法は事前に変換テーブルを作らないので、オリジナルよりも遅い。それに今書いたので、ちゃんとテストしていない。
オリジナルのコードにあった split('%') はとてもいいアイデアだと思う。