Procesar secuencias de escape en una cadena en Python

112

A veces, cuando obtengo información de un archivo o del usuario, obtengo una cadena con secuencias de escape. Me gustaría procesar las secuencias de escape de la misma manera que Python procesa las secuencias de escape en cadenas literales .

Por ejemplo, digamos que myStringse define como:

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

Quiero una función (la llamaré process) que haga esto:

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

Es importante que la función pueda procesar todas las secuencias de escape en Python (enumeradas en una tabla en el enlace de arriba).

¿Python tiene una función para hacer esto?

dln385
fuente
1
hmmm, ¿cómo esperaría exactamente que se 'spam'+"eggs"+'''some'''+"""more"""procesara una cadena que contiene ?
Nas Banov
@Nas Banov Esa es una buena prueba. Esa cadena no contiene secuencias de escape, por lo que debería ser exactamente la misma después del procesamiento. myString = "'spam'+\"eggs\"+'''some'''+\"\"\"more\"\"\"", print(bytes(myString, "utf-8").decode("unicode_escape"))parece funcionar.
dln385
5
La mayoría de las respuestas a esta pregunta tienen serios problemas. Parece que no hay una forma estándar de respetar las secuencias de escape en Python sin romper unicode. La respuesta publicada por @rspeer es la que adopté para Grako, ya que hasta ahora maneja todos los casos conocidos.
Apalala

Respuestas:

138

Lo correcto es usar el código 'string-escape' para decodificar la cadena.

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

No use AST o eval. Usar los códecs de cadena es mucho más seguro.

Jerub
fuente
3
sin duda, la mejor solución! por cierto, por documentos debería ser "string_escape" (con guión bajo) pero por alguna razón acepta cualquier cosa en el patrón 'string escape', 'string @ escape "y todo eso ... básicamente'string\W+escape'
Nas Banov
2
@Nas Banov La documentación hace una pequeña mención sobre eso :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
Esta solución no es lo suficientemente buena porque no maneja el caso en el que hay caracteres Unicode legítimos en la cadena original. Si lo intentas: >>> print("juancarlo\\tañez".encode('utf-8').decode('unicode_escape')) obtienes: juancarlo añez
Apalala
2
De acuerdo con @Apalala: esto no es suficiente. ¡Consulte la respuesta de rseeper a continuación para obtener una solución completa que funciona en Python2 y 3!
Christian Aichinger
2
Dado que latin1es asumido por unicode_escape, s.encode('utf-8').decode('unicode_escape').encode('latin1').decode('utf8')
rehaga
121

unicode_escape no funciona en general

Resulta que la solución string_escapeo unicode_escapeno funciona en general; en particular, no funciona en presencia de Unicode real.

Si puede estar seguro de que todos los caracteres que no sean ASCII se escaparán (y recuerde, cualquier cosa más allá de los primeros 128 caracteres no es ASCII), unicode_escapehará lo correcto por usted. Pero si ya hay caracteres literales no ASCII en su cadena, las cosas saldrán mal.

unicode_escapeestá diseñado fundamentalmente para convertir bytes en texto Unicode. Pero en muchos lugares, por ejemplo, el código fuente de Python, los datos de origen ya son texto Unicode.

La única forma en que esto puede funcionar correctamente es si primero codifica el texto en bytes. UTF-8 es la codificación sensata para todo el texto, así que debería funcionar, ¿verdad?

Los siguientes ejemplos están en Python 3, por lo que los literales de cadena son más limpios, pero existe el mismo problema con manifestaciones ligeramente diferentes en Python 2 y 3.

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

Bueno, eso está mal.

La nueva forma recomendada de utilizar códecs que decodifican texto en texto es llamar codecs.decodedirectamente. ¿Eso ayuda?

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

De ningún modo. (Además, lo anterior es un UnicodeError en Python 2.)

El unicode_escapecódec, a pesar de su nombre, asume que todos los bytes que no son ASCII están en la codificación Latin-1 (ISO-8859-1). Entonces tendrías que hacerlo así:

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

Pero eso es terrible. Esto lo limita a los 256 caracteres Latin-1, ¡como si Unicode nunca se hubiera inventado!

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

Agregar una expresión regular para resolver el problema

(Sorprendentemente, ahora no tenemos dos problemas).

Lo que tenemos que hacer es aplicar el unicode_escapedecodificador solo a cosas que estamos seguros de que serán texto ASCII. En particular, podemos asegurarnos de aplicarlo solo a secuencias de escape de Python válidas, que están garantizadas como texto ASCII.

El plan es que encontraremos secuencias de escape usando una expresión regular y usaremos una función como argumento re.subpara reemplazarlas con su valor sin escape.

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)

Y con eso:

