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 を持つ文字
として使えます。
例えば、「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のCookieはFormsDict
型に入っていて、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
とのことですが、今回の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()してみます。
- character encoding - Python: Converting from ISO-8859-1/latin1 to UTF-8 - Stack Overflow
- Fixing common Unicode mistakes with Python — after they’ve been made – Luminoso Blog
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側で変換する方法があるかもしれませんが、今回は置いておきます