Управляющие последовательности процесса в строке в Python

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

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

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

Мне нужна функция (я буду называть ее process), которая делает это:

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

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

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

Ответ 1

Правильная вещь - использовать код "escape-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. Использование строковых кодеков намного безопаснее.

Ответ 2

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

Ответ 3

Фактически правильный и удобный ответ для 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.

Ответ 4

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

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

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

Ответ 5

Ответ rspeer правильно указывает, что unicode-escape подразумевает неявное декодирование с использованием latin-1, но на этом не происходит. Если unicode-escape корректно декодирует 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

Ответ 6

Ниже приведен код, который должен работать для \n, который должен отображаться в строке.

import string

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

Ответ 7

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

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

PS. добавлена ​​противопожарная мера зла-кода-exec - теперь она будет разбивать все " до eval-ing