Reguläre Ausdrücke in VBA

Durch das Konzept der Objektbibliotheken kann VBA um zusätzliche Möglichkeiten erweitert werden. Das ist sinnvoll für Funktionen, die nur selten bzw. nur unter bestimmten Bedingungen gebraucht werden. Ein Beispiel dafür sind reguläre Ausdrücke, mit denen Textuntersuchungen ermöglicht werden, die weit über das hinausgehen, was mit den in VBA integrierten Zeichenfolgefunktionen möglich ist.

Bekanntlich kann man mit den Platzhalterzeichen des Like-Operators in einem Text ein Muster suchen. Darüber hinaus kennt VBA einige Zeichenfolgefunktionen, mit denen in gewissem Umfang auch Zeichenfolgen durchsucht oder verändert werden können. Will man aber z. B. strukturierte Textdateien im HTML-, XML- oder JSON-Format einlesen, oder ein Worddokument nach komplexen Kriterien durchsuchen, stößt man damit irgendwann an Grenzen. Mit regulären Ausdrücken, die ursprünglich aus anderen Programmiersprachen wie z.B. Perl oder PHP stammen, sind dagegen sehr viel flexiblere Suchmuster und Ersetzungen für Strings möglich.

Schauen wir uns zunächst einen einfachen Fall an, der mit dem Like-Operator lösbar ist:

Debug.Print "Mayer" Like "Ma?er"
Wahr

Nun stellen wir diesen Vergleich mit einem regulären Ausdruck nach. Dazu sollte die Microsoft VBScript Regular Expressions-Objektbibliothek eingebunden werden, wenn man nicht mit Late Binding arbeiten möchte.

Function vergleiche(str As String, muster As String) As Boolean
Dim regex As New RegExp

regex.Pattern = muster
vergleiche = regex.Test(str)

End Function

Debug.Print vergleiche("Mayer", "Ma.er")
Wahr

Es wird also ein Objekt vom Typ RegExp angelegt. Das Vergleichsmuster - also der eigentliche reguläre Ausdruck - wird in der Eigenschaft Pattern gesetzt. Wir sehen, dass die Vergleichsmuster eines regulären Ausdrucks anders sind als die Platzhalterzeichen des Like-Operators. Während allerdings damit die Möglichkeiten von Like allmählich ausgeschöpft sind, fängt RegExp hier erst an. Schon für die Suche nach einem einzelnen Zeichen stehen verschiedene Suchmuster zur Verfügung:

Reguläre Ausdrücke für einzelne Zeichen
Symbol Beispiel Ergebnis Verwendung
. Ma.er findet Maier, Majer, Mayer Ein beliebiges Zeichen
[ ] Ma[iy]er findet Maier, Mayer Eines der Zeichen in den Klammern
[ - ] b[a-z]d findet bad, bbd, bcd, bzd Ein Zeichen im angegebenen Bereich
(Reihenfolge gemäß Zuordnungstabelle)
[^ ] Ma[^iy]er  findet Majer, aber nicht Maier oder Mayer Ein nicht in den Klammern aufgelistetes Zeichen
\d Nr \d findet Nr 1, Nr 9 Eine Ziffer (entspricht [0-9])
\D Nr \D findet Nr A, aber nicht Nr 1 Keine Ziffer (entspricht [^0-9])
\w Anhang \w findet Anhang 1, Anhang A Buchstabe, Ziffer oder Unterstrich (entspricht [a-zA-Z0-9_])
\W abc\Wefg findet abc efg, aber nicht abcdefg kein Buchstabe, Ziffer oder Unterstrich (entspricht [^a-zA-Z0-9_])
\t abc\txyz findet abc & vbTab & xyz Steuerzeichen „Tabulator“
\n abc\nxyz findet abc & vbNewLine & xyz Steuerzeichen für Zeilenumbruch
\f abc\fxyz findet abc & vbFormFeed & xyz Steuerzeichen für vbFormFeed
\r abc\rxyz findet abc & vbLf & xyz Steuerzeichen für vbLf
\x \x41BC findet ABC Auf „x“ folgt der hexadezimale Zeichenwert eines Zeichens

Es gibt auch Möglichkeiten, die Plätze des Suchmusters innerhalb der zu durchsuchenden Zeichenkette anzugeben.

