読者です 読者をやめる 読者になる 読者になる

Python3で、リテラルのエスケープシーケンスに見える非リテラルの文字列を、Unicode文字へと変換する

Bottle Python

Python3で、リテラルに改行コードなどを含めたい場合、エスケープシーケンスを使います。
2.4.1. 文字列およびバイト列リテラル | 2. 字句解析 — Python 3.6.1 ドキュメント

 
例えば、「Hello(改行) world」としたい場合、

$ python
Python 3.6.1 (default, Apr  5 2017, 11:58:06) 
>>> print('Hello world')
Hello world
>>> print('Hello\n world')
Hello
 world

と、エスケープシーケンスの\nを使います。

 
また、エスケープシーケンスを使ってUnicode文字を表すこともできます。

例えば、\oooは、

8 進数値 ooo を持つ文字

文字列リテラル中では、エスケープ文字は与えられた値を持つ Unicode 文字を表します。

として使えます。

例えば、「a」というUnicode文字を8進数エスケープシーケンスで表す場合は、

>>> print('\141')
a

と、\141というエスケープシーケンスを使います。

 
そこで、今回の本題の「リテラルのエスケープシーケンスに見える非リテラルの文字列」についてです。

\141リテラルではなくて文字列データとして与えられた場合に、Unicode文字aへ変換する方法をメモします。

 
目次

 

環境

 

調べようと思ったそもそもの経緯

以前、wsgi-interceptを使ってテストを書いたときのことです。
Pythonで、wsgi-interceptを使って、WSGIサーバを起動せずにWSGIアプリのテストをする - メモ的な思考的な

 
上記では触れませんでしたが、CookieのテストのためにCookieの値を調べたところ、

form = {
    'title': 'タイトル',
    'handle': 'あ',
    'message': 'メッセージ',
}
with RequestsInterceptor(self.get_app, host='localhost', port=8081) as url:
    actual = requests.post(url, data=form, allow_redirects=False)

# ライブラリRequestsのCookieオブジェクトから、Cookieの値を取得する
handle = actual.cookies['handle']

# Cookieの値
print(handle)
#=> "\343\201\202"

# Cookieの値の型
print(type(handle))
#=> <class 'str'>

と、Cookieの値()がエスケープシーケンスに見える文字列(8進数3桁表記で\343\201\202)となっていました。

ここで、BottleのCookieFormsDict型に入っていて、latin1でデコードされています。

In Python 3 all strings are unicode, but HTTP is a byte-based wire protocol. The server has to decode the byte strings somehow before they are passed to the application. To be on the safe side, WSGI suggests ISO-8859-1 (aka latin1), a reversible single-byte codec that can be re-encoded with a different encoding later.

Notes | INTRODUCING FORMSDICT| Tutorial — Bottle 0.13-dev documentation

 
そこで、encode & decodeしてみましたが、

encoded = handle.encode('latin1')
print(encoded)
#=> b'"\\343\\201\\202"'

decoded = encoded.decode('utf-8')
print(decoded)
#=> "\343\201\202"

と変わりませんでした。

ダブルクォート(")が邪魔なのかとも思いましたが、

replaced = handle.strip('"').strip()
print(replaced)
#=> \343\201\202
print(type(replaced))
#=> <class 'str'>

encoded = replaced.encode('latin1')
print(encoded)
#=> b'\\343\\201\\202'

decoded = encoded.decode('utf-8')
print(decoded)
#=> \343\201\202

結果は変わりませんでした。

 
そこで、このリテラルのエスケープシーケンスに見える非リテラルの文字列をUnicode文字にする方法を調べることにしました*1

 

方法

stackoverflowに情報がありました。
string formatting - How to convert escaped characters in Python? - Stack Overflow

 
以下の2種類の方法が紹介されていました。

  • codecsモジュールのgetdecoder()
  • astモジュールのliteral_eval()

両方ともPythonの標準モジュールにあったため、今回はそれぞれを試してみます。

 

codecs.getdecoderを使う例

codecsモジュールの公式ドキュメントは以下です。
codecs.getencoder() | 7.2. codecs — codec レジストリと基底クラス — Python 3.6.1 ドキュメント

また、stackoverflowでエンコーディングとして指定しているunicode_escapeの情報は以下です。
7.2. codecs — codec レジストリと基底クラス — Python 3.6.1 ドキュメント

 
codecsモジュールを使って変換してみたところ、

import codecs
decoder_func = codecs.getdecoder('unicode_escape')
print(decoder_func)
#=> <built-in function unicode_escape_decode>

after_codecs_tuple = decoder_func(handle)
print(after_codecs_tuple)
#=> ('"ã\x81\x82"', 14)

after_codecs = after_codecs_tuple[0]
print(after_codecs)
#=> "ã"
print(type(after_codecs))
#=> <class 'str'>

文字化けした文字が返ってきました。

上記で見た通り、BottleではデータをLatin-1でデコードしていました。

 
そのため、latin1でエンコードしてバイト文字列化後、utf-8でデコードして文字列にしてみたところ、

encoded = after_codecs.encode('latin1')
print(encoded)
#=> b'"\xe3\x81\x82"'

decoded = encoded.decode('utf-8')
print(decoded)
#=> "あ"

# 前後のダブルクォートが不要なのでstripする
stripped = decoded.strip('"')
print(stripped)
#=> あ

assert stripped == 'あ'

Cookieの値であるを取得でき、テストをパスしました。

 

ast.literal_eval()を使う例

同じように ast.literal_eval()を使ってみます。
ast.literal_eval() | 32.2. ast — 抽象構文木 — Python 3.6.1 ドキュメント

なお、stackoverflowのコメントでは

literal_eval requires a valid string literal, including begin/end quotes.

Fred Nurk Jul 29 ‘11 at 2:29

http://stackoverflow.com/questions/6867588/how-to-convert-escaped-characters-in-python#comment8170329_6867896

とのことですが、今回のCookieは上記で見た通り、

handle = actual.cookies['handle']
print(handle)
#=> "\343\201\202"

とダブルクォートがついていましたので、そのまま使ってみます。

import ast
after_literal_eval = ast.literal_eval(handle)
print(type(after_literal_eval))
#=> <class 'str'>
print(after_literal_eval)
#=> ã

codecsと同じように文字化けした文字が帰ってきました。

 
そのため、同じようにデコード・エンコードしてみます。

encoded = after_literal_eval.encode('latin1')
print(encoded)
#=> b'\xe3\x81\x82'

decoded = encoded.decode('utf-8')
print(decoded)
#=> あ

assert decoded == 'あ'

こちらも、Cookieの値であるを取得でき、テストをパスしました。

 
以上より、codecs, astのどちらもでエスケープシーケンスのリテラルを、文字列として取得できました。

 

その他

日本語リテラルをエスケープシーケンスで表した時の動作

Cookie値でも使った\343\201\202(Unicode文字「あ」)を、リテラルのエスケープシーケンス(8進数3桁表記)として試してみました。

escaped_literal = '\343\201\202'
print(escaped_literal)
#=> ã

 
Unicode文字「あ」の「Octal Escape Sequence」を与えましたが、「あ」ではなく、文字化けした値が得られました。
あ | hiragana letter a (U+3042) @ Graphemica

 
そこで、以下を参考にlatin1でdecode() & utf-8でencode()してみます。

 

escaped_literal = '\343\201\202'

encoded = escaped_literal.encode('latin1')
print(encoded)
#=> b'\xe3\x81\x82'

decoded = encoded.decode('utf-8')
print(decoded)
#=> あ

「あ」を得られました。

 
なお、他のUnicode文字の8進数表記を知りたい時は、以下のツールが便利でした。
UTF8エンコードをデコードする

 

ソースコード

GitHubに上げました。e.g._escaped_literal_to_strディレクトリ以下が今回のサンプルファイルです。
thinkAmi-sandbox/python_misc_samples

*1:Bottle側で変換する方法があるかもしれませんが、今回は置いておきます