Przetwarzaj sekwencje ucieczki w ciągu w Pythonie


112

Czasami, gdy otrzymuję dane wejściowe z pliku lub użytkownika, otrzymuję ciąg znaków z sekwencjami ucieczki. Chciałbym przetwarzać sekwencje specjalne w taki sam sposób, w jaki Python przetwarza sekwencje specjalne w literałach łańcuchowych .

Na przykład, powiedzmy, że myStringjest zdefiniowany jako:

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

Chcę funkcji (tak ją nazywam process), która robi to:

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

Ważne jest, aby funkcja mogła przetwarzać wszystkie sekwencje specjalne w Pythonie (wymienione w tabeli w powyższym linku).

Czy Python ma do tego funkcję?


1
hmmm, jak dokładnie można oczekiwać, że 'spam'+"eggs"+'''some'''+"""more"""zostanie przetworzony ciąg zawierający ?
Nas Banov

@Nas Banov To dobry test. Ten ciąg nie zawiera sekwencji ucieczki, więc po przetworzeniu powinien być dokładnie taki sam. myString = "'spam'+\"eggs\"+'''some'''+\"\"\"more\"\"\"", print(bytes(myString, "utf-8").decode("unicode_escape"))Wydaje się działać.
dln385

5
Większość odpowiedzi na to pytanie zawiera poważne problemy. Wydaje się, że nie ma standardowego sposobu honorowania sekwencji ucieczki w Pythonie bez łamania kodu Unicode. Odpowiedź opublikowana przez @rspeer jest tą, którą zaadoptowałem dla Grako, ponieważ do tej pory obsługuje ona wszystkie znane przypadki.
Apalala

Odpowiedzi:


138

Właściwą rzeczą do zrobienia jest użycie kodu „ucieczki ciągu” do zdekodowania łańcucha.

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

Nie używaj AST ani eval. Korzystanie z kodeków tekstowych jest znacznie bezpieczniejsze.


3
bez dwóch zdań, najlepsze rozwiązanie! btw, według dokumentów powinno to być „string_escape” (z podkreśleniem), ale z jakiegoś powodu akceptuje wszystko we wzorcu „string escape”, „string @ escape” i tak dalej ... w zasadzie'string\W+escape'
Nas Banov

2
@Nas Banov Dokumentacja zawiera małą wzmiankę o tym :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
To rozwiązanie nie jest wystarczająco dobre, ponieważ nie obsługuje przypadku, w którym w oryginalnym ciągu znajdują się prawidłowe znaki Unicode. Jeśli spróbujesz: >>> print("juancarlo\\tañez".encode('utf-8').decode('unicode_escape')) Otrzymasz: juancarlo añez
Apalala

2
Zgadzam się z @Apalala: to nie wystarczy. Sprawdź odpowiedź Rseepera poniżej, aby uzyskać kompletne rozwiązanie, które działa w Python2 i 3!
Christian Aichinger

2
Ponieważ latin1jest zakładane przez unicode_escape, powtórz bit kodowania / dekodowania, np.s.encode('utf-8').decode('unicode_escape').encode('latin1').decode('utf8')
metatoaster

121

unicode_escape ogólnie nie działa

Okazuje się, że rozwiązanie string_escapelub unicode_escapeogólnie nie działa - w szczególności nie działa w obecności rzeczywistego Unicode.

Jeśli możesz być pewien, że każdy znak spoza ASCII zostanie usunięty (i pamiętaj, że wszystko poza pierwszymi 128 znakami nie jest ASCII), unicode_escapezrobi to dobrze. Ale jeśli w twoim ciągu znajdują się już jakieś dosłowne znaki spoza ASCII, coś pójdzie nie tak.

unicode_escapejest zasadniczo zaprojektowany do konwersji bajtów na tekst Unicode. Ale w wielu miejscach - na przykład w kodzie źródłowym Pythona - dane źródłowe są już tekstem Unicode.

Jedynym sposobem, w jaki może to działać poprawnie, jest zakodowanie tekstu w bajtach. UTF-8 to rozsądne kodowanie całego tekstu, więc to powinno działać, prawda?

Poniższe przykłady są w Pythonie 3, więc literały ciągów są czystsze, ale ten sam problem występuje z nieco innymi manifestacjami w obu Pythonie 2 i 3.

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

Cóż, to źle.

Nowym zalecanym sposobem używania kodeków, które dekodują tekst na tekst, jest codecs.decodebezpośrednie wywołanie . To pomaga?

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

Ani trochę. (Powyższe to również błąd UnicodeError w Pythonie 2.)

unicode_escapeKodek, pomimo swojej nazwy, okazuje się założyć, że wszystkie bajty są non-ASCII w kodowaniu Latin-1 (ISO-8859-1). Więc musiałbyś to zrobić w ten sposób:

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

Ale to straszne. To ogranicza cię do 256 znaków Latin-1, tak jakby Unicode nigdy nie został wynaleziony!

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

Dodanie wyrażenia regularnego w celu rozwiązania problemu

(Co zaskakujące, nie mamy teraz dwóch problemów.)

To, co musimy zrobić, to zastosować unicode_escapedekoder tylko do rzeczy, które na pewno są tekstem ASCII. W szczególności możemy upewnić się, że zastosujemy go tylko do prawidłowych sekwencji ucieczki Pythona, które na pewno będą tekstem ASCII.

Plan jest taki, że znajdziemy sekwencje specjalne przy użyciu wyrażenia regularnego i użyjemy funkcji jako argumentu, re.subaby zastąpić je wartością bez zmiany znaczenia.

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)

