Обработка управляющих последовательностей в строке в Python

112

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

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

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

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

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

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

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

dln385
источник
1
хммм, как именно вы ожидаете, что строка, содержащаяся, 'spam'+"eggs"+'''some'''+"""more"""будет обработана?
Нас Банов
@ Нас Банов. Хороший тест. Эта строка не содержит управляющих последовательностей, поэтому после обработки она должна быть точно такой же. myString = "'spam'+\"eggs\"+'''some'''+\"\"\"more\"\"\"", print(bytes(myString, "utf-8").decode("unicode_escape"))вроде работает.
dln385
5
Большинство ответов на этот вопрос содержат серьезные проблемы. Кажется, что нет стандартного способа соблюдать escape-последовательности в Python, не нарушая unicode. Ответ, опубликованный @rspeer, - это тот, который я принял для Grako, поскольку он пока обрабатывает все известные случаи.
Apalala

Ответы:

139

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

Джеруб
источник
3
руки вниз, лучшее решение! кстати, по документам это должно быть "string_escape" (с подчеркиванием), но по какой-то причине принимает что-либо в шаблоне 'string escape', 'string @ escape' и еще много чего ... в основном'string\W+escape'
Nas Banov
2
@Nas Banov В документации есть небольшое упоминание об этом :Notice that spelling alternatives that only differ in case or use a hyphen instead of an underscore are also valid aliases; therefore, e.g. 'utf-8' is a valid alias for the 'utf_8' codec.
dln385,
30
Это решение недостаточно хорошее, потому что оно не обрабатывает случай, когда в исходной строке есть допустимые символы Unicode. Если вы попробуете: >>> print("juancarlo\\tañez".encode('utf-8').decode('unicode_escape')) Вы получите: juancarlo añez
Апалала
2
Согласен с @Apalala: этого недостаточно. Ознакомьтесь с ответом rseeper ниже, чтобы узнать о полном решении, которое работает на Python2 и 3!
Christian Aichinger
2
Поскольку latin1предполагается unicode_escape, повторить бит кодирования / декодирования, напримерs.encode('utf-8').decode('unicode_escape').encode('latin1').decode('utf8')
metatoaster
121

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
rspeer
источник
2
нам нужны более объемные ответы, подобные этому. Спасибо.
v.oddou
Это os.sepвообще работает? Я пытаюсь это сделать: patt = '^' + self.prefix + os.sep ; name = sub(decode_escapes(patt), '', name)а не получается. Вместо новой строки стоит точка с запятой.
Pureferret
@Pureferret Я не совсем уверен, о чем вы спрашиваете, но вам, вероятно, не следует запускать это в строках, где обратная косая черта имеет другое значение, например пути к файлам Windows. (Это то, что у вас os.sepесть?) Если у вас есть escape-последовательности с обратной косой чертой в именах каталогов Windows, ситуацию практически невозможно исправить.
rspeer
В escape-последовательности нет escape-последовательностей, но я получаю ошибку «фиктивная escape-строка»
Pureferret
Это говорит мне, что вы завершили какое-то другое регулярное выражение обратной косой чертой: stackoverflow.com/questions/4427174/…
rspeer
33

Фактически правильный и удобный ответ для 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.
user19087
источник
Это настоящий ответ (: жаль, что он полагается на плохо документированную функцию.
jwd
5
Это ответ для ситуаций, когда у вас есть escape-последовательности, которые являются escape-последовательностями \xбайтов UTF-8. Но поскольку он декодирует байты в байты, он не может - и не может - декодировать любые escape-символы Unicode, отличные от ASCII, такие как \uescape -символы .
rspeer
К вашему сведению, эта функция технически не является публичной. см. bugs.python.org/issue30588
Hack5
8

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

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

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

Грег Хьюгилл
источник
Понимаю. Это , как представляется потенциально опасным , как вы говорите: myString = "\"\ndoBadStuff()\n\"", print(ast.literal_eval('"' + myString + '"'))кажется, пытаются кода выполнения. Чем ast.literal_evalотличается / безопаснее чем eval?
dln385
5
@ dln385: literal_evalникогда не выполняет код. Из документации: «Это можно использовать для безопасной оценки строк, содержащих выражения Python, из ненадежных источников без необходимости самостоятельно разбирать значения».
Грег Хьюгилл,
2

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

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

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

LimeTr33
источник
0

Код ниже должен работать, если \ n требуется для отображения в строке.

import string

our_str = 'The String is \\n, \\n and \\n!'
new_str = string.replace(our_str, '/\\n', '/\n', 1)
print(new_str)
Виньеш Рамсуббозе
источник
1
Это не работает так, как написано (косая черта replaceничего не делает), использует сильно устаревшие API ( stringфункции модуля такого рода устарели в Python 2.0, заменены strметодами и полностью исчезли в Python 3), и только обрабатывает конкретный случай замены одной новой строки, а не общую обработку escape.
ShadowRanger