>>> print(decode_escapes('Ernő \\t Rubik'))
Ernő     Rubik
rspeer
fuente
2
necesitamos tipos de respuestas más amplias como esa. Gracias.
v.oddou
¿Esto funciona os.sepen absoluto? Estoy tratando de hacer esto: patt = '^' + self.prefix + os.sep ; name = sub(decode_escapes(patt), '', name)y no funciona. El punto y coma está allí en lugar de una nueva línea.
Pureferret
@Pureferret No estoy realmente seguro de lo que está preguntando, pero probablemente no debería ejecutar esto en cadenas donde la barra invertida tiene un significado diferente, como las rutas de archivos de Windows. (¿Eso es lo que os.sepes tuyo ?). Si tienes secuencias de escape con barra invertida en los nombres de los directorios de Windows, la situación es prácticamente irrecuperable.
rspeer
La secuencia de escape no tiene escapes, pero recibo un error de 'cadena de escape falsa'
Pureferret
Eso me dice que terminó alguna otra expresión regular con una barra invertida: stackoverflow.com/questions/4427174/…
rspeer
33

La respuesta realmente correcta y conveniente para 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

Detalles sobre codecs.escape_decode:

  • codecs.escape_decode es un decodificador de bytes a bytes
  • codecs.escape_decodedecodifica secuencias de escape ascii, como: b"\\n"-> b"\n", b"\\xce"-> b"\xce".
  • codecs.escape_decode no le importa ni necesita saber acerca de la codificación del objeto byte, pero la codificación de los bytes escapados debe coincidir con la codificación del resto del objeto.

Antecedentes:

  • @rspeer es correcto: unicode_escapees la solución incorrecta para python3. Esto se debe a que unicode_escapedecodifica los bytes de escape, luego decodifica los bytes en una cadena Unicode, pero no recibe información sobre qué códec usar para la segunda operación.
  • @Jerub es correcto: evite el AST o eval.
  • Descubrí por primera vez a codecs.escape_decodepartir de esta respuesta a "¿cómo puedo .decode ('string-escape') en Python3?" . Como dice esa respuesta, esa función actualmente no está documentada para Python 3.
user19087
fuente
Esta es la respuesta real (: Lástima que se base en una función mal documentada.
jwd
5
Esta es la respuesta para situaciones en las que las secuencias de escape que tiene son \xescapes de bytes UTF-8. Pero debido a que decodifica bytes a bytes, no decodifica, y no puede, los escapes de caracteres Unicode que no son ASCII, como los \uescapes.
rspeer
Solo un FYI, esta función técnicamente no es pública. ver bugs.python.org/issue30588
Hack5
8

La ast.literal_evalfunción se acerca, pero espera que la cadena se cite correctamente primero.

Por supuesto, la interpretación de Python de los escapes de barra invertida depende de cómo se cite la cadena ( ""vs r""vs u"", comillas triples, etc.) por lo que es posible que desee envolver la entrada del usuario entre comillas adecuadas y pasar a literal_eval. Envolverlo entre comillas también evitará que se literal_evaldevuelva un número, tupla, diccionario, etc.

Las cosas aún pueden complicarse si el usuario escribe comillas sin comillas del tipo que pretende envolver alrededor de la cadena.

Greg Hewgill
fuente
Veo. Esto parece ser potencialmente peligroso como usted dice: myString = "\"\ndoBadStuff()\n\"", print(ast.literal_eval('"' + myString + '"'))parece intentar ejecutar código. ¿Cómo es ast.literal_evaldiferente / más seguro que eval?
dln385
5
@ dln385: literal_evalnunca ejecuta código. De la documentación, "Esto se puede utilizar para evaluar de forma segura cadenas que contienen expresiones de Python de fuentes no confiables sin la necesidad de analizar los valores por sí mismo".
Greg Hewgill
2

Esta es una mala forma de hacerlo, pero funcionó para mí cuando intentaba interpretar octals escapados pasados ​​en un argumento de cadena.

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

Vale la pena mencionar que hay una diferencia entre eval y ast.literal_eval (eval es mucho más inseguro). Ver ¿ Usar eval () de python frente a ast.literal_eval ()?

LimaTr33
fuente
0

El siguiente código debería funcionar para \ n es necesario que se muestre en la cadena.

import string

our_str = 'The String is \\n, \\n and \\n!'
new_str = string.replace(our_str, '/\\n', '/\n', 1)
print(new_str)
Vignesh Ramsubbose
fuente
1
Esto no funciona como está escrito (las barras inclinadas hacen replaceque no se haga nada), utiliza API muy desactualizadas (las stringfunciones del módulo de este tipo están obsoletas a partir de Python 2.0, reemplazadas por los strmétodos y desaparecieron por completo en Python 3), y solo maneja el caso específico de reemplazar una sola línea nueva, no el procesamiento de escape general.
ShadowRanger