Matchmaking mit regulären Ausdrücken

Wenn Sie in Perl oder einer anderen Sprache mit integrierten Funktionen für reguläre Ausdrücke programmiert haben, wissen Sie wahrscheinlich, wie viel einfacher reguläre Ausdrücke die Textverarbeitung und den Mustervergleich machen. Wenn Sie mit dem Begriff nicht vertraut sind, ist ein regulärer Ausdruck einfach eine Zeichenfolge, die ein Muster definiert, mit dem nach einer passenden Zeichenfolge gesucht wird.

Viele Sprachen, einschließlich Perl, PHP, Python, JavaScript und JScript, unterstützen jetzt reguläre Ausdrücke für die Textverarbeitung, und einige Texteditoren verwenden reguläre Ausdrücke für leistungsstarke Such- und Ersetzungsfunktionen. Was ist mit Java? Zum Zeitpunkt dieses Schreibens wurde eine Java-Spezifikationsanforderung genehmigt, die eine Bibliothek mit regulären Ausdrücken für die Textverarbeitung enthält. Sie können erwarten, dass es in einer zukünftigen Version des JDK angezeigt wird.

Aber was ist, wenn Sie jetzt eine Bibliothek mit regulären Ausdrücken benötigen? Glücklicherweise können Sie die Open-Source-Bibliothek von Jakarta ORO von Apache.org herunterladen. In diesem Artikel werde ich Ihnen zunächst eine kurze Einführung in reguläre Ausdrücke geben und Ihnen dann zeigen, wie Sie reguläre Ausdrücke mit der Open-Source-Jakarta-ORO-API verwenden.

Reguläre Ausdrücke 101

Fangen wir einfach an. Angenommen, Sie möchten nach einer Zeichenfolge mit dem Wort "cat" suchen. Ihr regulärer Ausdruck wäre einfach "Katze". Wenn bei Ihrer Suche die Groß- und Kleinschreibung nicht berücksichtigt wird, stimmen auch die Wörter "Katalog", "Catherine" oder "Anspruchsvoll" überein:

Regulärer Ausdruck: Katze

Streichhölzer: Katze, Katalog, Catherine, anspruchsvoll

Die Periodennotation

Stellen Sie sich vor, Sie spielen Scrabble und benötigen ein Wort aus drei Buchstaben, das mit dem Buchstaben "t" beginnt und mit dem Buchstaben "n" endet. Stellen Sie sich auch vor, Sie haben ein englisches Wörterbuch und durchsuchen den gesamten Inhalt mit einem regulären Ausdruck nach einer Übereinstimmung. Um einen solchen regulären Ausdruck zu bilden, würden Sie eine Platzhalter-Notation verwenden - das Punktzeichen (.). Der reguläre Ausdruck wäre dann "tn" und würde mit "tan", "Ten", "tin" und "ton" übereinstimmen; es würde auch mit "t # n", "tpn" und sogar "t n" sowie vielen anderen unsinnigen Wörtern übereinstimmen. Dies liegt daran, dass das Punktzeichen mit allem übereinstimmt, einschließlich dem Leerzeichen, dem Tabulatorzeichen und sogar Zeilenumbrüchen:

Regulärer Ausdruck: tn

Übereinstimmungen: tan, Ten, tin, ton, tn, t # n, tpn usw.

Die Klammernotation

Um das Problem der wahllosen Übereinstimmungen des Zeitraums zu lösen, können Sie Zeichen angeben, die Sie mit dem Ausdruck in Klammern ("[]") für sinnvoll halten, sodass nur diese Zeichen mit dem regulären Ausdruck übereinstimmen. Somit würde "t [aeio] n" nur mit "tan", "Ten", "tin" und "ton" übereinstimmen. "Toon" würde nicht übereinstimmen, da Sie nur ein einzelnes Zeichen in der Klammernotation abgleichen können:

Regulärer Ausdruck: t [aeio] n

Streichhölzer: Tan, Ten, Tin, Tonne

Der OP-Operator

Wenn Sie zusätzlich zu allen im vorherigen Abschnitt übereinstimmenden Wörtern "toon" zuordnen möchten, können Sie das "|" Notation, die im Grunde ein ODER-Operator ist. Verwenden Sie den regulären Ausdruck "t (a | e | i | o | oo) n", um "toon" zuzuordnen. Sie können die Klammernotation hier nicht verwenden, da sie nur einem einzelnen Zeichen entspricht. Verwenden Sie stattdessen Klammern - "()". Sie können auch Klammern für Gruppierungen verwenden (dazu später mehr):

Regulärer Ausdruck: t (a | e | i | o | oo) n

