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

dln385
źródło
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.

Jerub
źródło
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
rspeer
źródło
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.
user19087
źródło
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.

Greg Hewgill
źródło
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 ()?

LimeTr33
źródło
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)
Vignesh Ramsubbose
źródło
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