簡體   English   中英

處理Python中字符串中的轉義序列

[英]Process escape sequences in a string in Python

有時,當我從文件或用戶獲得輸入時,我會得到一個帶有轉義序列的字符串。 我想以與 Python 處理字符串文字中的轉義序列相同的方式處理轉義序列

例如,假設myString定義為:

>>> myString = "spam\\neggs"
>>> print(myString)
spam\neggs

我想要一個 function(我稱之為process )來執行此操作:

>>> print(process(myString))
spam
eggs

重要的是 function 可以處理 Python 中的所有轉義序列(在上面鏈接的表格中列出)。

Python 是否有 function 來執行此操作?

正確的做法是使用“字符串轉義”代碼來解碼字符串。

>>> myString = "spam\\neggs"
>>> decoded_string = bytes(myString, "utf-8").decode("unicode_escape") # python3 
>>> decoded_string = myString.decode('string_escape') # python2
>>> print(decoded_string)
spam
eggs

不要使用 AST 或 eval。 使用字符串編解碼器要安全得多。

unicode_escape通常不起作用

事實證明, string_escapeunicode_escape解決方案通常不起作用——特別是,它在實際 Unicode 存在的情況下不起作用。

如果您可以確定每個非 ASCII 字符都會被轉義(請記住,前 128 個字符之外的任何字符都是非 ASCII), unicode_escape將為您做正確的事情。 但是如果你的字符串中已經有任何文字的非 ASCII 字符,事情就會出錯。

unicode_escape從根本上設計用於將字節轉換為 Unicode 文本。 但在許多地方——例如 Python 源代碼——源數據已經是 Unicode 文本。

唯一可以正常工作的方法是先將文本編碼為字節。 UTF-8 是所有文本的合理編碼,所以應該可以,對吧?

以下示例在 Python 3 中,因此字符串文字更清晰,但同樣的問題存在於 Python 2 和 3 中的表現形式略有不同。

>>> s = 'naïve \\t test'
>>> print(s.encode('utf-8').decode('unicode_escape'))
naïve   test

嗯,這是錯誤的。

使用將文本解碼為文本的編解碼器的新推薦方法是直接調用codecs.decode 這有幫助嗎?

>>> import codecs
>>> print(codecs.decode(s, 'unicode_escape'))
naïve   test

一點也不。 (另外,上面是 Python 2 上的 UnicodeError。)

unicode_escape編解碼器,盡管它的名字,結果是假設所有非 ASCII 字節都在 Latin-1 (ISO-8859-1) 編碼中。 所以你必須這樣做:

>>> print(s.encode('latin-1').decode('unicode_escape'))
naïve    test

但這太可怕了。 這將您限制為 256 個 Latin-1 字符,就好像 Unicode 根本就沒有被發明過一樣!

>>> print('Ernő \\t Rubik'.encode('latin-1').decode('unicode_escape'))
UnicodeEncodeError: 'latin-1' codec can't encode character '\u0151'
in position 3: ordinal not in range(256)

添加正則表達式解決問題

(令人驚訝的是,我們現在沒有兩個問題。)

我們需要做的只是將unicode_escape解碼器應用於我們確定為 ASCII 文本的內容。 特別是,我們可以確保僅將其應用於有效的 Python 轉義序列,這些轉義序列保證為 ASCII 文本。

計划是,我們將使用正則表達式找到轉義序列,並使用函數作為re.sub的參數,以將它們替換為未轉義的值。

import re
import codecs

ESCAPE_SEQUENCE_RE = re.compile(r'''
    ( \\U........      # 8-digit hex escapes
    | \\u....          # 4-digit hex escapes
    | \\x..            # 2-digit hex escapes
    | \\[0-7]{1,3}     # Octal escapes
    | \\N\{[^}]+\}     # Unicode characters by name
    | \\[\\'"abfnrtv]  # Single-character escapes
    )''', re.UNICODE | re.VERBOSE)

def decode_escapes(s):
    def decode_match(match):
        return codecs.decode(match.group(0), 'unicode-escape')

    return ESCAPE_SEQUENCE_RE.sub(decode_match, s)

有了這個:

>>> print(decode_escapes('Ernő \\t Rubik'))
Ernő     Rubik

python 3的實際正確和方便的答案:

>>> import codecs
>>> myString = "spam\\neggs"
>>> print(codecs.escape_decode(bytes(myString, "utf-8"))[0].decode("utf-8"))
spam
eggs
>>> myString = "naïve \\t test"
>>> print(codecs.escape_decode(bytes(myString, "utf-8"))[0].decode("utf-8"))
naïve    test