Streichhölzer: tan, Ten, tin, ton, toon

Die Quantifizierernotationen

Tabelle 1 zeigt die Quantifizierernotationen, mit denen bestimmt wird, wie oft sich eine bestimmte Notation unmittelbar links von der Quantifizierernotation wiederholen soll:

Tabelle 1. Quantifizierernotationen
Notation Anzahl
* * 0 oder mehrmals
+ 1 oder mehrmals
? 0 oder 1 Mal
{n} Genau n Mal
{n, m} n bis m wie oft

Angenommen, Sie möchten in einer Textdatei nach einer Sozialversicherungsnummer suchen. Das Format für US-Sozialversicherungsnummern lautet 999-99-9999. Der reguläre Ausdruck, den Sie verwenden würden, um dies zu erreichen, ist in Abbildung 1 dargestellt. In regulären Ausdrücken hat die Bindestrichnotation ("-") eine besondere Bedeutung. Es gibt einen Bereich an, der mit einer beliebigen Zahl von 0 bis 9 übereinstimmt. Daher müssen Sie das Zeichen "-" mit einem Schrägstrich ("\") maskieren, wenn Sie mit den wörtlichen Bindestrichen in einer Sozialversicherungsnummer übereinstimmen.

Wenn Sie bei Ihrer Suche den Bindestrich optional machen möchten - wenn Sie beispielsweise die Formate 999-99-9999 und 999999999 als akzeptabel betrachten - können Sie das "?" Quantifizierernotation. Abbildung 2 zeigt diesen regulären Ausdruck:

Schauen wir uns ein anderes Beispiel an. Ein Format für US-Autokennzeichen besteht aus vier numerischen Zeichen, gefolgt von zwei Buchstaben. Der reguläre Ausdruck umfasst zuerst den numerischen Teil "[0-9] {4}", gefolgt vom Textteil "[AZ] {2}". Abbildung 3 zeigt den vollständigen regulären Ausdruck:

Die NICHT-Notation

Die Notation "^" wird auch als NOT-Notation bezeichnet. Bei Verwendung in Klammern gibt "^" das Zeichen an, mit dem Sie nicht übereinstimmen möchten. Beispielsweise stimmt der Ausdruck in Abbildung 4 mit allen Wörtern überein

außer

diejenigen, die mit dem Buchstaben X beginnen.

Die Klammern und Leerzeichen

Angenommen, Sie versuchen, den Geburtsmonat aus dem Geburtsdatum einer Person zu extrahieren. Das typische Geburtsdatum hat das folgende Format: 26. Juni 1951. Der reguläre Ausdruck, der mit der Zeichenfolge übereinstimmt, entspricht dem in Abbildung 5:

The new "\s" notation is the space notation and matches all blank spaces, including tabs. If the string matches perfectly, how do you extract the month field? You simply put parentheses around the month field, creating a group, and later retrieve the value using the ORO API (discussed in a following section). The appropriate regular expression is in Figure 6:

Other miscellaneous notations

To make life easier, some shorthand notations for commonly used regular expressions have been created, as shown in Table 2:

Table 2. Commonly used notations
Notation Equivalent Notation
\d [0-9]
\D [^0-9]
\w [A-Z0-9]
\W [^A-Z0-9]
\s [ \t\n\r\f]
\S [^ \t\n\r\f]

To illustrate, we can use "\d" for all instances of "[0-9]" we used before, as was the case with our social security number expressions. The revised regular expression is in Figure 7:

Jakarta-ORO library

Many open source regular expression libraries are available for Java programmers, and many support the Perl 5-compatible regular expression syntax. I use the Jakarta-ORO regular expression library because it is one of the most comprehensive APIs available and is fully compatible with Perl 5 regular expressions. It is also one of the most optimized APIs around.

The Jakarta-ORO library was formerly known as OROMatcher and has been kindly donated to the Jakarta Project by Daniel Savarese. You can download the package from a link in the Resources section below.

The Jakarta-ORO objects

I'll start by briefly describing the objects you need to create and access in order to use this library, and then I will show how you use the Jakarta-ORO API.

The PatternCompiler object

First, create an instance of the Perl5Compiler class and assign it to the PatternCompiler interface object. Perl5Compiler is an implementation of the PatternCompiler interface and lets you compile a regular expression string into a Pattern object used for matching:

 PatternCompiler compiler=new Perl5Compiler(); 

The Pattern object

To compile a regular expression into a

Pattern

object, call the

compile()

method of the compiler object, passing in the regular expression. For example, you can compile the regular expression

"t[aeio]n"

like so:

 Pattern pattern=null; try { pattern=compiler.compile("t[aeio]n"); } catch (MalformedPatternException e) { e.printStackTrace(); } 

By default, the compiler creates a case-sensitive pattern, so that the above setup only matches "tin", "tan", "ten", and "ton", but not "Tin" or "taN". To create a case-insensitive pattern, you would call a compiler with an additional mask:

 pattern=compiler.compile("t[aeio]n",Perl5Compiler.CASE_INSENSITIVE_MASK); 

Once you've created the Pattern object, you can use it for pattern matching with the PatternMatcher class.

The PatternMatcher object

The PatternMatcher object tests for a match based on the Pattern object and a string. You instantiate a Perl5Matcher class and assign it to the PatternMatcher interface. The Perl5Matcher class is an implementation of the PatternMatcher interface and matches patterns based on the Perl 5 regular expression syntax:

 PatternMatcher matcher=new Perl5Matcher(); 

You can obtain a match using the PatternMatcher object in one of several ways, with the string to be matched against the regular expression passed in as the first parameter:

  • boolean matches(String input, Pattern pattern): Used if the input string and the regular expression should match exactly; in other words, the regular expression should totally describe the string input
  • boolean matchesPrefix(String input, Pattern pattern): Used if the regular expression should match the beginning of the input string
  • boolean contains(String input, Pattern pattern): Used if the regular expression should match part of the input string (i.e., should be a substring)

You could also pass in a PatternMatcherInput object instead of a String object to the above three method calls; if you did so, you could continue matching from the point at which the last match was found in the string. This is useful when you have many substrings that are likely to be matched by a given regular expression. The method signatures with the PatternMatcherInput object instead of String are as follows:

  • boolean matches(PatternMatcherInput input, Pattern pattern)
  • boolean matchesPrefix(PatternMatcherInput input, Pattern pattern)
  • boolean contains(PatternMatcherInput input, Pattern pattern)

Scenarios for using the API

Now let's discuss some example uses of the Jakarta-ORO library.

Log file processing

Your job: analyze a Web server log file and determine how long each user spends on the Website. An entry from a typical BEA WebLogic log file looks like this:

172.26.155.241 - - [26/Feb/2001:10:56:03 -0500] "GET /IsAlive.htm HTTP/1.0" 200 15 

After analyzing this entry, you'll realize that you need to extract two things from the log file: the IP address and a page's access time. You can use the grouping notation (parentheses) to extract the IP address field and the timestamp field from the log entry.

Let's first discuss the IP address. It consists of 4 bytes, each with values between 0 and 255; each byte is separated from the others by a period. Thus, in each individual byte in the IP address, you have at least one and at most three digits. You can see the regular expression for this field in Figure 8:

You need to escape the period character because you literally want it to be there; you do not want it read in terms of its special meaning in regular expression syntax, which I explained earlier.

The log entry's timestamp part is surrounded by square brackets. You can extract whatever is within these brackets by first searching for the opening square bracket character ("[") and extracting whatever is not within the closing square bracket character ("]"), continuing until you reach the closing square bracket. Figure 9 shows the regular expression for this:

Now you combine these two regular expressions into a single expression with grouping notation (parentheses) for extraction of your IP address and timestamp. Notice that "\s-\s-\s" is added in the middle so that matching occurs, although you won't extract that. You can see the complete regular expression in Figure 10.

Now that you've formulated this regular expression, you can begin writing Java code using the regular expression library.

Using the Jakarta-ORO library

To begin using the Jakarta-ORO library, first create the regular expression string and the sample string to parse:

 String logEntry="172.26.155.241 - - [26/Feb/2001:10:56:03 -0500] \"GET /IsAlive.htm HTTP/1.0\" 200 15 "; String regexp="([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})\\s-\\s-\\s\\[([^\\]]+)\\]"; 

The regular expression used here is nearly identical to the one found in Figure 10, with only one difference: in Java, you need to escape every forward slash ("\"). Figure 10 is not in Java, so we need to escape the forward-slash character so as not to cause a compilation error. Unfortunately, this process is prone to error and you must do it carefully. You can type in the regular expression first without escaping the forward slashes, and then visually scan the string from left to right and replace every occurrence of the "\" character with "\\". To double check, print out the resulting string to the console.

After initializing the strings, instantiate the PatternCompiler object and create a Pattern object by using the PatternCompiler to compile the regular expression:

PatternCompiler compiler = neuer Perl5Compiler (); Pattern pattern = compiler.compile (regulärer Ausdruck);

Erstellen Sie nun das PatternMatcherObjekt und rufen Sie die contain()Methode in der PatternMatcherSchnittstelle auf, um festzustellen, ob Sie eine Übereinstimmung haben: