Reguläre Ausdrücke Python Tutorial Teil 25

Reguläre Ausdrücke (engl.: regular expressions) dienen dazu, Texte über syntaktische Regeln zu filtern oder zu verarbeiten.

Python Logo (CC-BY-SA The people from the Tango! project / Wikipedia)
Python Logo (CC-BY-SA The people from the Tango! project / Wikipedia)

Reguläre Ausdrücke

Die Wikipedia definiert einen regulären Ausdruck als die Beschreibung von Mengen mit Hilfe syntaktischer Regeln. Da so eine Definition natürlich schwer zu verstehen ist und hochtrabend klingt, untersuchen wir das hier lieber anhand von praktischen Beispielen.

Zunächst brauchen wir trotzdem etwas Theorie. regular expressions sind eine formale Sprache, die natürlich eine gewisse Syntax mit einem definierten Wortschatz hat. Dieser Wortschatz besteht aus Quantoren und Zeichenklassen, die ich hier zunächst mal aufliste.

ZeichenBedeutung
Quantoren
*der voranstehende Ausdruck darf beliebig oft vokommen
? der vorstehende Ausdruck, darf ein- oder keinmal vokommen.
{n,m}Ausdruck muss mind. n-mal vorkommen und darf max. m-mal vorkommen.
{n}Ausdruck muss exakt n-mal vorkommen.
{0,m}Audruck darf max. m-mal vorkommen.
Zeichenklassen:
[abc]eins der Zeichen a,b,c
[0-9]eine Ziffer zwischen und und 9
[^a]ein beliebiges Zeichen ausser ‚a‘ (Negation)
vordefinierte Zeichenklassen:
\deine Ziffer (äquivalent zu [0-9])
\DZeichen, das keine Ziffer ist (^\d)
\swhitspace (Leerzeichen, Tab Carriage Return)
Sonderzeichen
^Stringanfang
$Stringende

Du hast bestimmt schon mal einen regulären Ausdruck in der bash benutzt. Beispielsweise, um alle Dateien mit der Endung .py auf zu listen, gibst du

ls *.py

ein.

Screenshot: Beispiel für reguläre Ausdrücke,  ls mit *.py als regulärem Ausdruck zeigt nur Dateien mit der Endung .py an
Screenshot: Beispiel für reguläre Ausdrücke ls mit *.py zeigt nur Dateien mit der Endung .py an

Was passiert jetzt hier? Der Quantor * steht für beliebig viele beliebige Zeichen, gefolgt von einem . (genaugenommen einem beliebigen Zeichen) und py. Bei jeder Datei im aktuellen Verzeichnis, prüft ls nun, ob dieser reguläre Ausdruck auf den Dateinamen passt. Nur dann wird er ausgegeben.Mal sehen, was passiert, wenn wir den regulären Ausdruck mal etwas verändern

ls p*.*
Screenshot: Beispiel für reguläre Ausdrücke, ls mit p*.* zeigt nur Dateien an, die mit p beginnen
Screenshot: Beispiel für reguläre Ausdrücke, ls mit p*.* zeigt nur Dateien an, die mit p beginnen

In diesem Fall, muss der Dateinamen mit einem kleinen p beginnen, dann dürfen beliebige Zeichen folgen und hinter dem Punkt dürfen auch beliebige Zeichen stehen.

reguläre Ausdrücke in Python

Python bringt für die Verarbeitung von regulären Ausdrücken das Modul re (für regular expression) mit.

Dies bietet verschiedene Methoden an, um reguläre Ausdrücke zu verwenden. Ich habe die wichtigsten mal in einem kleinen Unittest zusammengefasst.

import re
import unittest
import logging

pattern_char_only = re.compile('^[A-Z][a-z]+$')
pattern_digits_only = re.compile(r'\d+')
pattern_no_digits = re.compile(r'\D+')
pattern_plz = re.compile('^[0-9]{5}$')
pattern_plz2 = re.compile('^\d{5}$')
pattern_quote= re.compile(':')
pattern_tab = re.compile(r'\t')
pattern_simpson = re.compile('Simpson')
pattern_simple = re.compile('O*')

logging.basicConfig( format='%(asctime)-15s [%(levelname)s] %(funcName)s: %(message)s', level=logging.DEBUG)

# Unittest für die Demonstration von regular expressions (re)
class TestRegEgEx(unittest.TestCase):

  # match: Passt der String auf die re
  def test_match(self):
    m = pattern_char_only.match('Olli')

    logging.debug(f'm={m}')
    self.assertTrue(m.span()[1] >0)

  # Sucht alle Vorkommnisse der re im String
  def test_findall(self):
    s = '5 Äpfel, 10 Bananen, 42 Erdbeeren'
    logging.debug(f' nur Ziffern: {pattern_digits_only.findall(s)}')
    logging.debug(f' keine Ziffern: {pattern_no_digits.findall(s)}')
    m = pattern_char_only.findall('Olli')
    logging.debug(f'm={m}')
    self.assertIsNotNone(m)

  # Prüft die Gültigkeit einer Postleitzahl ('^\d{5}$' genau 5 Ziffern)
  def test_plz(self):
      patterns = [pattern_plz,pattern_plz2]
      plz = ['42275','5600']

      for pattern in patterns:
        m = pattern.findall(plz[0])
        logging.debug(f'm={m}')
        self.assertNotEqual('',m[0])
        m = pattern.findall(plz[1])
        logging.debug(f'm={m}')
        self.assertEqual([],m)


  # Test für Suche
  def test_search(self):
    m = pattern_char_only.search('Olli')
    logging.debug(f'm={m}')
    #String passt auf die regular expression
    self.assertIsNotNone(m)

    m = pattern_char_only.search('123456')
    logging.debug(f'm={m}')
    #Dieser String passt nicht.
    self.assertIsNone(m)

  # Test zum Splitting (der Satz wird am ':' in die Worte zerlegt)
  def test_split(self):
    m = pattern_quote.split(' Die:Würde:des:Menschen:ist:unantastbar.')
    logging.debug(f'm={m}')
    self.assertIsNotNone(m)

    #Die einzelnen Worte ausgeben
    for word in m:
        logging.debug(f' Wort: {word}')


  # Test für Ersetzung (substitution von Tab mit Space)
  def test_sub(self):
    m  = pattern_tab.sub(' ','Satz\tmit\tTabulator.')
    logging.debug(f'm={m}')

  # Test für einfache regular Expression ('O*')
  def test_simple_re(self):
    m = pattern_simple.search('Olli')
    logging.debug(f'm={m.span()}')
    self.assertTrue(m.end() > 0)
    m = pattern_simple.search('Luana')
    logging.debug(f'm={m.span()}')
    self.assertFalse(m.end() >0)

  # Test für den Iterator ('*Simpson')
  def test_finditer(self):
    m = pattern_simpson.finditer('Homer Simpson, Marge Simpson, Bart Simpson, Lisa Simpson, Maggie Simpson')
    logging.debug(f'm={m}')

    # Die einzelnen gefunden Teilstrings ausgeben.
    for name in m:
      logging.debug(f'Name={name},Start bei: {name.start()}')




if __name__ == '__main__':
    unittest.main()

compile()

Dreh- und Angelpunkt des re Moduls ist die compile() Methode. Sie übersetzt den String mit dem regulären Ausdruck in ein Format, dass durch den Python Interpreter verarbeitbar ist. Da dies etwas Zeit kostet, solltest du dies möglichst nur einmal machen. Ich definiere die verschiedenen pattern_ Variablen direkt am Anfang des Unittest.

Die Match Klasse

Alle Methoden von re liefern ein Objekt vom Typ Match zurück. In diesem Match Objekt werden verschiedene Angaben zu der ausgewerteten regular expression festgehalten. Bei älteren Versionen lieferten Methoden ein None zurück, wenn kein Match gefunden wurde. Dies wurde inzwischen geändert. Hier ein Beispiel das dem Test oben:

test_search: m=<re.Match object; span=(0, 4), match='Olli'>

Das Match Objekt enthält ein Tupel span(), in dem festgehalten ist, an welcher Stelle im String der Match erfolgreich war (hier Index 0) und wie lang der matching String ist (4 Zeichen). Auf die Werte kannst entweder mit span()[0] und span()[1] oder mit start() und end() zugreifen.

match()

Die match() Methode prüft einfach, ob ein String auf den regular expression passt. Ist dies nicht der Fall, wird bis Python 3.10 None zurückgegeben. Bei Python 3.12 auf dem Raspberry Pi mit Debian 12 kommt in jedem Fall ein Match Objekt zurück, bei dem span()[1] bzw. end() gleich 0 ist. Daher testet die test_match() Methode beide Fälle. Die verwendete re ‚^[A-Z][a-z]+$‘, besagt folgendes: Vom Anfang des Strings ^ an, dürfen bis zum Ende $ nur Zeichen aus den Zeichenklassen Großbuchstaben [A-Z] und Kleinbuchstaben [a-z] einmal oder keinmal + vorkommen.

findAll()

findall() liefert dir eine Liste von Match Objekten mit allen Vorkommnissen der regular expression in einem String.

search()

search() sucht genau nach dem ersten Vorkommen der regular expression in einem String und liefert ein Match Objekt zurück. Der Ausdruck in pattern_simple soll mit ‚O*‘ nur auf Strings passen, die mit einen großen O beginnen und danach beliebige Zeichen folgen. Passt der Ausdruck, liefert end() von Match einen Wert größer als 0 zurück, im negativen Fall eine 0. Der String ‚Olli‘ passt natürlich, ‚Luana‘ nicht.

split()

Mit einer regular expression kannst du einen String auch in Einzelteile zerlegen. In pattern_quote ist die re ':' abgelegt. Sie definiert nur das Zeichen, an der der String 'Die:Würde:des:Menschen:ist:unantastbar.' getrennt werden soll. pattern_quote.split() mit diesem String liefert dann eine Liste der einzelnen Worte zurück.

sub()

Ebenso einfach ist es, Zeichen zu ersetzen. Die sub() Methode soll alle Tabulatorzeichen gegen ein Leerzeichen austauschen. Da '\t' in Python schon eine besondere Bedeutung hat, wird pattern_sub als Raw-String mit vorangestelltem r definiert r'\t'. Der Aufruf pattern_tab.sub(' ','Satz\tmit\tTabulator.') liefert dann den zweiten String mit Leerzeichen an Stelle der Tabulatorsymbole.

finditer()

Die Methode finditer() liefert dir eine iterierbare Liste aller Vorkommnisse eines regulären Ausdrucks in einem String. Interessant ist hier vermutlich meistens die Startposition eines Match.

Fallbeispiel: Telefonnummer verarbeiten

Als erstes praktische Anwendung möchte ich dir Zeigen, wie du mit einem regulären eine eingegebene Telefonnummer im internationalen Format verarbeiten kannst, dass nur noch die Ziffern übrigbleiben:

import re
import unittest

class RegEx(unittest.TestCase):

  pattern_phone = re.compile(r'\D')
  pattern_digits_only = re.compile(r'\d')

  def test_phone(self):

      m = self.pattern_phone.sub('','+1 (212) 555-6832')

      self.assertIsNotNone(self.pattern_digits_only.match(m))

      print(f'm={m}')

if __name__ == '__main__':
 unittest.main()

Die erste regular expression ersetzt alle Leerzeichen in der Telefonnummer gegen einen Leerstring, bei zweiten Aufruf wird alles entfernt, was keine Ziffer ist.

Programmausgabe: Nach Behandlung durch reguläre Ausdrücke sind nur noch die Ziffern der Telefonnummer übrig.
Programmausgabe: Nach Behandlung durch reguläre Ausdrücke sind nur noch die Ziffern der Telefonnummer übrig.

Fazit

Wenn du reguläre Ausdrücke beherrscht, kannst du damit u.U. viel Arbeit sparen. Im Repository habe ich dir noch ein Beispiel hinterlegt, wie du die Namensliste aus dem Matplotlib Teil mit re filtern kannst.

Schreibe einen Kommentar

Cookie Consent Banner von Real Cookie Banner