Ausführliche Beispiele
(Auszug aus "Reguläre Ausdrücke" von Jeffrey E. F. Friedl)
Mit diesen zwei Programmbeispielen beschließen wir das Kapitel.
CSV-Dateien mit PHP verarbeiten
Es folgt die PHP-Version des CSV-Beispiels aus Die Kunst, reguläre Ausdrücke zu schreiben (siehe Aufbrechen der Schleife bei CSV-Daten). Die Regex verwendet possessive Quantoren (siehe Possessive Quantoren) statt der atomaren Klammern, das sieht übersichtlicher aus.
Die eigentliche Regex ist die folgende:
$csv_regex = '{
\G(?:^|,)
(?:
# Entweder ein Feld in Anführungszeichen ...
" # Öffnendes Anführungszeichen
( [^"]*+ (?: "" [^"]*+ )*+ )
" # Schließendes Anführungszeichen
| # ... oder ...
# ... ein ›nacktes‹ Feld ohne Kommas und Anführungszeichen.
( [^",]*+ )
)
}x';
Mit dieser Regex wird eine $zeile im CSV-Format verarbeitet:
/* Regex anwenden, Zwischenresultate werden in $alle_treffer geschrieben. */
preg_match_all($csv_regex, $zeile, $alle_treffer);
/* Im Array $Resultate werden die einzelnen Feldinhalte aus $alle_treffer gesammelt. */
$Resultate = array ();
/* Alle Treffer abklappern ... */
for ($i = 0; $i < count($alle_treffer[0]); $i++)
{
/* Wenn im 2. Klammerpaar etwas aufgefangen wurde, ist das direkt der Feldinhalt. */
if (strlen($alle_treffer[2][$i]) > 0)
array_push($Resultate, $alle_treffer[2][$i]);
else
{
/* Ein Feld in Anführungszeichen. Maskierte Anführungszeichen zurückverwandeln. */
array_push($Resultate, preg_replace('/""/', '"', $alle_treffer[1][$i]));
}
}
/* Das Array $Resultate enthält nun die Feldinhalte. */
Tags auf korrekte Verschachtelung prüfen
Und als Abschluss noch ein Problem, das viele interessante Punkte berührt: Wir wollen prüfen, ob die Tags in einem XML-Text (oder XHTML oder ähnlichen Auszeichnungssprachen) in Bezug auf die Klammerung wohlgeformt sind, d.h., dass sie keine verwaisten Tags (öffnende ohne schließende und umgekehrt) enthalten und dass sie richtig verschachtelt sind. Unser Ansatz sucht nach zusammengehörenden paarigen Tags, nach Textstücken, die keine Tags enthalten, und nach der abgekürzten Schreibweise für leere Elemente (z.B. <br/>).
So sieht die Regex aus:
˹^((?:<(\w++)[^>]*+(?<!/)>(?1)</\2>|[^<>]++|<\w[^>]*/>)*+)$˼
Ein String, auf den diese Regex passt, hat keine falsch verschachtelten oder ungepaarten Tags (ein paar Vorbehalte dazu später).
Die Regex mag zu Anfang sehr komplex erscheinen, aber wenn man sie in sinnvolle Teile zerlegt, wird es schnell übersichtlich. Die äußere Klammer mit den Ankern ˹^(...)$˼ verpackt den eigentlichen Kern der Regex und stellt sicher, dass ein erfolgreicher Treffer den ganzen Suchtext umfasst. Die einfangenden Klammern werden außerdem dazu verwendet, die ganze darin enthaltene Kern-Regex rekursiv anzuwenden.
Die Kern-Regex in diesem Ausdruck
Der eigentliche Hauptteil der Regex besteht aus drei Alternativen (oben separat unterstrichen), die in ˹(?:...)*+˼ verpackt sind, so dass im untersuchten Text eine Folge dieser Alternativen auftreten kann. Die drei Alternativen sollen auf die drei Bausteine passen: auf paarige Elemente, auf Text ohne Tags und auf leere Elemente in Kurzschreibweise.
Weil jede Alternative die andere ausschließt (d.h. es gibt keinen String, der auf mehr als eine der Alternativen passen könnte), ist sichergestellt, dass ein Backtracking-Vorgang nie einen Treffer finden wird, bei dem der zurückgegebene Text auf eine andere Alternative passt. Das können wir ausnützen und zur Optimierung ein possessives * um die Alternation schreiben. Wenn eine der Alternativen gepasst hat, wird die Regex-Maschine also keinen Backtrack-Vorgang mehr auslösen.
Aus dem gleichen Grund ist die Reihenfolge der Alternativen irrelevant. Ich nehme die Alternative zuerst, von der ich annehme, dass sie am häufigsten vorkommt (siehe Die wahrscheinlichste Alternative zuerst).
Betrachten wir nun die Alternativen der Reihe nach:
Zweite Alternative: Text ohne Tags
Die mittlere Alternative ist die einfachste: ˹[^<>]++˼. Dieser Unterausdruck passt auf Textstücke ohne spitze Klammern, also können darin keine Tags vorkommen. Der Einsatz des possessiven Quantors ˹(?:...)*+)˼ mag übertrieben sein, weil ja schon bei der äußeren Klammer ein solcher possessiver Quantor verwendet wird; aber es ist mir aus Sicherheitsgründen lieber, einen possessiven Quantor zu verwenden, auch wenn er fakultativ ist. (Meistens verwendet man possessive Quantoren aus Effizienzgründen, aber sie können natürlich die Bedeutung einer Regex verändern; die Auswirkungen müssen sorgfältig bedacht werden, siehe Atomare Gruppen und possessive Quantoren verwenden.)
Dritte Alternative: Leere Elemente in Kurzschreibweise
Die dritte Alternative ˹<\w[^>]*/>˼ passt auf »Leere Elemente«-Tags vom Typ <br/> oder <img .../>. Solche Tags haben einen Schrägstrich vor der schließenden spitzen Klammer und treten nicht paarweise als Start- und End-Tag auf.
Erste Alternative: Elemente mit öffnenden und schließenden Tags
Zum Schluss die komplizierteste Alternative: ˹<(\w++)[^>]*+(?<!/)>(?1)</\2>˼
Der erste, unterstrichene Teil dieses Unterausdrucks passt auf ein öffnendes Tag, wobei der Name dieses Tags mit ˹(\w++)˼ aufgefangen wird; das ist in der ganzen Regex das zweite einfangende Klammerpaar. (Die Verwendung des possessiven Quantors bei ˹\w++˼ ist wichtig, wir werden darauf zurückkommen.)
˹(?<!/)˼ ist ein Ausdruck für negatives Lookbehind (siehe Lookahead; Lookbehind), der verlangt, dass das gerade erkannte Zeichen vor dem ˹>˼ kein Schrägstrich ist. So stellen wir sicher, dass das gerade gefundene öffnende Tag nicht in Wirklichkeit ein Leeres-Element-Tag vom Typ <hr/> ist, denn diese Tags sollen ausschließlich von der dritten Alternative behandelt werden.
Wenn das Start-Tag gefunden ist, wird mit ˹(?1)˼ der Inhalt der ersten einfangenden Klammer rekursiv angewandt. Das ist die angesprochene »Kern-Regex«, die eigentlich nichts anderes beschreibt als eine Textfolge mit korrekt verschachtelten Tags. Nach dem Inhalt sollten wir jetzt besser das passende End-Tag zum öffnenden finden (den Namen dieses Tags haben wir uns mit dem zweiten einfangenden Klammerpaar gemerkt). Das ˹</˼ am Anfang von ˹</\2>˼ stellt sicher, dass es sich um ein End-Tag handelt. Die Rückwärtsreferenz ˹\2>˼ sorgt außerdem dafür, dass dieses End-Tag das richtige ist, nämlich eins mit dem gleichen Namen wie das Start-Tag. (Wenn die Auszeichnungssprache – wie z.B. HTML – die Groß- und Kleinschreibung bei Tag-Namen ignoriert, würde man ein ˹(?i)˼ am Anfang der Regex einbauen oder den i-Modifikator benutzen.)
Puh!
Possessive Quantoren
Noch ein paar Worte zum Gebrauch des possessiven Quantors bei ˹\w++˼ in der ersten Alternative, ˹<(\w++)[^>]*+(?<!/)>˼. Wenn weder possessive Quantoren noch atomare Gruppen (siehe Atomare Klammern) zur Verfügung stehen würden, hätte ich nach dem (\w+) ein ˹\b˼ eingefügt: ˹<(\w+)\b[^>]*(?<!/)>˼.
Das \b verbietet ein unvollständiges Matching des Tag-Namens durch (\w+)˼, beispielsweise nur das ›li‹ bei einem ›<link>...</li>‹-Tag. Dabei geriete das ›nk‹ außerhalb der einfangenden Klammer, und bei der Rückwärtsreferenz ˹\2˼ würde der falsche Tag-Name verwendet.
Bei wohlgeformten XML-Daten passiert das nicht, denn das \w+ ist gierig und versucht, so viele Wort-Zeichen wie möglich zu finden. Bei falsch verschachtelten Tags hingegen könnte der Backtracking-Vorgang eine Rückgabe von ›nk‹ erzwingen und die Regex würde das fehlerhafte ›<link>...</li>‹ als korrekt erkennen. Mit ˹\b˼ wird das vermieden.
Die preg-Maschine von PHP unterstützt aber possessive Quantoren, und so können wir ˹(\w++)˼ verwenden, was die gleiche Auswirkung hat: Teile von Tag-Namen sind nicht erlaubt.
XML in der Praxis
Im Allgemeinen besitzen XML-Daten ein komplexeres Format als die bisher beschriebenen Tags. Wir müssen außerdem – neben anderem – noch XML-Kommentare, CDATA-Blöcke und XML-Verarbeitungsinstruktionen (processing instructions) berücksichtigen.
Die XML-Kommentare erledigen wir mit einer vierten Alternative, ˹<!–.*?–>;˼; wir müssen aber daran denken, dass der Punkt auf jedes Zeichen passen soll, und entweder ˹(?s)˼ oder den s-Modifikator verwenden.
Ganz ähnlich verfahren wir mit den CDATA-Abschnitten der Form <![CDATA[...]]>; diese werden mit einer weiteren Alternative behandelt: ˹<!\[CDATA\[.*?]]>˼. Ebenso werden die Verarbeitungsinstruktionen wie etwa ›<?xml●version="1.0"?>‹ mit der zusätzlichen Alternative ˹<\?.*?\?>˼ behandelt.
Entity-Deklarationen haben die Form ›<!ENTITY...>‹ und werden mit ˹<!ENTITY\b.*?>˼ abgehandelt. Es gibt noch eine Reihe von ähnlichen XML-Strukturen, die dem gleichen Muster folgen; mit ˹<![A-Z].*?>˼ erfassen wir alle davon.
Es gäbe noch ein paar wenige XML-Feinheiten zu berücksichtigen, aber das bisher Behandelte deckt fast alle XML-Daten ab. Zusammengesetzt erhalten wir das folgende PHP-Programmstück:
$xml_regex = '{
^(
(?: <(\w++) [^>]*+ (?<!/)> (?1) </\2> # Paar von Tags, rekursiv
| [^<>]++ # Text ohne Tags
| <\w[^>]*/> # Leeres-Element-Tag, Kurzform
| <!--.*?--> # Kommentar
| <!\[CDATA\[.*?]]> # CDATA-Block
| <\?.*?\?> # Verarbeitungsinstruktionen (PIs)
| <![A-Z].*?> # Entity-Deklarationen usw.
)*+
)$
}sx';
if (preg_match($xml_regex, $xml_string))
echo "Korrekt verschachtelt.\n";
else
echo "Ungültige Verschachtelung.\n";
Geht das auch mit HTML?
Leider enthält das HTML in der freien Wildbahn sehr oft Dinge, die eine Validierung nach dem obigen Muster unmöglich machen: ungepaarte Tags, falsch verschachtelte Tags und nicht als Entities codierte Zeichen wie ›<‹ und ›>‹. Aber auch sauberes HTML mit korrekt verschachtelten Tags besitzt einige Eigenheiten, die berücksichtigt werden müssen: Kommentare und <script>-Tags.
HTML-Kommentare können auf die gleiche Weise wie bei XML behandelt werden: ˹<!–.*?–>˼ mit dem s-Modifikator.
Die <script>-Elemente sind insofern speziell, als sie »nackte« ›<‹- und ›>‹-Zeichen enthalten dürfen. Wir können das abfangen, indem wir zwischen dem öffnenden <script...> und dem schließenden </script> schlichtweg alle Zeichen erlauben: ˹<script\b[^>]*>.*?</script>˼. Diese Alternative wird nur dann benötigt, wenn im <script>-Element tatsächlich spitze Klammern vorkommen, denn sonst erfüllen sie ja die Forderung der paarigen Tags und werden von der ersten Alternative behandelt.
Hier also die HTML-Version des Programmbeispiels:
$html_regex = '{
^(
(?: <(\w++) [^>]*+ (?<!/)> (?1) </\2> # Paar von Tags, rekursiv
| [^<>]++ # Text ohne Tags
| <\w[^>]*/> # Leeres-Element-Tag, Kurzform
| <!--.*?--> # Kommentar
| <script\b[^>]*>.*?</script> # script-Element
)*+
)$
}isx';
if (preg_match($html_regex, $html_string))
echo "Korrekt verschachtelt.\n";
else
echo "Ungültige Verschachtelung.\n";
Tipp der data2type-Redaktion: Zum Thema Reguläre Ausdrücke bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an: |
Copyright der deutschen Ausgabe © 2008 by O’Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Reguläre Ausdrücke" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
O’Reilly Verlag GmbH & Co. KG, Balthasarstr. 81, 50670 Köln