Wie kann ich die ANSI-Escape-Sequenzen aus einer Zeichenfolge in Python entfernen?

72

Das ist meine Zeichenfolge:

'ls\r\n\x1b[00m\x1b[01;31mexamplefile.zip\x1b[00m\r\n\x1b[01;31m'

Ich habe Code verwendet, um die Ausgabe eines SSH-Befehls abzurufen, und ich möchte, dass meine Zeichenfolge nur 'examplefile.zip' enthält.

Was kann ich verwenden, um die zusätzlichen Escape-Sequenzen zu entfernen?

SpartaSixZero
quelle
1
mögliches Duplikat des Herausfilterns von ANSI-Escape-Sequenzen
fuenfundachtzig

Antworten:

140

Löschen Sie sie mit einem regulären Ausdruck:

import re

# 7-bit C1 ANSI sequences
ansi_escape = re.compile(r'''
    \x1B  # ESC
    (?:   # 7-bit C1 Fe (except CSI)
        [@-Z\\-_]
    |     # or [ for CSI, followed by a control sequence
        \[
        [0-?]*  # Parameter bytes
        [ -/]*  # Intermediate bytes
        [@-~]   # Final byte
    )
''', re.VERBOSE)
result = ansi_escape.sub('', sometext)

oder ohne VERBOSEFlagge in komprimierter Form:

ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
result = ansi_escape.sub('', sometext)

Demo:

>>> import re
>>> ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
>>> sometext = 'ls\r\n\x1b[00m\x1b[01;31mexamplefile.zip\x1b[00m\r\n\x1b[01;31m'
>>> ansi_escape.sub('', sometext)
'ls\r\nexamplefile.zip\r\n'

Der obige reguläre Ausdruck deckt alle 7-Bit-ANSI-C1-Escape-Sequenzen ab, nicht jedoch die 8-Bit-C1-Escape-Sequenzöffner. Letztere werden in der heutigen UTF-8-Welt, in der derselbe Bytebereich eine andere Bedeutung hat, niemals verwendet.

Wenn Sie auch die 8-Bit-Codes abdecken müssen (und dann vermutlich mit bytesWerten arbeiten), wird der reguläre Ausdruck zu einem Bytemuster wie dem folgenden:

# 7-bit and 8-bit C1 ANSI sequences
ansi_escape_8bit = re.compile(br'''
    (?: # either 7-bit C1, two bytes, ESC Fe (omitting CSI)
        \x1B
        [@-Z\\-_]
    |   # or a single 8-bit byte Fe (omitting CSI)
        [\x80-\x9A\x9C-\x9F]
    |   # or CSI + control codes
        (?: # 7-bit CSI, ESC [ 
            \x1B\[
        |   # 8-bit CSI, 9B
            \x9B
        )
        [0-?]*  # Parameter bytes
        [ -/]*  # Intermediate bytes
        [@-~]   # Final byte
    )
''', re.VERBOSE)
result = ansi_escape_8bit.sub(b'', somebytesvalue)

die bis zu verdichtet werden kann

# 7-bit and 8-bit C1 ANSI sequences
ansi_escape_8bit = re.compile(
    br'(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])'
)
result = ansi_escape_8bit.sub(b'', somebytesvalue)

Weitere Informationen finden Sie unter:

