Eine URL in der Praxis erkennen
(Auszug aus "Reguläre Ausdrücke" von Jeffrey E. F. Friedl)
Bei meiner Arbeit für Yahoo! Finance schrieb ich Programme, die ankommende Finanz- und Börsen-News verarbeiten. Die Artikel werden normalerweise als nackte ASCII-Texte angeliefert, und meine Programme setzen diese in möglichst gut lesbares HTML um. (Wenn Sie in den letzten zehn Jahren auf "http://finance.yahoo.com" waren, haben Sie einen Eindruck gewonnen, ob mir das gelungen ist.)
Das ist nicht immer eine leichte Aufgabe, weil diese ankommenden Texte ziemlich beliebig (oder gar nicht) formatiert sind. Es ist viel schwieriger, in solchen Texten Hostnamen und URLs zu identifizieren, als sie auf syntaktische Korrektheit zu prüfen, wenn sie einmal aus den Texten herausgelöst sind. Im letzten Abschnitt wurde darauf angespielt; hier zeige ich Code, der bei Yahoo! wirklich im Einsatz ist.
Das Programm sucht nach verschiedenen URL-Typen: mailto, http, https und ftp. Wenn wir in einem Text ein ›http://‹ vorfinden, können wir ziemlich sicher sein, dass es sich wirklich um eine URL handelt, und wir suchen mit einem relativ einfachen regulären Ausdruck wie ˹http://[-\w]+(\.\w[-\w]*)+˼ nach dem Hostnamen. Wir wissen, dass es sich um englische Texte in ASCII handelt, daher können wir ziemlich unbesorgt ˹-\w˼ statt ˹[-a-z0-9]˼ verwenden. ˹\w˼ erkennt auch den Unterstrich und bei manchen Systemen sämtliche Unicode-Zeichen, aber das spielt bei den hier vorliegenden Daten keine Rolle.
Dagegen werden Webadressen statt als richtige URLs oft in verkürzter Form ohne http:// oder mailto: angegeben, z.B.:
...visit us at www.oreilly.com or mail to orders@oreilly.com.
In diesen Fällen müssen wir vorsichtiger vorgehen. Bei Yahoo! benutzte ich einen Ausdruck, der dem aus dem letzten Abschnitt ähnelt, sich aber in ein paar Punkten von ihm unterscheidet:
(?i: [a-z0-9] (?:[-a-z0-9]*[a-z0-9])? \. )+ # Subdomains.
# Top-level-Domains -- wir verlangen hier Kleinbuchstaben.
(?-i: com\b
| edu\b | biz\b
| org\b | gov\b
| in(?:t|fo)\b # .int oder .info
| mil\b | net\b
| name\b | museum\b
| coop\b | aero\b
| [a-z][a-z]\b # Zweibuchstabige Ländercodes nach ISO.
)
In dieser Regex werden ˹(?-i:...)˼ und ˹(?i:...)˼ benutzt. In bestimmten Teilen der Regex wird also unter Berücksichtigung der Groß- und Kleinschreibung gesucht, in anderen ohne (siehe unter Kommentare und Modus-Modifikatoren). Wir wollen URLs mit wechselnder Schreibung wie ›www.OReilly.com‹ erkennen, nicht aber ein Börsenkürzel wie ›NT.TO‹ (das Symbol für die Börsenkurse von Nortel Networks an der Börse von Toronto – bei unseren Finanzdaten gibt es solche Abkürzungen in großer Anzahl). Offiziell, nach RFC, darf der letzte Teil eines Hostnamens (z.B. ›.com‹) durchaus auch in Großbuchstaben geschrieben werden, aber in der Praxis ist das ganz selten der Fall, und wir vernachlässigen diese Möglichkeit. Wir müssen hier zwischen dem, was wir erkennen wollen (jede Art von URL), dem, was wir vermeiden wollen (Börsenkürzel), und der Komplexität abwägen. Wir könnten das ˹(?-i:...)˼ auch nur auf die zweibuchstabigen ISO-Ländercodes anwenden, aber in der Praxis sind durchgehend in Großbuchstaben geschriebene URLs selten.
Das Folgende ist ein Programmgerüst zum Auffinden von URLs in Texten, in das Sie die einzelnen Unterausdrücke für den Hostnamen einsetzen können:
\b
# Protokollteil mit Hostname oder nur Hostname
(
# Protokoll-Präfix ftp://, http:// oder https://
(ftp|https?)://[-\w]+(\.\w[-\w]*)+
|
# Wenn ohne Präfix: ein spezifischerer Unterausdruck für den Hostnamen
Volle-Hostnamen-Regex
)
# Optionale Portnummer
( : \d+ )?
# Der Rest der URL ist fakultativ; wenn vorhanden, beginnt er mit einem Slash.
(
/ Pfad-Teil
)?
Mit dem Pfad-Teil (also dem Teil der URL, der nach dem Hostnamen kommt und der in "http://www.oreilly.com/catalog/regex/" unterstrichen ist) haben wir uns noch nicht beschäftigt. Es stellt sich heraus, dass dies der Teil ist, der am schwierigsten zu erkennen ist; hier müssen wir schon ein bisschen raten, damit die Regex in der Praxis etwas taugt. Wie Sie schon unter Erweiterte einführende Beispiele gesehen haben, kommt nicht selten direkt nach der URL Text, der auch zulässigerweise Teil der URL sein könnte.
Bei einem Text wie
Lesen Sie seine Kommentare auf "http://www.oreilly.com/ask_tim/index.html". Er ...
sehen Sie schnell, dass der Punkt nach ›index.html‹ ein Satzpunkt ist und nicht zur URL gehört. Dagegen ist der Punkt innerhalb von ›index.html‹ sehr wohl Teil der URL.
Für einen menschlichen Leser liegt das auf der Hand, für ein Programm ist das sehr viel schwieriger; wir müssen hier heuristisch vorgehen. Beim Beispiel unter Erweiterte einführende Beispiele hatten wir negatives Lookbehind verwendet und so sichergestellt, dass die URL nicht mit einem Satzzeichen endet. Die bei Yahoo! Finance verwendete Lösung hatte ich entwickelt, bevor es negatives Lookbehind gab. Sie ist komplizierter als die Methode von Erweiterte einführende Beispiele, erreicht aber dasselbe. Der Ansatz ist im folgenden Listing wiedergegeben und unterscheidet sich in mehreren Punkten von dem aus dem Beispiel in HTTP-URLs »verlinken«; ein Vergleich lohnt sich. Insbesondere die Java-Version der gleichen Regex im folgenden Kasten "Eine Regex aus Variablen aufbauen, in Java" zeigt, wie die Regex aufgebaut ist.
\b
# Protokollteil mit Hostname oder nur Hostname
(
# Protokoll-Präfix ftp://, http:// oder https://
(ftp|https?)://[-\w]+(\.\w[-\w]*)+
|
# Wenn ohne Präfix: ein spezifischerer Unterausdruck für den Hostnamen
(?i: [a-z0-9] (?:[-a-z0-9]*[a-z0-9])? \. )+ # Subdomains
# Top-level-Domains -- wir verlangen hier Kleinbuchstaben.
(?-i: com\b | edu\b
| biz\b | gov\b
| in(?:t|fo)\b # .int oder .info
| mil\b | net\b
| org\b | [a-z][a-z]\b # Zweibuchstabige Ländercodes nach ISO
)
)
# Optionale Portnummer
( : \d+ )?
# Der Rest der URL ist fakultativ; wenn vorhanden, beginnt er mit einem Slash.
(
/
# Für den Rest gehen wir heuristisch vor: Was in der Praxis funktioniert, ist »korrekt«.
[^!.,?;"fl<>()\[\]{}\s\x7F-\xFF]*
(?:
[!.,?]+ [^!.,?;"fl<>()\[\]{}\s\x7F-\xFF]+
)*
)?
String SubDomain = "(?i:[a-z0-9]|[a-z0-9][-a-z0-9]*[a-z0-9])";
String TopDomains = "(?x-i:com\\b \n" +
" |edu\\b \n" +
" |biz\\b \n" +
" |in(?:t|fo)\\b \n" +
" |mil\\b \n" +
" |net\\b \n" +
" |org\\b \n" +
" |[a-z][a-z]\\b \n" + // Ländercodes
") \n";
String Hostname = "(?:" + SubDomain + "\\.)+" + TopDomains;
String NOT_IN = ";\"'<>()\\[\\]{}\\s\\x7F-\\xFF";
String NOT_END = "!.,?";
String ANYWHERE = "[^" + NOT_IN + NOT_END + "]";
String EMBEDDED = "[" + NOT_END + "]";
String UrlPath = "/"+ANYWHERE + "*("+EMBEDDED+"+"+ANYWHERE+"+)*";
String Url =
"(?x: \n"+
" \\b \n"+
" ## Hostname-Teil erkennen \n"+
" ( \n"+
" (?: ftp | http s? ): // [-\\w]+(\\.\\w[-\\w]*)+ \n"+
" | \n"+
" " + Hostname + " \n"+
" ) \n"+
" # Optionale Portnummer zulassen \n"+
" (?: :\\d+ )? \n"+
" \n"+
" # Rest der URL ist optional und beginnt mit / \n"+
" (?: " + UrlPath + ")? \n"+
")";
// String-Darstellung der Regex in ein Regex-Objekt umwandeln.
Pattern UrlRegex = Pattern.compile(Url);
// Jetzt können wir die Regex auf Textdateien anwenden ...
.
.
.
In Wirklichkeit würde ich kaum eine riesige Monster-Regex wie diese schreiben. Ich würde mir eher eine kleine Sammlung oder Bibliothek von regulären Ausdrücken aufbauen und diese zu einer größeren zusammensetzen. Ein einfaches Beispiel für diese Technik haben wir schon beim Gebrauch der Variablen $HostnameRegex bei Eine Regex-Sammlung benutzt, außerdem im obigen Kasten bei der Java-Version des gleichen Programms.
<< zurück | vor >> |
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