Reguläre Ausdrücke für den Gesamtausdruck
Symbol Beispiel Ergebnis Verwendung
^ ^Wort findet Wort am Anfang markiert den Anfang der Zeichenkette/Zeile
$ Wort$ findet Wort am Ende markiert das Ende der Zeichenkette/Zeile
\b ung\b findet ung am Wortende bezeichnet eine Wortgrenze (zwischen \w und \W)
\B \Baus findet Haus oder Maus, aber nicht aus bezeichnet alles außer einer Wortgrenze

Will man nach einem Zeichen suchen, das in einem regulären Ausdruck eine Bedeutung hat, wie z. B. [ oder ^, muss man einen Backslash (\) voranstellen. Das nennt man „das Zeichen maskieren“. Um nach dem Backslash selbst zu suchen, entsprechend zweimal den Backslash.

Debug.Print vergleiche("F:\Data", "\\")
Wahr

Mit regulären Ausdrücken kann man auch nach längeren Zeichenketten suchen. In aller Regel muss dafür diese Zeichenkette mit (runden Klammern) zu einem Teilausdruck gruppiert werden. In der folgenden Tabelle werden die entsprechenden Möglichkeiten anhand einzelner Zeichen aufgeführt, genauso könnten dort aber auch Teilausdrücke stehen.

Reguläre Ausdrücke für (Teil)ausdrücke
Symbol Beispiel Ergebnis Verwendung
* Han*s findet Has, Hans, Hanns der vorhergehende Ausdruck nicht oder beliebig oft
+ Han+s findet Hans, Hanns der vorhergehende Ausdruck einmal oder beliebig oft
? Han?s findet Has oder Hans der vorhergehende Ausdruck nicht oder einmal
{ } A{3} findet AAA der vorhergehende Ausdruck exakt so oft, wie in der Klammer angegeben
{ , } A{2,4} findet AA, AAA und AAAA der vorhergehende Ausdruck so oft, wie in der Klammer angegeben
{ ,} A{2,} findet AAA, AAAA, AAAAA, ... der vorhergehende Ausdruck mindestens so oft, wie in der Klammer angegeben
{, } A{,4} findet A, AA, AAA und AAAA der vorhergehende Ausdruck höchstens so oft, wie in der Klammer angegeben

Um nach Teilausdrücken zu suchen, ist also beispielsweise auch Ich gehe( langsam)? zu dir möglich, wenn man nicht weiß, ob „langsam“ an der entsprechenden Stelle im String vorkommt.

In den Klammern können mit | auch Alternativen eingebaut werden, z. B.
And(i|reas), für Andi oder Andreas,
Wir gehen zu (dir|mir|uns|euch).

Bei Suchmustern mit * oder + ist zu beachten, dass sie im Normalfall besonders „gierig“ sind:

Debug.Print vergleiche("Mein Haus! Mein Auto! Mein Boot!", "Mein .+!")
Wahr

Zur besseren Übersichtlichkeit ist in diesem Beispiel markiert, was der reguläre Ausdruck Mein .+! alles erkennt: Wegen des .+ nämlich alles zwischen dem ersten „Mein “ bis zum letzten Ausrufezeichen. Soll der Ausdruck statt dessen nur bis zum ersten Ausrufezeichen gelten, muss auf das + ein ? folgen, um nicht so gierig zu sein:

Debug.Print vergleiche("Mein Haus! Mein Auto! Mein Boot!", "Mein .+?!")
Wahr

Bisher haben wir uns lediglich die Test-Methode sowie die Pattern-Eigenschaft des RegExp-Objekts angesehen. Dieses Objekt hat allerdings noch weitere Eigenschaften und Methoden: Mit der IgnoreCase-Eigenschaft kann bestimmt werden, ob zwischen Groß- und Kleinschreibung unterschieden wird, mit MultiLine wird bestimmt, ob Zeilenumbrüche im Suchstring dazu führen, dass jede Zeile als eigener Ausdruck behandelt werden soll, und die Global-Eigenschaft bestimmt, ob nur die erste gefundene Suchstelle zurückgegeben werden soll, wenn der Ausdruck mehrfach matcht.

Insbesondere die letzten beiden Eigenschaften werden erst interessant, wenn nicht nur einfach die Test-Methode zum Einsatz kommt. Test gibt nur zurück, ober der Ausdruck matcht oder nicht. Dagegen wendet die Execute-Methode den Ausdruck zunächst an und speichert das Ergebnis in Objekten vom Typ Match, die wiederum in einem MatchCollection-Objekt zusammengefasst sind.

Sub suchInZeile()
Dim regex As New RegExp, Fundstellen As MatchCollection, Fund As Match
Dim myString As String

myString = "Zeile A" & vbNewLine & "Zeile B"

regex.Pattern = "\w$"
regex.MultiLine = True
regex.Global = True

Set Fundstellen = regex.Execute(myString)

Debug.Print Fundstellen.Count
For Each Fund In Fundstellen
    Debug.Print Fund
Next

End Sub

In diesem Beispiel wird zuerst ein zweizeiliger String definiert. Als Suchmuster wird \w$ definiert; es wird also nach Buchstaben am Ende gesucht. Während aber üblicherweise die Suche beendet wird, sobald ein erster Treffer gefunden wurde, wird hier mit Global nach allen Vorkommen gesucht. Da auch noch MultiLine gesucht wird, bekommt das $ im Suchmuster eine etwas andere Bedeutung: Nun heißt es nicht mehr „Ende des gesamten zu durchsuchenden Strings", sondern „Ende einer jeden Zeile“. Abschließend wird ausgegeben, wie viele Treffer gefunden wurden, und dann noch alle einzelnen Treffer.

suchInZeile
 2
A
B

Wenn nicht sowohl Global als auch MultiLine aktiviert sind, wird immer nur eines der beiden Zeilenenden gefunden, und zwar entweder „A“ oder „B“.

Wie schon erwähnt, kann man Teilausdrücke mit (runden Klammern) gruppieren. Diese Teilausdrücke werden zugleich auch in eigenen Objekten gespeichert. Während der gesamte gefundene Ausdruck, wie eben beschrieben, im Match-Objekt gespeichert wird, finden sich Teilausdrücke darin in SubMatch-Objekten:

Sub suchKlammern()
Dim regex As New RegExp, Fundstellen As MatchCollection, Fund As Match
Dim myString As String

myString = "Lukas ist ein bisschen eigenartig"

regex.Global = True
regex.Pattern = "\b(\w)(\w+)"

Set Fundstellen = regex.Execute(myString)

Debug.Print Fundstellen.Count
For Each Fund In Fundstellen
    Debug.Print Fund.SubMatches(0), Fund.SubMatches(1)
Next

End Sub

Dieses Beispiel ähnelt dem vorigen. Im Unterschied dazu werden nun im Suchmuster Klammern verwendet, nämlich einmal für den ersten Buchstaben eines Worts sowie für den Rest des Wortes. Wegen der Global-Eigenschaft kommt es je Wort zu zwei Funden, und die For-Each-Schleife gibt die jeweiligen Inhalte der Klammern mit Hilfe des Submatches-Objekts aus.

suchKlammern
 5
L             ukas
i             st
e             in
b             isschen
e             igenartig

Mit der Replace-Methode sind auch Ersetzungen im Suchstring möglich.

Function ersetze(myString As String, Muster As String, Ersatz As String)
Dim regex As New RegExp

regex.Global = True
regex.Pattern = Muster
Set Fundstellen = regex.Execute(myString)

ersetze = regex.Replace(myString, Ersatz)

End Function

Dieser einfachen Prozedur werden der zu durchsuchende String und ein Suchmuster übergeben. Mit Replace werden die gefundenen Suchmuster durch strErsatz ersetzt.

Debug.Print ersetze("überflüssig", "ü", "ue")
ueberfluessig

Enthält der Suchstring geklammerte Ausdrücke, kann man im Ersetzungsstring auf den Inhalt der Klammern zugreifen. Der Inhalt der Klammern wird in „Variablen“ mit den vordefinierten Namen $1, $2 usw. gespeichert, in der Reihenfolge der Klammern.

Debug.Print ersetze("Maierhuber, Dieter", "(.+), (.+)", "$2 $1")
Dieter Maierhuber