Обработка escape-последовательностей в строке в Python

Иногда, когда я получаю данные из файла или от пользователя, я получаю строку с escape-последовательностями. Я хотел бы обрабатывать escape-последовательности так же, как Python обрабатывает escape-последовательности в строковых литералах.

Например, скажем myString определяется как:

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

Я хочу функцию (я назову это process) это делает это:

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

Важно, чтобы функция могла обрабатывать все escape-последовательности в Python (перечислены в таблице по ссылке выше).

Есть ли в Python функция для этого?

10 ответов

Решение

Правильнее всего использовать код 'string-escape' для декодирования строки.

>>> 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_escape или же unicode_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

Не за что. (Также выше приведено UnicodeError на Python 2.)

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. В частности, мы можем убедиться, что применяем его только к допустимым escape-последовательностям Python, которые гарантированно будут ASCII-текстом.

План такой: мы найдем escape-последовательности с использованием регулярного выражения и используем функцию в качестве аргумента для 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 декодирует escape-последовательности ascii, такие как: b"\\n" -> b"\n", b"\\xce" -> b"\xce",
  • codecs.escape_decode не заботится или не должен знать о кодировке байтового объекта, но кодировка экранированных байтов должна соответствовать кодировке остальной части объекта.

Фон:

  • @rspeer правильно: unicode_escape неверное решение для python3. Это потому что unicode_escape декодирует экранированные байты, затем декодирует байты в строку Unicode, но не получает информации о том, какой кодек использовать для второй операции.
  • @Jerub правильно: избегайте AST или eval.
  • Я впервые обнаружил codecs.escape_decode из этого ответа на вопрос "как я могу.decode('string-escape') в Python3?", Как говорится в этом ответе, эта функция в настоящее время не документирована для Python 3.

ast.literal_eval Функция подходит близко, но она будет ожидать, что строка будет правильно заключена в кавычки.

Конечно, интерпретация Python экранирования от обратной косой черты зависит от того, как строка заключена в кавычки ("" против r"" против u"", тройные кавычки и т. д.), поэтому вы можете заключить пользовательский ввод в подходящие кавычки и перейти к literal_eval, Завершение в кавычки также предотвратит literal_eval от возврата числа, кортежа, словаря и т. д.

Вещи все еще могут быть хитрыми, если пользователь вводит кавычки без кавычек того типа, который вы собираетесь обернуть вокруг строки.

(В настоящее время) принятый ответ Jerub верен для python2, но неверен и может давать искаженные результаты (как указывает Апалала в комментарии к этому решению) для python3. Это потому, что кодек unicode_escape требует, чтобы его источник был закодирован в latin-1, а не в utf-8, согласно официальной документации python . Следовательно, в 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 за обнаружение ошибки в этом решении).

Это плохой способ сделать это, но он работал для меня, когда я пытался интерпретировать экранированные восьмеричные аргументы, передаваемые в строковом аргументе.

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

Стоит отметить, что есть разница между eval и ast.literal_eval (eval намного более небезопасен). Смотрите Использование python eval() против ast.literal_eval()?

Ответ rspeer правильно указывает на то, что unicode-escape включает в себя неявное декодирование с использованием latin-1, но не доводит до конца. Если unicode-escape правильно декодирует экранирование, но неправильно обрабатывает необработанные байты не ASCII, декодируя их как latin-1тогда прямое исправление состоит не в том, чтобы включить регулярное выражение, а в том, чтобы перекодировать его как latin-1 затем (чтобы отменить ошибочную часть процесса), затем декодировать в правильной кодировке. Например, пример неправильного использования:

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

можно сделать тривиально правильно, добавив .encode('latin-1').decode('utf-8'), делая это:

>>> s = 'naïve \\t test'
>>> print(s.encode('utf-8').decode('unicode_escape').encode('latin-1').decode('utf-8'))
naïve    test
# Or using codecs.decode to replace the first encode/decode pair with a single text->text transform:
>>> print(codecs.decode(s, 'unicode_escape').encode('latin-1').decode('utf-8'))
naïve    test

Конечно, это много взад-вперёд, и я бы не хотел вставлять это в свой код, но это можно отнести к отдельной функции, которая работает для обоих str а также bytes (с необязательным шагом декодирования для bytes если результат находится в известной кодировке):

def decode_escapes(s, encoding=None):
    if isinstance(s, str):
        if encoding is not None:
            return TypeError("Do not pass encoding for string arguments")
        # UTF-8 will allow correct interpretation of escapes when bytes form
        # interpreted as latin-1
        s = s.encode('utf-8')
        encoding = 'utf-8'
    decoded = s.decode('unicode_escape').encode('latin-1')
    if encoding is not None:
        # If encoding is provided, or we started with an arbitrary string, decode
        decoded = decode.decode(encoding)
    return decoded

Правильно заключайте строку в кавычки, чтобы она выглядела как эквивалентный строковый литерал Python, а затем используйтеast.literal_eval. Это безопасно , но сделать это гораздо сложнее, чем вы могли бы ожидать.

Достаточно просто добавить в начало и конец строки, но нам также нужно убедиться, что любой"внутри строки правильно экранированы. Если нам нужен полностью совместимый с Python перевод, нам нужно учитывать устаревшее поведение недопустимых escape-последовательностей.

Получается, что нам нужно добавить одну обратную косую черту к:

  • любая последовательность из четного числа обратных косых черт, за которыми следует двойная кавычка (чтобы мы экранировали кавычку, если это необходимо, но не экранировали обратную косую черту и не экранировали кавычку, если она уже была экранирована); а также

  • последовательность нечетного числа обратных косых черт в конце входных данных (поскольку в противном случае обратная косая черта ускользнула бы от нашей прилагаемой двойной кавычки).

Вот входной тест, показывающий кучу сложных случаев:

      >>> 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)

Если вы доверяете источнику данных, просто заключите в кавычки и eval() это?

>>> myString = 'spam\\neggs'
>>> print eval('"' + myString.replace('"','') + '"')
spam
eggs

PS. добавлена ​​контр-мера evil-code-exec - теперь она удалит все " перед оценкой

Другие вопросы по тегам