Das von Ihnen angegebene Beispiel enthält 4 CSI-Codes (Control Sequence Introducer), die durch die \x1B[oder ESC-[ Öffnungsbytes gekennzeichnet sind, und enthält jeweils einen SGR-Code (Select Graphic Rendition), da diese jeweils mit enden m. Die Parameter (durch ;Semikolons getrennt ) dazwischen teilen Ihrem Terminal mit, welche Grafikwiedergabeattribute verwendet werden sollen. Für jede \x1B[....mSequenz werden also drei Codes verwendet:

  • 0 (oder 00in diesem Beispiel): Zurücksetzen , alle Attribute deaktivieren
  • 1 (oder 01im Beispiel): fett
  • 31: rot (Vordergrund)

ANSI bietet jedoch mehr als nur CSI-SGR-Codes. Mit CSI allein können Sie auch den Cursor steuern, Linien oder die gesamte Anzeige löschen oder scrollen (vorausgesetzt, das Terminal unterstützt dies natürlich). Über CSI hinaus gibt es Codes, mit denen Sie alternative Schriftarten ( SS2und SS3) auswählen , "private Nachrichten" senden (Passwörter denken), mit dem Terminal ( DCS), dem Betriebssystem ( OSC) oder der Anwendung selbst kommunizieren können ( APCeine Möglichkeit für Anwendungen) Huckepack-benutzerdefinierte Steuercodes für den Kommunikationsstrom) und weitere Codes zum Definieren von Zeichenfolgen ( SOS, Beginn der Zeichenfolge, Zeichenfolgenabschluss ST) oder zum Zurücksetzen aller Elemente auf einen Basiszustand ( RIS). Die obigen regulären Ausdrücke decken all dies ab.

Beachten Sie, dass der obige reguläre Ausdruck jedoch nur die ANSI C1-Codes entfernt und keine zusätzlichen Daten, die diese Codes möglicherweise markieren (z. B. die zwischen einem OSC-Öffner und dem abschließenden ST-Code gesendeten Zeichenfolgen). Das Entfernen dieser würde zusätzliche Arbeit außerhalb des Rahmens dieser Antwort erfordern.

Martijn Pieters
quelle
44

Die akzeptierte Antwort auf diese Frage berücksichtigt nur Farb- und Schrifteffekte. Es gibt viele Sequenzen, die nicht mit 'm' enden, z. B. Cursorpositionierung, Löschen und Scrollen.

Der vollständige reguläre Ausdruck für Kontrollsequenzen (auch bekannt als ANSI-Escape-Sequenzen) lautet

/(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]/

Siehe ECMA-48, Abschnitt 5.4 und ANSI-Escape-Code

Jeff
quelle
1
Es fehlt OSC (Anfang und Ende).
Thomas Dickey
1
OSC ist in ECMA-48 Sek. 5.6 - was bringt es, das hier anzusprechen?
Jeff
3
OSC ist eine "ANSI-Escape-Sequenz", wird häufig verwendet und würde mit einem anderen Muster beginnen. Ihre Antwort ist unvollständig .
Thomas Dickey
Dies funktioniert nicht für Farbcodes bluetoothctl, die beispielsweise von: erstellt wurden \x1b[0;94m. Herstellung des Ausdruck Groß- und Kleinschreibung oder Ersetzen 1Bmit 1bin dem Muster keinen Unterschied gemacht. Ich benutze Python und die Linie re.compile(r'/(\x9b|\x1b\[)[0-?]*[ -\/]*[@-~]/', re.I). Dann mache ich, pattern.sub("", my_string)was nichts bewirkt. Mache ich etwas falsch?
Hubro
1
Ich sehe drei Probleme mit dieser Antwort: 1) /.../ist keine Python-Syntax , sondern eine Syntax, die Sie in VI oder Perl oder awk verwenden würden. 2) Der \x9BOpener (für CSI-Codes) ist nicht mit UTF-8 kompatibel und wird daher nur noch selten verwendet. ESC [wird bevorzugt. 3) Ihr Muster deckt nur CSI-Codes ab, nicht den gesamten Bereich der ANSI-Escapezeichen (einschließlich OSC) Thomas Dickly erwähnt, aber auch SS2, SS3, DCS, ST, OSC, SOS, PM, APC und RIS!
Martijn Pieters
30

Funktion

Basierend auf der Antwort von Martijn Pieters ♦ mit Jeffs regulärem Ausdruck .

def escape_ansi(line):
    ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
    return ansi_escape.sub('', line)

Prüfung

def test_remove_ansi_escape_sequence(self):
    line = '\t\u001b[0;35mBlabla\u001b[0m                                  \u001b[0;36m172.18.0.2\u001b[0m'

    escaped_line = escape_ansi(line)

    self.assertEqual(escaped_line, '\tBlabla                                  172.18.0.2')

Testen

Wenn Sie es selbst ausführen möchten, verwenden Sie python3(bessere Unicode-Unterstützung, blablabla). So sollte die Testdatei sein:

import unittest
import re

def escape_ansi(line):class TestStringMethods(unittest.TestCase):
    def test_remove_ansi_escape_sequence(self):if __name__ == '__main__':
    unittest.main()
Édouard Lopez
quelle
Warum haben Sie das /Escapezeichen im vorletzten Zeichensatz verlassen [ -\/]?
Andrew Gelnar
1
@AndrewGelnar @ ÉdouardLopez [ -/]wird ausreichen.
Rodrigo Martins de Oliveira
1
Mein Regex wurde längst erweitert, um alle ANSI C1-Codes (7 Bit) abzudecken, und ich habe heute auch eine separate 8-Bit-Variante hinzugefügt.
Martijn Pieters
7

Die vorgeschlagene Regex hat mir nicht geholfen, also habe ich eine eigene erstellt. Das Folgende ist ein Python-Regex, den ich basierend auf der hier gefundenen Spezifikation erstellt habe

ansi_regex = r'\x1b(' \
             r'(\[\??\d+[hl])|' \
             r'([=<>a-kzNM78])|' \
             r'([\(\)][a-b0-2])|' \
             r'(\[\d{0,2}[ma-dgkjqi])|' \
             r'(\[\d+;\d+[hfy]?)|' \
             r'(\[;?[hf])|' \
             r'(#[3-68])|' \
             r'([01356]n)|' \
             r'(O[mlnp-z]?)|' \
             r'(/Z)|' \
             r'(\d+)|' \
             r'(\[\?\d;\d0c)|' \
             r'(\d;\dR))'
ansi_escape = re.compile(ansi_regex, flags=re.IGNORECASE)

Ich habe meinen regulären Ausdruck auf dem folgenden Snippet getestet (im Grunde genommen ein Kopieren und Einfügen von der Seite ascii-table.com).

\x1b[20h    Set
\x1b[?1h    Set
\x1b[?3h    Set
\x1b[?4h    Set
\x1b[?5h    Set
\x1b[?6h    Set
\x1b[?7h    Set
\x1b[?8h    Set
\x1b[?9h    Set
\x1b[20l    Set
\x1b[?1l    Set
\x1b[?2l    Set
\x1b[?3l    Set
\x1b[?4l    Set
\x1b[?5l    Set
\x1b[?6l    Set
\x1b[?7l    Reset
\x1b[?8l    Reset
\x1b[?9l    Reset
\x1b=   Set
\x1b>   Set
\x1b(A  Set
\x1b)A  Set
\x1b(B  Set
\x1b)B  Set
\x1b(0  Set
\x1b)0  Set
\x1b(1  Set
\x1b)1  Set
\x1b(2  Set
\x1b)2  Set
\x1bN   Set
\x1bO   Set
\x1b[m  Turn
\x1b[0m Turn
\x1b[1m Turn
\x1b[2m Turn
\x1b[4m Turn
\x1b[5m Turn
\x1b[7m Turn
\x1b[8m Turn
\x1b[1;2    Set
\x1b[1A Move
\x1b[2B Move
\x1b[3C Move
\x1b[4D Move
\x1b[H  Move
\x1b[;H Move
\x1b[4;3H   Move
\x1b[f  Move
\x1b[;f Move
\x1b[1;2    Move
\x1bD   Move/scroll
\x1bM   Move/scroll
\x1bE   Move
\x1b7   Save
\x1b8   Restore
\x1bH   Set
\x1b[g  Clear
\x1b[0g Clear
\x1b[3g Clear
\x1b#3  Double-height
\x1b#4  Double-height
\x1b#5  Single
\x1b#6  Double
\x1b[K  Clear
\x1b[0K Clear
\x1b[1K Clear
\x1b[2K Clear
\x1b[J  Clear
\x1b[0J Clear
\x1b[1J Clear
\x1b[2J Clear
\x1b5n  Device
\x1b0n  Response:
\x1b3n  Response:
\x1b6n  Get
\x1b[c  Identify
\x1b[0c Identify
\x1b[?1;20c Response:
\x1bc   Reset
\x1b#8  Screen
\x1b[2;1y   Confidence
\x1b[2;2y   Confidence
\x1b[2;9y   Repeat
\x1b[2;10y  Repeat
\x1b[0q Turn
\x1b[1q Turn
\x1b[2q Turn
\x1b[3q Turn
\x1b[4q Turn
\x1b<   Enter/exit
\x1b=   Enter
\x1b>   Exit
\x1bF   Use
\x1bG   Use
\x1bA   Move
\x1bB   Move
\x1bC   Move
\x1bD   Move
\x1bH   Move
\x1b12  Move
\x1bI  
\x1bK  
\x1bJ  
\x1bZ  
\x1b/Z 
\x1bOP 
\x1bOQ 
\x1bOR 
\x1bOS 
\x1bA  
\x1bB  
\x1bC  
\x1bD  
\x1bOp 
\x1bOq 
\x1bOr 
\x1bOs 
\x1bOt 
\x1bOu 
\x1bOv 
\x1bOw 
\x1bOx 
\x1bOy 
\x1bOm 
\x1bOl 
\x1bOn 
\x1bOM 
\x1b[i 
\x1b[1i
\x1b[4i
\x1b[5i

Hoffentlich hilft das anderen :)

kfir
quelle
Diese Spezifikation ist auch nicht vollständig, der Standard erlaubt eine Menge Erweiterungen, die VT100 nicht verwendet hat, aber andere Terminals, und Ihre Regex ist zu ausführlich für diesen Zweck.
Martijn Pieters
Ihr Muster weist auch einige seltsame Diskrepanzen auf. ESC- O(SS3) versetzt das Terminal in einen alternativen Schriftmodus, und das nächste Byte wird in diesem bestimmten Modus interpretiert. Die möglichen Werte in diesem Modus sind nicht beschränkt auf m, n, l, oder pdurch z. Ich würde das Byte nach SS3 nicht einmal entfernen. SS2 ist im Grunde die gleiche Funktionalität (nur eine andere Schriftart), aber Ihre Regex zieht nicht das nächste Byte ein.
Martijn Pieters
Last but not least kann Ihr Regex die vollständigen ANSI-Codes im Fragenbeispiel nicht entfernen , da das mletzte Byte zurückbleibt .
Martijn Pieters
0

Für 2020 mit Python 3.5 ist es so einfach wie string.encode().decode('ascii')

ascii_string = 'ls\r\n\x1b[00m\x1b[01;31mexamplefile.zip\x1b[00m\r\n\x1b[01;31m'
decoded_string = ascii_string.encode().decode('ascii')
print(decoded_string) 

>ls
>examplefile.zip
>
V. Ignatov
quelle
-1

Wenn es zukünftigen Stapelüberläufern hilft, habe ich die Buntstiftbibliothek verwendet , um meiner Python-Ausgabe eine etwas visuellere Wirkung zu verleihen. Dies ist vorteilhaft, da sie sowohl auf Windows- als auch auf Linux-Plattformen funktioniert. Ich habe jedoch sowohl auf dem Bildschirm angezeigt als auch Protokolldateien angehängt, und die Escape-Sequenzen haben die Lesbarkeit der Protokolldateien beeinträchtigt. Deshalb wollte ich sie entfernen. Die durch Buntstifte eingefügten Escape-Sequenzen führten jedoch zu einem Fehler:

expected string or bytes-like object

Die Lösung bestand darin, den Parameter in eine Zeichenfolge umzuwandeln, sodass nur eine geringfügige Änderung der allgemein akzeptierten Antwort erforderlich war:

def escape_ansi(line):
    ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
    return ansi_escape.sub('', str(line))
Rory
quelle
Das ist jedoch nicht wirklich das gleiche Problem. Es gibt viele verschiedene Bibliotheken, die möglicherweise benutzerdefinierte Objekte erzeugen, die eine Zeichenfolge umschließen. Wir benötigen hier keine Antworten für jede Variante, die in eine Zeichenfolge konvertiert werden muss, bevor ein regulärer Ausdruck darauf arbeitet.
Martijn Pieters
-3

Wenn Sie das \r\nBit entfernen möchten , können Sie die Zeichenfolge über diese Funktion ( geschrieben von sarnold ) übergeben:

def stripEscape(string):
    """ Removes all escape sequences from the input string """
    delete = ""
    i=1
    while (i<0x20):
        delete += chr(i)
        i += 1
    t = string.translate(None, delete)
    return t

Vorsicht, dies wird den Text vor und hinter den Escape-Sequenzen zusammenfassen. Wenn 'ls\r\nexamplefile.zip\r\n'Sie also Martijns gefilterten String verwenden , erhalten Sie lsexamplefile.zip. Beachten Sie den lsvor dem gewünschten Dateinamen.

Ich würde zuerst die Funktion stripEscape verwenden, um die Escape-Sequenzen zu entfernen, und dann die Ausgabe an Martijns regulären Ausdruck übergeben, wodurch eine Verkettung des unerwünschten Bits vermieden würde.

Neodied
quelle
Die Frage verlangt nicht, dass Leerzeichen entfernt werden, sondern nur ANSI- Escape-Codes. Ihre Übersetzung der string.translate()Option von sarnold ist auch nicht gerade idiomatisch (warum sollte sie verwendet werden, whilewenn forsie beendet ist xrange(), z. B. ''.join([chr(i) for i in range(0x20)])) und gilt nicht für Python 3 (wo Sie sie nur dict.fromkeys(range(0x20)))als string.translate()Karte verwenden könnten ).
Martijn Pieters