Traiter les séquences d'échappement dans une chaîne en Python

112

Parfois, lorsque j'obtiens une entrée d'un fichier ou de l'utilisateur, j'obtiens une chaîne avec des séquences d'échappement. Je voudrais traiter les séquences d'échappement de la même manière que Python traite les séquences d'échappement dans les littéraux de chaîne .

Par exemple, disons qu'il myStringest défini comme:

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

Je veux une fonction (je l'appellerai process) qui fait ceci:

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

Il est important que la fonction puisse traiter toutes les séquences d'échappement en Python (répertoriées dans un tableau dans le lien ci-dessus).

Python a-t-il une fonction pour faire cela?

dln385
la source
1
hmmm, comment vous attendriez-vous exactement à ce qu'une chaîne contenant 'spam'+"eggs"+'''some'''+"""more"""soit traitée?
Nas Banov
@Nas Banov C'est un bon test. Cette chaîne ne contient aucune séquence d'échappement, elle doit donc être exactement la même après le traitement. myString = "'spam'+\"eggs\"+'''some'''+\"\"\"more\"\"\"", print(bytes(myString, "utf-8").decode("unicode_escape"))semble fonctionner.
dln385
5
La plupart des réponses à cette question posent de sérieux problèmes. Il ne semble y avoir aucun moyen standard d'honorer les séquences d'échappement en Python sans rompre l'unicode. La réponse publiée par @rspeer est celle que j'ai adoptée pour Grako car elle traite jusqu'à présent tous les cas connus.
Apalala

Réponses:

138

La bonne chose à faire est d'utiliser le code 'string-escape' pour décoder la chaîne.

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

N'utilisez pas AST ou eval. L'utilisation des codecs de chaîne est beaucoup plus sûre.

Jerub
la source
3
haut la main, la meilleure solution! btw, par docs, il devrait être "string_escape" (avec un trait de soulignement) mais pour une raison quelconque, accepte tout ce qui est dans le motif "string escape", "string @ escape" et tout le reste ... en gros'string\W+escape'
Nas Banov
2
@Nas Banov La documentation fait une petite mention à ce sujet :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
Cette solution n'est pas suffisante car elle ne gère pas le cas où il y a des caractères Unicode légitimes dans la chaîne d'origine. Si vous essayez: >>> print("juancarlo\\tañez".encode('utf-8').decode('unicode_escape')) Vous obtenez: juancarlo añez
Apalala
2
D'accord avec @Apalala: ce n'est pas suffisant. Consultez la réponse de rseeper ci-dessous pour une solution complète qui fonctionne en Python2 et 3!
Christian Aichinger
2
Puisque latin1est supposé par unicode_escape, refaire le bit d'encodage / décodage, par exemples.encode('utf-8').decode('unicode_escape').encode('latin1').decode('utf8')
metatoaster
121

unicode_escape ne fonctionne pas en général

Il s'avère que la solution string_escapeou unicode_escapene fonctionne pas en général - en particulier, elle ne fonctionne pas en présence d'Unicode réel.

Si vous pouvez être sûr que chaque caractère non ASCII sera échappé (et rappelez-vous que tout ce qui dépasse les 128 premiers caractères n'est pas ASCII), unicode_escapefera ce qu'il faut pour vous. Mais s'il y a déjà des caractères littéraux non ASCII dans votre chaîne, les choses iront mal.

unicode_escapeest fondamentalement conçu pour convertir des octets en texte Unicode. Mais dans de nombreux endroits - par exemple, le code source Python - les données source sont déjà du texte Unicode.

La seule façon dont cela peut fonctionner correctement est de coder d'abord le texte en octets. UTF-8 est le codage sensé pour tout le texte, donc cela devrait fonctionner, non?

Les exemples suivants sont en Python 3, de sorte que les littéraux de chaîne sont plus propres, mais le même problème existe avec des manifestations légèrement différentes sur Python 2 et 3.

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

Eh bien, c'est faux.

La nouvelle façon recommandée d'utiliser les codecs qui décodent du texte en texte est d'appeler codecs.decodedirectement. Est ce que ça aide?

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

Pas du tout. (De plus, ce qui précède est une erreur UnicodeError sur Python 2.)

Le unicode_escapecodec, malgré son nom, s'avère supposer que tous les octets non-ASCII sont dans le codage Latin-1 (ISO-8859-1). Vous devriez donc le faire comme ceci:

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

Mais c'est terrible. Cela vous limite aux 256 caractères Latin-1, comme si l'Unicode n'avait jamais été inventé du tout!

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

Ajout d'une expression régulière pour résoudre le problème

(Étonnamment, nous n'avons pas actuellement deux problèmes.)

Ce que nous devons faire, c'est n'appliquer le unicode_escapedécodeur qu'aux choses dont nous sommes certains d'être du texte ASCII. En particulier, nous pouvons nous assurer de ne l'appliquer qu'aux séquences d'échappement Python valides, qui sont garanties comme du texte ASCII.

Le plan est de trouver des séquences d'échappement à l'aide d'une expression régulière et d'utiliser une fonction comme argument pour re.subles remplacer par leur valeur sans échappement.

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)

Et avec cela:

>>> print(decode_escapes('Ernő \\t Rubik'))
Ernő     Rubik
rsperer
la source
2
nous avons besoin de types de réponses plus globales comme ça. Merci.
v.oddou
Cela fonctionne-t-il avec os.sepdu tout? J'essaye de faire ceci: patt = '^' + self.prefix + os.sep ; name = sub(decode_escapes(patt), '', name)et ça ne marche pas. Le point-virgule est là à la place d'une nouvelle ligne.
Pureferret
@Pureferret Je ne suis pas vraiment sûr de ce que vous demandez, mais vous ne devriez probablement pas l'exécuter sur des chaînes où la barre oblique inverse a une signification différente, comme les chemins de fichiers Windows. (Est-ce ce que vous os.sepêtes?) Si vous avez des séquences d'échappement contre-obliques dans vos noms de répertoire Windows, la situation est pratiquement irrécupérable.
rspeer le
La séquence d'échappement ne contient pas d'échappements, mais
j'obtiens une
Cela me dit que vous avez terminé une autre expression régulière avec une barre oblique inverse: stackoverflow.com/questions/4427174
...
33

La réponse réellement correcte et pratique pour 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

Détails concernant codecs.escape_decode:

  • codecs.escape_decode est un décodeur octets en octets
  • codecs.escape_decodedécode les séquences d'échappement ascii, telles que: b"\\n"-> b"\n", b"\\xce"-> b"\xce".
  • codecs.escape_decode ne se soucie pas ou n'a pas besoin de connaître le codage de l'objet octet, mais le codage des octets échappés doit correspondre au codage du reste de l'objet.

Contexte:

  • @rspeer est correct: unicode_escapeest la solution incorrecte pour python3. Cela est dû au fait que unicode_escapedécode les octets échappés, puis décode les octets en chaîne Unicode, mais ne reçoit aucune information concernant le codec à utiliser pour la deuxième opération.
  • @Jerub est correct: évitez l'AST ou l'évaluation.
  • J'ai découvert pour la première fois à codecs.escape_decodepartir de cette réponse "comment puis-je .decode ('string-escape') en Python3?" . Comme l'indique cette réponse, cette fonction n'est actuellement pas documentée pour python 3.
user19087
la source
C'est la vraie réponse (: Dommage que cela repose sur une fonction mal documentée.
jwd
5
C'est la réponse aux situations où les séquences d'échappement que vous avez sont des \xéchappements d'octets UTF-8. Mais comme il décode des octets en octets, il ne décode pas - et ne peut pas - décoder les échappements de caractères Unicode non ASCII, tels que les \uéchappements.
rspeer
Juste un FYI, cette fonction n'est techniquement pas publique. voir bugs.python.org/issue30588
Hack5
8

La ast.literal_evalfonction se rapproche, mais elle s'attendra à ce que la chaîne soit correctement citée en premier.

Bien sûr, l'interprétation par Python des échappements de barre oblique inverse dépend de la façon dont la chaîne est citée ( ""vs r""vs u"", triples guillemets, etc.), vous pouvez donc placer l'entrée utilisateur entre guillemets appropriés et passer à literal_eval. Le mettre entre guillemets empêchera également literal_evalde renvoyer un nombre, un tuple, un dictionnaire, etc.

Les choses peuvent encore devenir délicates si l'utilisateur tape des guillemets non entre guillemets du type que vous souhaitez enrouler autour de la chaîne.

Greg Hewgill
la source
Je vois. Cela semble être potentiellement dangereux comme vous le dites: myString = "\"\ndoBadStuff()\n\"", print(ast.literal_eval('"' + myString + '"'))semble essayer de le code d'exécution. En quoi est- ast.literal_evalce différent / plus sûr que eval?
dln385
5
@ dln385: literal_evaln'exécute jamais de code. D'après la documentation, "Cela peut être utilisé pour évaluer en toute sécurité des chaînes contenant des expressions Python provenant de sources non fiables sans avoir besoin d'analyser les valeurs soi-même."
Greg Hewgill
2

C'est une mauvaise façon de le faire, mais cela a fonctionné pour moi lorsque j'ai essayé d'interpréter des octals échappés passés dans un argument de chaîne.

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

Il convient de mentionner qu'il existe une différence entre eval et ast.literal_eval (eval étant beaucoup plus dangereux). Voir Utiliser eval () de python contre ast.literal_eval ()?

LimeTr33
la source
0

Le code ci-dessous devrait fonctionner pendant \ n doit être affiché sur la chaîne.

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
la source
1
Cela ne fonctionne pas comme écrit (les barres obliques ne replacefont rien faire), utilise des API extrêmement obsolètes (les stringfonctions de module de ce type sont obsolètes à partir de Python 2.0, remplacées par les strméthodes et disparues complètement dans Python 3), et seulement gère le cas spécifique du remplacement d'une seule nouvelle ligne, pas le traitement d'échappement général.
ShadowRanger