I z tym:

>>> print(decode_escapes('Ernő \\t Rubik'))
Ernő     Rubik

2
potrzebujemy więcej obejmujących typów odpowiedzi. dzięki.
v.oddou,

Czy to os.sepw ogóle działa ? Próbuję to zrobić: patt = '^' + self.prefix + os.sep ; name = sub(decode_escapes(patt), '', name)i to nie działa. W miejscu nowej linii znajduje się średnik.
Pureferret

@Pureferret Nie jestem pewien, o co pytasz, ale prawdopodobnie nie powinieneś uruchamiać tego na łańcuchach, w których ukośnik odwrotny ma inne znaczenie, na przykład ścieżki plików systemu Windows. (Czy to os.sepjest twoje ?) Jeśli masz sekwencje specjalne z odwrotnym ukośnikiem w nazwach katalogów Windows, sytuacja jest prawie nie do naprawienia.
rspeer

Sekwencja ucieczki nie ma w sobie znaków ucieczki, ale otrzymuję błąd „fałszywy ciąg ucieczki”
Pureferret,

To mówi mi, że zakończyłeś inne wyrażenie regularne odwrotnym ukośnikiem: stackoverflow.com/questions/4427174/ ...
rspeer

33

Właściwie poprawna i wygodna odpowiedź dla Pythona 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

Szczegóły dotyczące codecs.escape_decode:

  • codecs.escape_decode jest dekoderem bajtów do bajtów
  • codecs.escape_decodedekoduje sekwencje specjalne ascii, takie jak: b"\\n"-> b"\n", b"\\xce"-> b"\xce".
  • codecs.escape_decode nie przejmuje się ani nie musi wiedzieć o kodowaniu obiektu bajtowego, ale kodowanie bajtów ze ucieczką powinno być zgodne z kodowaniem reszty obiektu.

Tło:

  • @rspeer jest poprawne: unicode_escapeto nieprawidłowe rozwiązanie dla python3. Dzieje się tak, ponieważ unicode_escapedekoduje bajty ze ucieczką, a następnie dekoduje bajty na łańcuch znaków Unicode, ale nie otrzymuje żadnych informacji dotyczących kodeka do użycia w drugiej operacji.
  • @Jerub ma rację: unikaj AST lub eval.
  • Po raz pierwszy dowiedziałem się codecs.escape_decodez tej odpowiedzi "jak mogę .decode ('string-escape') w Python3?" . Zgodnie z tą odpowiedzią funkcja ta nie jest obecnie udokumentowana dla Pythona 3.

Oto prawdziwa odpowiedź (: Szkoda, że ​​polega na słabo udokumentowanej funkcji.
jwd

5
To jest odpowiedź na sytuacje, w których sekwencje ucieczki, które masz, są sekwencjami ucieczki \xbajtów UTF-8. Ale ponieważ dekoduje bajty na bajty, nie dekoduje - i nie może - dekodować żadnych znaków ucieczki znaków Unicode spoza ASCII, takich jak znaki specjalne \u.
rspeer

Tylko do Twojej wiadomości, ta funkcja nie jest technicznie publiczna. zobacz bugs.python.org/issue30588
Hack5

8

ast.literal_evalFunkcja jest blisko, ale będzie oczekiwać, że łańcuch jest prawidłowo cytowany pierwszy.

Oczywiście interpretacja znaków ucieczki z ukośnikiem odwrotnym w Pythonie zależy od tego, w jaki sposób ciąg jest cytowany ( ""vs r""vs u"", potrójne cudzysłowy itp.), Więc możesz chcieć zawinąć dane wejściowe użytkownika w odpowiednie cudzysłowy i przekazać do literal_eval. Zawinięcie go w cudzysłów zapobiegnie również literal_evalzwróceniu liczby, krotki, słownika itp.

Sprawy mogą się jeszcze skomplikować, jeśli użytkownik wpisze niecytowane cudzysłowy typu, który zamierzasz zawijać wokół ciągu.


Widzę. Wydaje się to potencjalnie niebezpieczne, jak mówisz:, wydaje się myString = "\"\ndoBadStuff()\n\"", print(ast.literal_eval('"' + myString + '"'))że próbuje uruchomić kod. Czym jest ast.literal_evalcoś innego / bezpieczniejszego niż eval?
dln385

5
@ dln385: literal_evalnigdy nie wykonuje kodu. W dokumentacji: „Można to wykorzystać do bezpiecznego oceniania ciągów znaków zawierających wyrażenia Pythona z niezaufanych źródeł bez konieczności samodzielnego analizowania wartości”.
Greg Hewgill

2

Jest to zły sposób, ale zadziałał, gdy próbowałem zinterpretować znaki ósemkowe ze zmianą znaczenia przekazane w argumencie łańcuchowym.

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

Warto wspomnieć, że istnieje różnica między eval i ast.literal_eval (eval jest znacznie bardziej niebezpieczny). Zobacz Używanie metody eval () w Pythonie vs. ast.literal_eval ()?


0

Poniższy kod powinien działać dla \ n musi być wyświetlany w ciągu znaków.

import string

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

1
To nie działa tak, jak napisano (ukośniki powodują, że replacenic nie robią), używa dziko przestarzałych interfejsów API ( stringfunkcje modułu tego rodzaju są przestarzałe od Pythona 2.0, zastąpione strmetodami i zniknęły całkowicie w Pythonie 3) i tylko obsługuje konkretny przypadek zastępowania pojedynczego znaku nowej linii, a nie ogólnego przetwarzania zmiany znaczenia.
ShadowRanger
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.