關於codecs.escape_decode的詳細信息:

  • codecs.escape_decode是一個字節到字節的解碼器
  • codecs.escape_decode解碼 ascii 轉義序列,例如: b"\\\\n" -> b"\\n"b"\\\\xce" -> b"\\xce"
  • codecs.escape_decode不關心或不需要知道字節對象的編碼,但轉義字節的編碼應該與對象其余部分的編碼相匹配。

背景:

  • @rspeer是正確的: unicode_escape是 python3 的錯誤解決方案。 這是因為unicode_escape解碼轉義字節,然后將字節解碼為 un​​icode 字符串,但沒有接收到有關用於第二個操作的編解碼器的信息。
  • @Jerub是正確的:避免使用 AST 或 eval。
  • 我首先從這個答案中發現了codecs.escape_decode “我如何在 Python3 中進行 .decode('string-escape')?” . 正如該答案所述,該函數目前沒有為 python 3 記錄。

ast.literal_eval函數很接近,但它希望字符串首先被正確引用。

當然,Python 對反斜杠轉義的解釋取決於字符串的引用方式( "" vs r"" vs u"" ,三引號等),因此您可能希望將用戶輸入包裝在合適的引號中並傳遞給literal_eval 用引號括起來也可以防止literal_eval返回數字、元組、字典等。

如果用戶鍵入您打算環繞字符串的類型的不帶引號的引號,事情仍然可能會變得棘手。

這是一種不好的方法,但是在嘗試解釋在字符串參數中傳遞的轉義八進制時它對我有用。

input_string = eval('b"' + sys.argv[1] + '"')

值得一提的是 eval 和 ast.literal_eval 之間存在差異(eval 更加不安全)。 請參閱使用 python 的 eval() 與 ast.literal_eval()?

Jerub(當前)接受的答案對於 python2 是正確的,但對於 python3 是不正確的並且可能會產生亂碼結果(正如 Apalala 在對該解決方案的評論中指出的那樣)。 這是因為根據官方python 文檔,unicode_escape 編解碼器要求其源代碼以 latin-1 而非 utf-8 編碼。 因此,在 python3 中使用:

>>> myString="špåm\\nëðþ\\x73"
>>> print(myString)
špåm\nëðþ\x73
>>> decoded_string = myString.encode('latin-1','backslashreplace').decode('unicode_escape')
>>> print(decoded_string)
špåm
ëðþs

此方法還避免了 metatoaster 對 Jerub 解決方案的注釋中字符串和字節之間額外不必要的往返(但要感謝 metatoaster 以識別該解決方案中的錯誤)。

正確引用字符串,使其看起來像等效的 Python 字符串文字,然后使用ast.literal_eval 是安全的,但比您預期的要正確得多。

在字符串的開頭和結尾添加一個"很容易,但我們還需要確保字符串中的任何"都被正確轉義。 如果我們想要完全符合 Python 的翻譯,我們需要考慮無效轉義序列的棄用行為

結果是我們需要添加一個反斜杠:

  • 偶數個反斜杠后跟雙引號的任意序列(以便我們在需要時轉義引號,但不要轉義反斜杠,如果引號已經轉義則取消轉義引號);

  • 輸入末尾的一系列奇數反斜杠(因為否則反斜杠會逃脫我們封閉的雙引號)。

這是一個酸性測試輸入,顯示了一堆困難的情況:

>>> text = r'''\\ \ \" \\" \\\" \'你好'\n\u062a\xff\N{LATIN SMALL LETTER A}"''' + '\\'
>>> text
'\\\\ \\ \\" \\\\" \\\\\\" \\\'你好\'\\n\\u062a\\xff\\N{LATIN SMALL LETTER A}"\\'
>>> print(text)
\\ \ \" \\" \\\" \'你好'\n\u062a\xff\N{LATIN SMALL LETTER A}"\

我最終能夠計算出一個正則表達式來正確處理所有這些情況,允許使用literal_eval

>>> def parse_escapes(text):
...     fixed_escapes = re.sub(r'(?<!\\)(\\\\)*("|\\$)', r'\\\1\2', text)
...     return ast.literal_eval(f'"{fixed_escapes}"')
... 

測試結果:

>>> parse_escapes(text)
'\\ \\ " \\" \\" \'你好\'\nتÿa"\\'
>>> print(parse_escapes(text))
\ \ " \" \" '你好'
تÿa"\

這應該可以正確處理所有內容——包含單引號和雙引號的字符串、每個帶有反斜杠的奇怪情況以及輸入中的非 ASCII 字符。 (我承認用肉眼驗證結果有點困難!)

下面的代碼應該適用於 \\n 需要顯示在字符串上。

import string

our_str = 'The String is \\n, \\n and \\n!'
new_str = string.replace(our_str, '/\\n', '/\n', 1)
print(new_str)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM