Text zerlegen, bereinigen und vereinheitlichen mit regulären Ausdrücken, und die Fallen bei Punkt, Gier und Umlauten.
Kernideen
Ein regulärer Ausdruck beschreibt ein Muster, nicht einen festen Text
Erst bereinigen (Leerzeichen, Gross- und Kleinschreibung), dann zerlegen, dann prüfen, wie viel erkannt wurde
Der Punkt steht für jedes Zeichen; ein echter Punkt wird maskiert: \.
Quantoren sind gierig: .* nimmt so viel wie möglich, .*? so wenig wie möglich
[A-Za-z] kennt keine Umlaute; für Buchstaben aller Sprachen gibt es eigene Klassen
Nach jeder Regel zählen, wie viele Zeilen nicht gepasst haben; die sind das Problem
Erklärung
Vorwissen:Data Wrangling für die Arbeit mit Spalten und Datenqualität, wo uneinheitliche Schreibweisen zum ersten Mal auftauchen. Für Datumsangaben in Text gibt es eigene Funktionen, siehe Datum und Zeit.
Die Bausteine
Baustein
Bedeutung
Beispiel
passt auf
.
ein beliebiges Zeichen
a.c
abc, a-c
\d, \s, \w
Ziffer, Leerraum, Wortzeichen
\d{4}
8001
\D, \S, \W
jeweils das Gegenteil
\D
alles ausser Ziffern
[abc], [^abc]
eines dieser Zeichen, keines davon
[aeiou]
Vokal
*, +, ?
beliebig oft, mindestens einmal, höchstens einmal
\d+
7, 101
{n}, {n,m}
genau n, zwischen n und m Mal
\d{2,3}
12, 123
^, $
Anfang, Ende
^\d+$
nur Ziffern
( )
Gruppe, wird einzeln herausgezogen
(\d{4}) (.+)
PLZ und Ort
.*?, +?
nicht gierig
<b>.*?</b>
ein einzelnes Tag
(?=...)
Lookahead: folgt darauf, ohne es mitzunehmen
41(?=\d{9}$)
41 vor neun Ziffern
R und Python
Aufgabe
R (Basis, perl = TRUE)
Python
passt das Muster?
grepl(muster, x, perl = TRUE)
re.search(muster, x), s.str.contains()
ersetzen
gsub(muster, ersatz, x, perl = TRUE)
re.sub(), s.str.replace(regex=True)
Gruppen herausziehen
regmatches(x, regexec(muster, x, perl = TRUE))
s.str.extract()
alle Treffer
regmatches(x, gregexpr(muster, x, perl = TRUE))
re.findall()
Buchstaben aller Sprachen
\\p{L}
[^\W\d_] im Modul re
In R steht jeder Backslash doppelt, weil die Zeichenkette ihn selbst schon als Maskierung liest: "\\d" im Code ist \d im Muster. In Python verhindert ein vorangestelltes r dasselbe Problem: r"\d".
Das Paket stringr bietet in R dieselben Funktionen mit einheitlicheren Namen (str_detect, str_extract, str_replace_all). Die Muster sind dieselben.
Fünf Adressen stehen als Text in einer Spalte. Gebraucht werden Strasse, Hausnummer, Postleitzahl und Ort in eigenen Spalten. Eine Adresse ist ein Postfach, eine liegt in Deutschland und hat eine fünfstellige Postleitzahl.
strasse nr plz ort
[1,] "Bahnhofstrasse" "12" "8001" "Zürich"
[2,] "Rue du Lac" "5a" "1700" "Fribourg"
[3,] NA NA NA NA
[4,] "Hauptgasse" "7" "4500" "Solothurn"
[5,] NA NA NA NA
c(erkannt =sum(!is.na(teile[, "plz"])), von =length(adressen))
.+? erlaubt Leerzeichen im Strassennamen, [a-z]? den Buchstaben
Postfach, 3000 Bern
nichts erkannt
keine Hausnummer; richtig, dass das Muster nicht passt
Hauptgasse 7 , 4500 Solothurn
erkannt trotz Leerzeichen vor dem Komma
\s* vor und nach dem Komma
Seestrasse 101, 88400 Konstanz
nichts erkannt
\d{4} verlangt vier Ziffern, die deutsche PLZ hat fünf
Interpretation
Drei von fünf Adressen passen. Die zwei anderen sind kein Fehler des Musters, sondern Fälle, die gesondert behandelt werden müssen. Genau deshalb gehört nach jedem Zerlegen die Zählung der nicht erkannten Zeilen dazu: Ein Muster, das alles „erkennt”, hat meistens etwas falsch erkannt.
Frage und Datenlage
Sechs Telefonnummern in verschiedenen Schreibweisen sollen in die einheitliche Form +41 plus neun Ziffern gebracht werden.
tel = ["+41 79 123 45 67", "079/123 45 67", "0041791234567","079 123 4567", "79 123 45 67", "044-555-12-12"]def normiere(x): z = re.sub(r"\D", "", x) # nur Ziffern behalten z = re.sub(r"^0041", "", z) # internationale Vorwahl z = re.sub(r"^41(?=\d{9}$)", "", z) # +41 ohne Nullen z = re.sub(r"^0", "", z) # fuehrende Nullreturn"+41"+ z if re.fullmatch(r"\d{9}", z) elseNoneprint([normiere(x) for x in tel])
\D entfernt Plus und Leerzeichen, danach 41 vor neun Ziffern
079/123 45 67
+41791234567
führende Null
0041791234567
+41791234567
0041
079 123 4567
+41791234567
andere Gruppierung, gleiche Ziffern
79 123 45 67
+41791234567
schon ohne Null, neun Ziffern
044-555-12-12
+41445551212
Festnetz, gleiche Regeln
Interpretation
Die Reihenfolge der Schritte trägt die Logik: erst alles ausser Ziffern entfernen, dann Vorwahlen, zuletzt prüfen, ob genau neun Ziffern übrig sind. Eine Nummer, die das nicht erfüllt, wird NA statt einer halb richtigen Form. Der Lookahead in Schritt drei verhindert, dass eine Nummer, die zufällig mit 41 beginnt, beschnitten wird.
Frage und Datenlage
Zwei Fehler, die Muster still falsch machen: ein unmaskierter Punkt und ein gieriger Quantor.
werte = ["3.5", "3x5", "305"]print("punkt_roh: ", [bool(re.search(r"3.5", w)) for w in werte])
punkt_roh: [True, True, True]
print("punkt_maskiert:", [bool(re.search(r"3\.5", w)) for w in werte])
punkt_maskiert: [True, False, False]
s ="<b>Preis</b> und <b>Menge</b>"print(re.findall(r"<b>.*</b>", s))
['<b>Preis</b> und <b>Menge</b>']
print(re.findall(r"<b>.*?</b>", s))
['<b>Preis</b>', '<b>Menge</b>']
Output Zeile für Zeile
Muster
Ergebnis
Wie es zu lesen ist
3.5
passt auf alle drei
der Punkt steht für jedes Zeichen, auch x und 0
3\.5
passt nur auf 3.5
maskiert ist der Punkt ein Punkt
<b>.*</b>
ein einziger Treffer über den ganzen Text
.* läuft bis zum letzten</b>
<b>.*?</b>
zwei Treffer
.*? hört beim ersten</b> auf
Interpretation
Beide Fehler erzeugen keine Fehlermeldung, sondern zu viele Treffer. Das fällt nur auf, wenn man die Treffer ansieht. Für HTML ist ein regulärer Ausdruck ohnehin das falsche Werkzeug; dafür gibt es Parser, siehe APIs und Web-Scraping.
Frage und Datenlage
Aus Freitextrückmeldungen sollen die häufigsten Wörter gezählt werden. Der Text enthält Umlaute.
z_ascii = pd.Series(nur_ascii).value_counts()z_voll = pd.Series(alle_buchstaben).value_counts()print({w: int(z_ascii[w]) for w in ["qualit", "t", "sp"]})
{'qualit': 3, 't': 4, 'sp': 1}
print({w: int(z_voll[w]) for w in ["qualität", "spät", "pünktlich"]})
{'qualität': 3, 'spät': 1, 'pünktlich': 1}
Output Zeile für Zeile
Ausgabe
Wert
Wie er zu lesen ist
Wörter mit [A-Za-z]+
17
zu viele: jedes Umlautwort zerfällt in Stücke
Wörter mit allen Buchstaben
12
richtig
qualit
3
„Qualität” ohne Endung
t
4
Reststücke aus „Qualität” und „spät”
qualität
3
mit der vollständigen Klasse korrekt gezählt
Interpretation
[A-Za-z] ist in Anleitungen verbreitet und für deutschen Text falsch. Das Ergebnis sieht plausibel aus, bis man die Liste der Wörter liest. In R hilft \p{L} mit perl = TRUE, in Python [^\W\d_], weil \w dort Unicode-Buchstaben bereits einschliesst, aber auch Ziffern und den Unterstrich.