Java正則表達式教程 [1]
Regular Expressions of Java Tutorial
譯者序(下載代碼
  正則表達式善于處理文本,對匹配、搜索和替換等操作都有意想不到的作用。正因如此,正則表達式現在是作為程序員七種基本技能之一*,因此學習和使用它在工作中都能達到很高的效率。
  正則表達式應用于程序設計語言中,首次是出現在 Perl 語言,這也讓 Perl 奠定了正則表達式旗手的地位。現在,它已經深入到了所有的程序設計語言中,在程序設計語言中,正則表達式可以說是標準配置了。
  Java 中從 JDK 1.4 開始增加了對正則表達式的支持,至此正則表達式成為了 Java 中的基本類庫,使用時不需要再導入第三方的類庫了。Java 正則表達式的語法來源于象征著正則表達式標準的 Perl 語言,但也不是完全相同的,具體的可以參看 Pattern 類的 API 文檔說明。
  我在一次偶然中發現了位于 java.sun.com 站點上的 Java Tutorial,也在那里看到了關于 Java 的正則表達式教程,感覺它不同于其他的正則表達式教程,文中以大量的匹配實例來進行說明。為了能讓 Java 學習者能更好地使用正則表達式,就將其完整地譯出了。該教程中所介紹的正則表達式應用僅僅是最為簡單的(并沒有完全地涉及到 Pattern 類支持的所有正則表達式語法,也沒有涉及到高級的應用),適合于從未接觸過或者是尚未完全明白正則表達式基礎的學習者。在學習完該教程后,應該對正則表達式有了初步的了解,并能熟練地運用 java.util.regex 包中的關于正則表達式的類庫,為今后學習更高級的正則表達式技術奠定良好的基礎。
  教程中所有的源代碼都在 src 目錄下,可以直接編譯運行。由于當前版本的 Java Tutorial 是基于 JDK 6.0 的,因此其中的示例程序也用到了 JDK 6.0 中的新增類庫,但正則表達式在 JDK 1.4 就已經存在了,為了方便大家使用,改寫了部分的源代碼,源代碼類名中后綴為“V4”的表示用于 JDK 1.4 或以上版本,“V5”的表示用于 JDK 5.0 或以上版本,沒有這些后綴的類在各個版本中均可以正常使用。
  由于譯者的水平和技術能力有限,譯稿雖經多次校對,難免有疏漏之處,敬請大家批評和指正。若有發現不妥之處,請發送郵件至 [email protected],我會在 blog 中進行勘誤,謝謝!

    火龍果頓首!

2008 年 2 月 27 日



   *  這是由《程序員》雜志社評出的,刊登在《程序員》2007 年 3 月刊上。這七種基本技能是:數組,字符串與哈希表、正則表達式、調試、兩門語言、一個開發環境、SQL 語言和編寫軟件的思想。

目錄
     本文介紹如何使用 java.util.regex API 作為正則表達式模式匹配。雖然說這個包中可被接受的語法參數與 Perl 是相似的,但我們并不需要掌握 Perl 的語法知識。本教程將從基礎開始,逐層深入到更多的高級技巧。下面是各章節的主要內容:
0 引言
  粗略地看一下正則表達式,同時也介紹組成 API 的核心類。
1 測試用具
  編寫了一個簡單的應用程序,用于測試正則表達式的模式匹配。
2 字符串
  介紹基本的模式匹配、元字符和引用。
3 字符類
  描述簡單字符類、否定、范圍、并集、交集和差集。
4 預定義字符類
  描述空白字符、字母和數字字符等基本的預定義字符。
5 量詞
  使用貪婪(greedy)、勉強(reluctant)和侵占(possessive)量詞,來匹配指定表達式 X 的次數。
6 捕獲組
  解釋如何把多個字符作為一個單獨的單元進行處理。
7 邊界匹配器
  描述行、單詞和輸入的邊界。
8 Pattern 類的方法
  測試了 Pattern 中一些有用的方法,以及探究一些高級的特性,諸如:帶標記的編譯和使用內嵌標記表達式。
9 Matcher 類的方法
  描述了 Matcher 類中通常使用的方法。
10 PatternSyntaxException 類的方法
  描述了如何檢查一個 PatternSyntaxException 異常。
11 更多的資源
  要了解更多正則表達式,可以參考這一節。
12 問題和練習
  鞏固一下本教程所介紹的正則表達式的基本知識,并附有答案。

  為了區分文檔中的正則表達式和普通字符串,均以\d[abc]{2}的形式表示正則表達式的模式。
0 引言返回目錄
0.1 什么是正則表達式?返回目錄
  正則表達式(regular expressions)是一種描述字符串集的方法,它是以字符串集中各字符串的共有特征為依據的。正則表達式可以用于搜索、編輯或者是操作文本和數據。它超出了 Java 程序設計語言的標準語法,因此有必要去學習特定的語法來構建正則表達式。正則表達式的變化是復雜的,一旦你理解了它們是如何被構造的話,你就能解析或者構建任意的正則表達式了。
  本教程講授 java.util.regex API 所支持的正則表達式語法,以及介紹幾個可運行的例子來說明不同的對象間是如何交互的。在正則表達式的世界中,有不同風格的選擇,比如:grep[2]、Perl、Tcl、Python、PHP 和 awk。java.util.regex API 中的正則表達式語法與 Perl 中的最為相似。
0.2 java.util.regex 包是如何描述正則表達式的?返回目錄
  java.util.regex 包主要由三個類所組成:Pattern、Matcher 和 PatternSyntaxException。
  • Pattern 對象表示一個已編譯的正則表達式。Pattern 類沒有提供公共的構造方法。要構建一個模式,首先必須調用公共的靜態 compile 方法,它將返回一個 Pattern 對象。這個方法接受正則表達式作為第一個參數。本教程的開始部分將教你必需的語法。
  • Matcher 是一個靠著輸入的字符串來解析這個模式和完成匹配操作的對象。與 Pattern 相似,Matcher 也沒有定義公共的構造方法,需要通過調用 Pattern 對象的 matcher 方法來獲得一個 Matcher 對象。
  • PatternSyntaxException 對象是一個未檢查異常,指示了正則表達式中的一個語法錯誤。
  本教程的最后幾節課程會詳細地說明各個類。首當其沖的問題是:必須理解正則表達式是如何被構建的,因此下一節引入了一個簡單的測試用具,重復地用于探究它們的語法。
1 測試用具返回目錄
  這節給出了一個可重用的測試用具 RegexTestHarness.java,用于探究構建 API 所支持的正則表達式。使用
java RegexTestHarness
這個命令來運行,沒有被接受的命令行參數。這個應用會不停地循環執行下去[3],提示用戶輸入正則表達式和字符串。雖然說使用這個測試用具是可選的,但你會發現它用于探究下文所討論的測試用例將更為方便。
import java.io.Console;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class RegexTestHarness {

    public static void main(String[] args) {
        Console console = System.console();
        if (console == null) {
            System.err.println("No console.");
            System.exit(1);
        }
      
        while (true) {
            Pattern pattern = Pattern.compile(console.readLine("%nEnter your regex: "));
            Matcher matcher = pattern.matcher(console.readLine("Enter input string to search: "));
            boolean found = false;
            while (matcher.find()) {
                console.format("I found the text \"%s\" starting at index %d " +
                        "and ending at index %d.%n",
                        matcher.group(), matcher.start(), matcher.end());
                found = true;
            }
            if (!found) {
                console.format("No match found.%n");
            }
        }
    }
}
  在繼續下一節之前,確認開發環境支持必需的包,并保存和編譯這段代碼。
【譯者注】
  由于當前版本的 Java Tutorial 是基于 JDK 6.0 編寫的,上述的測試用具由于使用到 JDK 6.0 中新增的類庫(java.io.Console),所以該用具只能在 JDK 6.0 的環境中編譯運行,由于 Console 訪問操作系統平臺上的控制臺,因此這個測試用具只能在操作系統的字符控制臺中運行,不能運行在 IDE 的控制臺中。
  正則表達式是 JDK 1.4 所增加的類庫,為了兼容 JDK 1.4 和 JDK 5.0 的版本,重新改寫了這個測試用具,讓其能適用于不同的版本。
  JDK 5.0 適用的測試用具(RegexTestHarnessV5.java,該用具可以在 IDE 中執行),建議 JDK 6.0 環境也采用該用具。
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTestHarnessV5 {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.printf("%nEnter your regex: ");
            Pattern pattern = Pattern.compile(scanner.nextLine());
            System.out.printf("Enter input string to search: ");
            Matcher matcher = pattern.matcher(scanner.nextLine());
            boolean found = false;
            while (matcher.find()) {
                System.out.printf(
                        "I found the text \"%s\" starting at index %d and ending at index %d.%n",
                        matcher.group(), matcher.start(), matcher.end()
                    );
                found = true;
            }
            if (!found) {
                System.out.printf("No match found.%n");
            }
        }
    }
}
  JDK 1.4 適用的測試用具(RegexTestHarnessV4.java):
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTestHarnessV4 {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(
                new InputStreamReader(new BufferedInputStream(System.in))
            );
        while (true) {
            System.out.print("\nEnter your regex: ");
            Pattern pattern = Pattern.compile(br.readLine());
            System.out.print("Enter input string to search: ");
            Matcher matcher = pattern.matcher(br.readLine());
            boolean found = false;
            while (matcher.find()) {
                System.out.println("I found the text \"" + matcher.group() +
                        "\" starting at index " + matcher.start() +
                        " and ending at index " + matcher.end() +
                        ".");
                found = true;
            }
            if (!found) {
                System.out.println("No match found.");
            }
        }
    }
}
2 字符串返回目錄
  在大多數的情況下,API所支持模式匹配的基本形式是匹配字符串,如果正則表達式是foo,輸入的字符串也是 foo,這個匹配將會是成功的,因為這兩個字符串是相同的。試著用測試用具來測試一下:
Enter your regex: foo
Enter input string to search: foo
I found the text "foo" starting at index 0 and ending at index 3.
  結果確實是成功的。注意當輸入的字符串是 3 個字符長度的時候,開始的索引是 0,結束的索引是 3。這個是約定俗成的,范圍包括開始的索引,不包括結束的索引,如下圖所示:

圖 1 字符串“foo”的單元格編號和索引值[4]
  字符串中的每一個字符位于其自身的單元格(cell)中,在每個單元格之間有索引指示位。字符串“foo”始于索引 0 處,止于索引 3 處,即使是這些字符它們自己僅占據了 0、1 和 2 號單元格。
  就子序列匹配而言,你會注意到一些重疊,下一次匹配開始索引與前一次匹配的結束索引是相同的:
Enter your regex: foo
Enter input string to search: foofoofoo
I found the text "foo" starting at index 0 and ending at index 3.
I found the text "foo" starting at index 3 and ending at index 6.
I found the text "foo" starting at index 6 and ending at index 9.
2.1 元字符返回目錄
  API 也支持許多可以影響模式匹配的特殊字符。把正則表達式改為cat.并輸入字符串“cats”,輸出如下所示:
Enter your regex: cat.
Enter input string to search: cats
I found the text "cats" starting at index 0 and ending at index 4.
  雖然在輸入的字符串中沒有點(.),但這個匹配仍然是成功的。這是由于點(.)是一個元字符(metacharacters)(被這個匹配翻譯成了具有特殊意義的字符了)。這個例子為什么能匹配成功的原因在于,元字符.指的是“任意字符”。
  API 所支持的元字符有:([{\^-$|}])?*+.

注意:在學習過更多的如何構建正則表達式后,你會碰到這些情況:上面的這些特殊字符不應該被處理為元字符。然而也能夠使用這個清單來檢查一個特殊的字符是否會被認為是元字符。例如,字符 !、@ 和 # 決不會有特殊的意義。

  有兩種方法可以強制將元字符處理成為普通字符:
  1. 在元字符前加上反斜線(\);
  2. 把它放在\Q(引用開始)和\E(引用結束)之間[5]。在使用這種技術時,\Q\E能被放于表達式中的任何位置(假設先出現\Q[6]
3 字符類返回目錄
  如果你曾看過 Pattern 類的說明,會看到一些構建正則表達式的概述。在這一節中你會發現下面的一些表達式:
字符類
[abc] a, b 或 c(簡單類)
[^abc] 除 a, b 或 c 之外的任意字符(取反)
[a-zA-Z] a 到 z,或 A 到 Z,包括(范圍)
[a-d[m-p]] a 到 d,或 m 到 p:[a-dm-p](并集)
[a-z&&[def]] d,e 或 f(交集)
[a-z&&[^bc]] 除 b 和 c 之外的 a 到 z 字符:[ad-z](差集)
[a-z&&[^m-p]] a 到 z,并且不包括 m 到 p:[a-lq-z](差集)
  左邊列指定正則表達式構造,右邊列描述每個構造的匹配的條件。

注意:“字符類(character class)”這個詞中的“類(class)”指的并不是一個 .class 文件。在正則表達式的語義中,字符類是放在方括號里的字符集,指定了一些字符中的一個能被給定的字符串所匹配。

3.1 簡單類(Simple Classes)返回目錄
  字符類最基本的格式是把一些字符放在一對方括號內。例如:正則表達式[bcr]at會匹配“bat”、“cat”或者“rat”,這是由于其定義了一個字符類(接受“b”、“c”或“r”中的一個字符)作為它的首字符。
Enter your regex: [bcr]at
Enter input string to search: bat
I found the text "bat" starting at index 0 and ending at index 3.

Enter your regex: [bcr]at
Enter input string to search: cat
I found the text "cat" starting at index 0 and ending at index 3.

Enter your regex: [bcr]at
Enter input string to search: rat
I found the text "rat" starting at index 0 and ending at index 3.

Enter your regex: [bcr]at
Enter input string to search: hat
No match found.
  在上面的例子中,在第一個字符匹配字符類中所定義字符中的一個時,整個匹配就是成功的。
3.1.1 否定返回目錄
  要匹配除那些列表之外所有的字符時,可以在字符類的開始處加上^元字符,這種就被稱為否定(negation)。
Enter your regex: [^bcr]at
Enter input string to search: bat
No match found.

Enter your regex: [^bcr]at
Enter input string to search: cat
No match found.

Enter your regex: [^bcr]at
Enter input string to search: rat
No match found.

Enter your regex: [^bcr]at
Enter input string to search: hat
I found the text "hat" starting at index 0 and ending at index 3.
  在輸入的字符串中的第一個字符不包含在字符類中所定義字符中的一個時,匹配是成功的。
3.1.2 范圍返回目錄
  有時會想要定義一個包含值范圍的字符類,諸如,“a 到 h”的字母或者是“1 到 5”的數字。指定一個范圍,只要在被匹配的首字符和末字符間插入-元字符,比如:[1-5]或者是[a-h]。也可以在類里每個的邊上放置不同的范圍來提高匹配的可能性,例如:[a-zA-Z]將會匹配 a 到 z(小寫字母)或者 A 到 Z(大寫字母)中的任何一個字符。
  下面是一些范圍和否定的例子:
Enter your regex: [a-c]
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.

Enter your regex: [a-c]
Enter input string to search: b
I found the text "b" starting at index 0 and ending at index 1.

Enter your regex: [a-c]
Enter input string to search: c
I found the text "c" starting at index 0 and ending at index 1.

Enter your regex: [a-c]
Enter input string to search: d
No match found.

Enter your regex: foo[1-5]
Enter input string to search: foo1
I found the text "foo1" starting at index 0 and ending at index 4.

Enter your regex: foo[1-5]
Enter input string to search: foo5
I found the text "foo5" starting at index 0 and ending at index 4.

Enter your regex: foo[1-5]
Enter input string to search: foo6
No match found.

Enter your regex: foo[^1-5]
Enter input string to search: foo1
No match found.

Enter your regex: foo[^1-5]
Enter input string to search: foo6
I found the text "foo6" starting at index 0 and ending at index 4.
3.1.3 并集返回目錄
  可以使用并集(union)來建一個由兩個或兩個以上字符類所組成的單字符類。構建一個并集,只要在一個字符類的邊上嵌套另外一個,比如:[0-4[6-8]],這種奇特方式構建的并集字符類,可以匹配 0,1,2,3,4,6,7,8 這幾個數字。
Enter your regex: [0-4[6-8]]
Enter input string to search: 0
I found the text "0" starting at index 0 and ending at index 1.

Enter your regex: [0-4[6-8]]
Enter input string to search: 5
No match found.

Enter your regex: [0-4[6-8]]
Enter input string to search: 6
I found the text "6" starting at index 0 and ending at index 1.

Enter your regex: [0-4[6-8]]
Enter input string to search: 8
I found the text "8" starting at index 0 and ending at index 1.

Enter your regex: [0-4[6-8]]
Enter input string to search: 9
No match found.
3.1.4 交集返回目錄
  建一個僅僅匹配自身嵌套類中公共部分字符的字符類時,可以像[0-9&&[345]]中那樣使用&&。這種方式構建出來的交集(intersection)簡單字符類,僅僅以匹配兩個字符類中的 3,4,5 共有部分。
Enter your regex: [0-9&&[345]]
Enter input string to search: 3
I found the text "3" starting at index 0 and ending at index 1.

Enter your regex: [0-9&&[345]]
Enter input string to search: 4
I found the text "4" starting at index 0 and ending at index 1.

Enter your regex: [0-9&&[345]]
Enter input string to search: 5
I found the text "5" starting at index 0 and ending at index 1.

Enter your regex: [0-9&&[345]]
Enter input string to search: 2
No match found.

Enter your regex: [0-9&&[345]]
Enter input string to search: 6
No match found.
  下面演示兩個范圍交集的例子:
Enter your regex: [2-8&&[4-6]]
Enter input string to search: 3
No match found.

Enter your regex: [2-8&&[4-6]]
Enter input string to search: 4
I found the text "4" starting at index 0 and ending at index 1.

Enter your regex: [2-8&&[4-6]]
Enter input string to search: 5
I found the text "5" starting at index 0 and ending at index 1.

Enter your regex: [2-8&&[4-6]]
Enter input string to search: 6
I found the text "6" starting at index 0 and ending at index 1.

Enter your regex: [2-8&&[4-6]]
Enter input string to search: 7
No match found.
3.1.5 差集返回目錄
  最后,可以使用差集(subtraction)來否定一個或多個嵌套的字符類,比如:[0-9&&[^345]],這個是構建一個匹配除 3,4,5 之外所有 0 到 9 間數字的簡單字符類。
Enter your regex: [0-9&&[^345]]
Enter input string to search: 2
I found the text "2" starting at index 0 and ending at index 1.

Enter your regex: [0-9&&[^345]]
Enter input string to search: 3
No match found.

Enter your regex: [0-9&&[^345]]
Enter input string to search: 4
No match found.

Enter your regex: [0-9&&[^345]]
Enter input string to search: 5
No match found.

Enter your regex: [0-9&&[^345]]
Enter input string to search: 6
I found the text "6" starting at index 0 and ending at index 1.

Enter your regex: [0-9&&[^345]]
Enter input string to search: 9
I found the text "9" starting at index 0 and ending at index 1.
  到此為止,已經涵蓋了如何建立字符類的部分。在繼續下一節之前,可以試著回想一下那張字符類表
4 預定義字符類返回目錄
  Pattern 的 API 包有許多有用的預定義字符類(predefined character classes),提供了常用正則表達式的簡寫形式。
預定義字符類
. 任何字符(匹配或者不匹配行結束符)
\d 數字字符:[0-9]
\D 非數字字符:[^0-9]
\s 空白字符:[\t\n\x0B\f\r]
\S 非空白字符:[^\s]
\w 單詞字符:[a-zA-Z_0-9]
\W 非單詞字符:[^\w]
  上表中,左列是構造右列字符類的簡寫形式。例如:\d指的是數字范圍(0~9),\w指的是單詞字符(任何大小寫字母、下劃線或者是數字)。無論何時都有可能使用預定義字符類,它可以使代碼更易閱讀,更易從難看的字符類中排除錯誤。
  以反斜線(\)開始的構造稱為轉義構造(escaped constructs)。回顧一下在 字符串 一節中的轉義構造,在那里我們提及了使用反斜線,以及用于引用的\Q\E。在字符串中使用轉義構造,必須在一個反斜線前再增加一個反斜用于字符串的編譯,例如:
private final String REGEX = "\\d";        // 單個數字
  這個例子中\d是正則表達式,另外的那個反斜線是用于代碼編譯所必需的。但是測試用具讀取的表達式,是直接從控制臺中輸入的,因此不需要那個多出來的反斜線。
  下面的例子說明了預字義字符類的用法:
Enter your regex: .
Enter input string to search: @
I found the text "@" starting at index 0 and ending at index 1.

Enter your regex: .
Enter input string to search: 1
I found the text "1" starting at index 0 and ending at index 1.

Enter your regex: .
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.

Enter your regex: \d
Enter input string to search: 1
I found the text "1" starting at index 0 and ending at index 1.

Enter your regex: \d
Enter input string to search: a
No match found.

Enter your regex: \D
Enter input string to search: 1
No match found.

Enter your regex: \D
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.

Enter your regex: \s
Enter input string to search:  
I found the text " " starting at index 0 and ending at index 1.

Enter your regex: \s
Enter input string to search: a
No match found.

Enter your regex: \S
Enter input string to search:  
No match found.

Enter your regex: \S
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.

Enter your regex: \w
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.

Enter your regex: \w
Enter input string to search: !
No match found.

Enter your regex: \W
Enter input string to search: a
No match found.

Enter your regex: \W
Enter input string to search: !
I found the text "!" starting at index 0 and ending at index 1.
  在開始的三個例子中,正則表達式是簡單的,.(“點”元字符)表示“任意字符”,因此,在所有的三個例子(隨意地選取了“@”字符,數字和字母)中都是匹配成功的。在接下來的例子中,都使用了預定義字符類表格中的單個正則表達式構造。你應該可以根據這張表指出前面每個匹配的邏輯:
  \d 匹配數字字符
  \s 匹配空白字符
  \w 匹配單詞字符
  也可以使用意思正好相反的大寫字母:
  \D 匹配非數字字符
  \S 匹配非空白字符
  \W 匹配非單詞字符
5 量詞返回目錄
  這一節我們來看一下貪婪(greedy)、勉強(reluctant)和侵占(possessive)量詞,來匹配指定表達式X的次數。
  量詞(quantifiers)允許指定匹配出現的次數,方便起見,當前 Pattern API 規范下,描述了貪婪、勉強和侵占三種量詞。首先粗略地看一下,量詞X?X??X?+都允許匹配 X 零次或一次,精確地做同樣的事情,但它們之間有著細微的不同之處,在這節結束前會進行說明。
量 詞 種 類 意  義
貪婪 勉強 侵占
X? X?? X?+ 匹配 X 零次或一次
X* X*? X*+ 匹配 X 零次或多次
X+ X+? X++ 匹配 X 一次或多次
X{n} X{n}? X{n}+ 匹配 X n 次
X{n,} X{n,}? X{n,}+ 匹配 X 至少 n 次
X{n,m} X{n,m}? X{n,m}+ 匹配 X 至少 n 次,但不多于 m 次
  那我們現在就從貪婪量詞開始,構建三個不同的正則表達式:字母a后面跟著?*+。接下來看一下,用這些表達式來測試輸入的字符串是空字符串時會發生些什么:
Enter your regex: a?
Enter input string to search: 
I found the text "" starting at index 0 and ending at index 0.

Enter your regex: a*
Enter input string to search: 
I found the text "" starting at index 0 and ending at index 0.

Enter your regex: a+
Enter input string to search: 
No match found.
5.1 零長度匹配返回目錄
  在上面的例子中,開始的兩個匹配是成功的,這是因為表達式a?a*都允許字符出現零次。就目前而言,這個例子不像其他的,也許你注意到了開始和結束的索引都是 0。輸入的空字符串沒有長度,因此該測試簡單地在索引 0 上匹配什么都沒有,諸如此類的匹配稱之為零長度匹配(zero-length matches)。零長度匹配會出現在以下幾種情況:輸入空的字符串、在輸入字符串的開始處、在輸入字符串最后字符的后面,或者是輸入字符串中任意兩個字符之間。由于它們開始和結束的位置有著相同的索引,因此零長度匹配是容易被發現的。
  我們來看一下關于零長度匹配更多的例子。把輸入的字符串改為單個字符“a”,你會注意到一些有意思的事情:
Enter your regex: a?
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.

Enter your regex: a*
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.

Enter your regex: a+
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
  所有的三個量詞都是用來尋找字母“a”的,但是前面兩個在索引 1 處找到了零長度匹配,也就是說,在輸入字符串最后一個字符的后面。回想一下,匹配把字符“a”看作是位于索引 0 和索引 1 之間的單元格中,并且測試用具一直循環下去直到不再有匹配為止。依賴于所使用的量詞不同,最后字符后面的索引“什么也沒有”的存在可以或者不可以觸發一個匹配。
  現在把輸入的字符串改為一行 5 個“a”時,會得到下面的結果:
Enter your regex: a?
Enter input string to search: aaaaa
I found the text "a" starting at index 0 and ending at index 1.
I found the text "a" starting at index 1 and ending at index 2.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "a" starting at index 3 and ending at index 4.
I found the text "a" starting at index 4 and ending at index 5.
I found the text "" starting at index 5 and ending at index 5.

Enter your regex: a*
Enter input string to search: aaaaa
I found the text "aaaaa" starting at index 0 and ending at index 5.
I found the text "" starting at index 5 and ending at index 5.

Enter your regex: a+
Enter input string to search: aaaaa
I found the text "aaaaa" starting at index 0 and ending at index 5.
  在“a”出現零次或一次時,表達式a?尋找到所匹配的每一個字符。表達式a*找到了兩個單獨的匹配:第一次匹配到所有的字母“a”,然后是匹配到最后一個字符后面的索引 5。最后,a+匹配了所有出現的字母“a”,忽略了在最后索引處“什么都沒有”的存在。
  在這里,你也許會感到疑惑,開始的兩個量詞在遇到除了“a”的字母時會有什么結果。例如,在“ababaaaab”中遇到了字母“b”會發生什么呢?
  下面我們來看一下:
Enter your regex: a?
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "" starting at index 3 and ending at index 3.
I found the text "a" starting at index 4 and ending at index 5.
I found the text "a" starting at index 5 and ending at index 6.
I found the text "a" starting at index 6 and ending at index 7.
I found the text "a" starting at index 7 and ending at index 8.
I found the text "" starting at index 8 and ending at index 8.
I found the text "" starting at index 9 and ending at index 9.

Enter your regex: a*
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "" starting at index 3 and ending at index 3.
I found the text "aaaa" starting at index 4 and ending at index 8.
I found the text "" starting at index 8 and ending at index 8.
I found the text "" starting at index 9 and ending at index 9.

Enter your regex: a+
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "aaaa" starting at index 4 and ending at index 8.
  即使字母“b”在單元格 1、3、8 中出現,但在這些位置上的輸出報告了零長度匹配。正則表達式a?不是特意地去尋找字母“b”,它僅僅是去找字母“a”存在或者其中缺少的。如果量詞允許匹配“a”零次,任何輸入的字符不是“a”時將會作為零長度匹配。在前面的例子中,根據討論的規則保證了 a 被匹配。
  對于要精確地匹配一個模式 n 次時,可以簡單地在一對花括號內指定一個數值:
Enter your regex: a{3}
Enter input string to search: aa
No match found.

Enter your regex: a{3}
Enter input string to search: aaa
I found the text "aaa" starting at index 0 and ending at index 3.

Enter your regex: a{3}
Enter input string to search: aaaa
I found the text "aaa" starting at index 0 and ending at index 3.
  這里,正則表確定式a{3}在一行中尋找連續出現三次的字母“a”。第一次測試失敗的原由在于,輸入的字符串沒有足夠的 a 用來匹配;第二次測試輸出的字符串正好包括了三個“a”,觸發了一次匹配;第三次測試也觸發了一次匹配,這是由于在輸出的字符串的開始部分正好有三個“a”。接下來的事情與第一次的匹配是不相關的,如果這個模式將在這一點后繼續出現,那它將會觸發接下來的匹配:
Enter your regex: a{3}
Enter input string to search: aaaaaaaaa
I found the text "aaa" starting at index 0 and ending at index 3.
I found the text "aaa" starting at index 3 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.
  對于需要一個模式出現至少 n 次時,可以在這個數字后面加上一個逗號(,):
Enter your regex: a{3,}
Enter input string to search: aaaaaaaaa
I found the text "aaaaaaaaa" starting at index 0 and ending at index 9.
  輸入一樣的字符串,這次測試僅僅找到了一個匹配,這是由于一個中有九個“a”滿足了“至少”三個“a”的要求。
  最后,對于指定出現次數的上限,可以在花括號添加第二個數字。
Enter your regex: a{3,6} // 尋找一行中至少連續出現 3 個(但不多于 6 個)“a”
Enter input string to search: aaaaaaaaa
I found the text "aaaaaa" starting at index 0 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.
  這里,第一次匹配在 6 個字符的上限時被迫終止了。第二個匹配包含了剩余的三個 a(這是匹配所允許最小的字符個數)。如果輸入的字符串再少掉一個字母,這時將不會有第二個匹配,之后僅剩余兩個 a。
5.2 捕獲組和字符類中的量詞返回目錄
  到目前為止,僅僅測試了輸入的字符串包括一個字符的量詞。實際上,量詞僅僅可能附在一個字符后面一次,因此正則表達式abc+的意思就是“a 后面接著 b,再接著一次或者多次的 c”,它的意思并不是指abc一次或者多次。然而,量詞也可能附在字符類和捕獲組的后面,比如,[abc]+表示一次或者多次的 a 或 b 或 c,(abc)+表示一次或者多次的“abc”組。
  我們來指定(dog)組在一行中三次進行說明。
Enter your regex: (dog){3}
Enter input string to search: dogdogdogdogdogdog
I found the text "dogdogdog" starting at index 0 and ending at index 9.
I found the text "dogdogdog" starting at index 9 and ending at index 18.

Enter your regex: dog{3}
Enter input string to search: dogdogdogdogdogdog
No match found.
  上面的第一個例子找到了三個匹配,這是由于量詞用在了整個捕獲組上。然而,把圓括號去掉,這時的量詞{3}現在僅用在了字母“g”上,從而導致這個匹配失敗。
  類似地,也能把量詞應用于整個字符類:
Enter your regex: [abc]{3}
Enter input string to search: abccabaaaccbbbc
I found the text "abc" starting at index 0 and ending at index 3.
I found the text "cab" starting at index 3 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.
I found the text "ccb" starting at index 9 and ending at index 12.
I found the text "bbc" starting at index 12 and ending at index 15.

Enter your regex: abc{3}
Enter input string to search: abccabaaaccbbbc
No match found.
  上面的第一個例子中,量詞{3}應用在了整個字符類上,但是第二個例子這個量詞僅用在字母“c”上。
5.3 貪婪、勉強和侵占量詞間的不同返回目錄
  在貪婪、勉強和侵占三個量詞間有著細微的不同。
  貪婪量詞之所以稱之為“貪婪的”,這是由于它們強迫匹配器讀入(或者稱之為吃掉)整個輸入的字符串,來優先嘗試第一次匹配,如果第一次嘗試匹配(對于整個輸入的字符串)失敗,匹配器會通過回退整個字符串的一個字符再一次進行嘗試,不斷地進行處理直到找到一個匹配,或者左邊沒有更多的字符來用于回退了。賴于在表達式中使用的量詞,最終它將嘗試地靠著 1 或 0 個字符的匹配。
  但是,勉強量詞采用相反的途徑:從輸入字符串的開始處開始,因此每次勉強地吞噬一個字符來尋找匹配,最終它們會嘗試整個輸入的字符串。
  最后,侵占量詞始終是吞掉整個輸入的字符串,嘗試著一次(僅有一次)匹配。不像貪婪量詞那樣,侵占量詞絕不會回退,即使這樣做是允許全部的匹配成功。
  為了說明一下,看看輸入的字符串是 xfooxxxxxxfoo 時。
Enter your regex: .*foo  // 貪婪量詞
Enter input string to search: xfooxxxxxxfoo
I found the text "xfooxxxxxxfoo" starting at index 0 and ending at index 13.

Enter your regex: .*?foo  // 勉強量詞
Enter input string to search: xfooxxxxxxfoo
I found the text "xfoo" starting at index 0 and ending at index 4.
I found the text "xxxxxxfoo" starting at index 4 and ending at index 13.

Enter your regex: .*+foo // 侵占量詞
Enter input string to search: xfooxxxxxxfoo
No match found.
  第一個例子使用貪婪量詞.*,尋找緊跟著字母“f”“o”“o”的“任何東西”零次或者多次。由于量詞是貪婪的,表達式的.*部分第一次“吃掉”整個輸入的字符串。在這一點,全部表達式不能成功地進行匹配,這是由于最后三個字母(“f”“o”“o”)已經被消耗掉了。那么匹配器會慢慢地每次回退一個字母,直到返還的“foo”在最右邊出現,這時匹配成功并且搜索終止。
  然而,第二個例子采用勉強量詞,因此通過首次消耗“什么也沒有”作為開始。由于“foo”并沒有出現在字符串的開始,它被強迫吞掉第一個字母(“x”),在 0 和 4 處觸發了第一個匹配。測試用具會繼續處理,直到輸入的字符串耗盡為止。在 4 和 13 找到了另外一個匹配。
  第三個例子的量詞是侵占,所以在尋找匹配時失敗了。在這種情況下,整個輸入的字符串被.*+消耗了,什么都沒有剩下來滿足表達式末尾的“foo”。
  你可以在想抓取所有的東西,且決不回退的情況下使用侵占量詞,在這種匹配不是立即被發現的情況下,它將會優于等價的貪婪量詞。
6 捕獲組返回目錄
  在上一節中,學習了每次如何把量詞放在一個字符、字符類或者捕獲組中。到目前為止,還沒有詳細地討論過捕獲組的概念。
  捕獲組(capturing group)是將多個字符作為單獨的單元來對待的一種方式。構建它們可以通過把字符放在一對圓括號中而成為一組。例如,正則表達式(dog)建了單個的組,包括字符“d”“o”和“g”。匹配捕獲組輸入的字符串部分將會存放于內存中,稍后通過反向引用再次調用。(在 6.2 節 中將會討論反向引用)
6.1 編號方式返回目錄
  在 Pattern 的 API 描述中,捕獲組通過從左至右計算開始的圓括號進行編號。例如,在表達式((A)(B(C)))中,有下面的四組:
  1. ((A)(B(C)))
  2. (A)
  3. (B(C))
  4. (C)
  要找出當前的表達式中有多少組,通過調用 Matcher 對象的 groupCount 方法。groupCount 方法返回 int 類型值,表示當前 Matcher 模式中捕獲組的數量。例如,groupCount 返回 4 時,表示模式中包含有 4 個捕獲組。
  有一個特別的組——組 0,它表示整個表達式。這個組不包括在 groupCount 的報告范圍內。以(?開始的組是純粹的非捕獲組(non-capturing group),它不捕獲文本,也不作為組總數而計數。(可以看 8 Pattern 類的方法 一節中非捕獲組的例子。)
  Matcher 中的一些方法,可以指定 int 類型的特定組號作為參數,因此理解組是如何編號的是尤為重要的。
  :返回之前的匹配操作期間,給定組所捕獲的子序列的初始索引。
  :返回之前的匹配操作期間,給定組所捕獲子序列的最后字符索引加 1。
  :返回之前的匹配操作期間,通過給定組而捕獲的輸入子序列。
6.2 反向引用返回目錄
  匹配輸入字符串的捕獲組部分會存放在內存中,通過反向引用(backreferences)稍后再調用。在正則表達式中,反向引用使用反斜線(\)后跟一個表示需要再調用組號的數字來表示。例如,表達式(\d\d)定義了匹配一行中的兩個數字的捕獲組,通過反向引用\1,表達式稍候會被再次調用。
  匹配兩個數字,且后面跟著兩個完全相同的數字時,就可以使用(\d\d)\1作為正則表達式:
Enter your regex: (\d\d)\1
Enter input string to search: 1212
I found the text "1212" starting at index 0 and ending at index 4.
  如果更改最后的兩個數字,這時匹配就會失敗:
Enter your regex: (\d\d)\1
Enter input string to search: 1234
No match found.
  對于嵌套的捕獲組而言,反向引用采用完全相同的方式進行工作,即指定一個反斜線加上需要被再次調用的組號。
7 邊界匹配器返回目錄
  就目前而言,我們的興趣在于指定輸入字符串中某些位置是否有匹配,還沒有考慮到字符串的匹配產生在什么地方。
  通過指定一些邊界匹配器(boundary matchers)的信息,可以使模式匹配更為精確。比如說你對某個特定的單詞感興趣,并且它只出現在行首或者是行尾時。又或者你想知道匹配發生在單詞邊界(word boundary),或者是上一個匹配的尾部。
  下表中列出了所有的邊界匹配器及其說明。
邊界匹配器
^ 行首
$ 行尾
\b 單詞邊界
\B 非單詞邊界
\A 輸入的開頭
\G 上一個匹配的結尾
\Z 輸入的結尾,僅用于最后的結束符(如果有的話)
\z 輸入的結尾
  接下來的例子中,說明了^$邊界匹配器的用法。注意上表中,^匹配行首,$匹配行尾。
Enter your regex: ^dog$
Enter input string to search: dog
I found the text "dog" starting at index 0 and ending at index 3.

Enter your regex: ^dog$
Enter input string to search:       dog
No match found.

Enter your regex: \s*dog$
Enter input string to search:             dog
I found the text "            dog" starting at index 0 and ending at index 15.

Enter your regex: ^dog\w*
Enter input string to search: dogblahblah
I found the text "dogblahblah" starting at index 0 and ending at index 11.
  第一個例子的匹配是成功的,這是因為模式占據了整個輸入的字符串。第二個例子失敗了,是由于輸入的字符串在開始部分包含了額外的空格。第三個例子指定的表達式是不限的空格,后跟著在行尾的 dog。第四個例子,需要 dog 放在行首,后面跟的是不限數量的單詞字符。
  對于檢查一個單詞開始和結束的邊界模式(用于長字符串里子字符串),這時可以在兩邊使用\b,例如\bdog\b
Enter your regex: \bdog\b
Enter input string to search: The dog plays in the yard.
I found the text "dog" starting at index 4 and ending at index 7.

Enter your regex: \bdog\b
Enter input string to search: The doggie plays in the yard.
No match found.
  對于匹配非單詞邊界的表達式,可以使用\B來代替:
Enter your regex: \bdog\B
Enter input string to search: The dog plays in the yard.
No match found.

Enter your regex: \bdog\B
Enter input string to search: The doggie plays in the yard.
I found the text "dog" starting at index 4 and ending at index 7.
  對于需要匹配僅出現在前一個匹配的結尾,可以使用\G
Enter your regex: dog
Enter input string to search: dog dog
I found the text "dog" starting at index 0 and ending at index 3.
I found the text "dog" starting at index 4 and ending at index 7.

Enter your regex: \Gdog
Enter input string to search: dog dog
I found the text "dog" starting at index 0 and ending at index 3.
  這里的第二個例子僅找到了一個匹配,這是由于第二次出現的“dog”不是在前一個匹配結尾的開始。[7]
8 Pattern 類的方法返回目錄
  到目前為止,僅使用測試用具來建立最基本的 Pattern 對象。在這一節中,我們將探討一些諸如使用標志構建模式、使用內嵌標志表達式等高級的技術。同時也探討了一些目前還沒有討論過的其他有用的方法。
8.1 使用標志構建模式返回目錄
  Pattern 類定義了備用的 compile 方法,用于接受影響模式匹配方式的標志集。標志參數是一個位掩碼,可以是下面公共靜態字段中的任意一個:
Pattern.CANON_EQ
  啟用規范等價。在指定此標志后,當且僅當在其完整的規范分解匹配時,兩個字符被視為匹配。例如,表達式a\u030A[8]在指定此標志后,將匹配字符串“\u00E5”(即字符 å)。默認情況下,匹配不會采用規范等價。指定此標志可能會對性能會有一定的影響。
Pattern.CASE_INSENSITIVE
  啟用不區分大小寫匹配。默認情況下,僅匹配 US-ASCII 字符集中的字符。Unicode 感知(Unicode-aware)的不區分大小寫匹配,可以通過指定 UNICODE_CASE 標志連同此標志來啟用。不區分大小寫匹配也能通過內嵌標志表達式(?i)來啟用。指定此標志可能會對性能會有一定的影響。
Pattern.COMMENTS
  模式中允許存在空白和注釋。在這種模式下,空白和以#開始的直到行尾的內嵌注釋會被忽略。注釋模式也能通過內嵌標志表達式(?x)來啟用。
Pattern.DOTALL
  啟用 dotall 模式。在 dotall 模式下,表達式.匹配包括行結束符在內的任意字符。默認情況下,表達式不會匹配行結束符。dotall 模式也通過內嵌標志表達式(?x)來啟用。[s 是“單行(single-line)”模式的助記符,與 Perl 中的相同。]
Pattern.LITERAL
  啟用模式的字面分析。指定該標志后,指定模式的輸入字符串作為字面上的字符序列來對待。輸入序列中的元字符和轉義字符不具有特殊的意義了。CASE_INSENSITIVE 和 UNICODE_CASE 與此標志一起使用時,會對匹配產生一定的影響。其他的標志就變得多余了。啟用字面分析沒有內嵌標志表達式。
Pattern.MULTILINE
  啟用多行(multiline)模式。在多行模式下,表達式^$分別匹配輸入序列行結束符前面和行結束符的前面。默認情況下,表達式僅匹配整個輸入序列的開始和結尾。多行模式也能通過內嵌標志表達式(?m)來啟用。
Pattern.UNICODE_CASE
  啟用可折疊感知 Unicode(Unicode-aware case folding)大小寫。在指定此標志后,需要通過 CASE_INSENSITIVE 標志來啟用,不區分大小寫區配將在 Unicode 標準的意義上來完成。默認情況下,不區分大小寫匹配僅匹配 US-ASCII 字符集中的字符。可折疊感知 Unicode 大小寫也能通過內嵌標志表達式(?u)來啟用。指定此標志可能會對性能會有一定的影響。
Pattern.UNIX_LINES
  啟用 Unix 行模式。在這種模式下,.^$的行為僅識別“\n”的行結束符。Unix 行模式可以通過內嵌標志表達式(?d)來啟用。
  接下來,將修改測試用具 RegexTestHarness.java,用于構建不區分大小寫匹配的模式。
  首先,修改代碼去調用 complie 的另外一個備用的方法:
Pattern pattern = Pattern.compile(
        console.readLine("%nEnter your regex: "),
        Pttern.CASE_INSENSITIVE
    );
  編譯并運行這個測試用具,會得出下面的結果:
Enter your regex: dog
Enter input string to search: DoGDOg
I found the text "DoG" starting at index 0 and ending at index 3.
I found the text "DOg" starting at index 3 and ending at index 6.
  正如你所看到的,不管是否大小寫,字符串字面上是“dog”的都產生了匹配。使用多個標志來編譯一個模式,使用按位或操作符“|”分隔各個標志。為了更清晰地說明,下面的示例代碼使用硬編碼(hardcode)的方式,來取代控制臺中的讀取:
pattern = Pattern.compile("[az]$", Pattern.MULTILINE | Pattern.UNIX_LINES);
  也可以使用一個 int 類型的變量來代替:
final int flags = Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
Pattern pattern = Pattern.compile("aa", flags);
8.2 內嵌標志表達式返回目錄
  使用內嵌標志表達式(embedded flag expressions)也可以啟用不同的標志。對于兩個參數的 compile 方法,內嵌標志表達式是可選的,因為它在自身的正則表達式中被指定了。下面的例子使用最初的測試用具(RegexTestHarness.java),使用內嵌標志表達式(?i)來啟用不區分大小寫的匹配。
Enter your regex: (?i)foo
Enter input string to search: FOOfooFoOfoO
I found the text "FOO" starting at index 0 and ending at index 3.
I found the text "foo" starting at index 3 and ending at index 6.
I found the text "FoO" starting at index 6 and ending at index 9.
I found the text "foO" starting at index 9 and ending at index 12.
  所有匹配無關大小寫都一次次地成功了。
  內嵌標志表達式所對應 Pattern 的公用的訪問字段表示如下表:
常  量 等價的內嵌標志表達式
Pattern.CANON_EQ 沒有
Pattern.CASE_INSENSITIVE (?i)
Pattern.COMMENTS (?x)
Pattern.MULTILINE (?m)
Pattern.DOTALL (?s)
Pattern.LITERAL 沒有
Pattern.UNICODE_CASE (?u)
Pattern.UNIX_LINES (?d)
8.3 使用 matches(String, CharSequence) 方法返回目錄
  Pattern 類定義了一個方便的 matches 方法,用于快速地檢查模式是否表示給定的輸入字符串。與使用所有的公共靜態方法一樣,應該通過它的類名來調用 matches 方法,諸如 Pattern.matches("\\d","1");。這個例子中,方法返回 true,這是由于數字“1”匹配了正則表達式\d
8.4 使用 split(String) 方法返回目錄
  split 方法是一個重要的工具,用于收集依賴于被匹配的模式任一邊的文本。如下面的 SplitDemo.java 所示,split 方法能從“one:two:three:four:five”字符串中解析出“one two three four five”單詞:
import java.util.regex.Pattern;

public class SplitDemo {

    private static final String REGEX = ":";
    private static final String INPUT = "one:two:three:four:five";
    
    public static void main(String[] args) {
        Pattern p = Pattern.compile(REGEX);
        String[] items = p.split(INPUT);
        for(String s : items) {
            System.out.println(s);
        }
    }
}
  輸出:
one
two
three
four
five
  簡而言之,已經使用冒號(:)取代了復雜的正則表達式匹配字符串文字。以后仍會使用 Pattern 和 Matcher 對象,也能使用 split 得到位于任意正則表達式各邊的文本。下面的 SplitDemo2.java 是個一樣的例子,使用數字作為 split 的參數:
import java.util.regex.Pattern;

public class SplitDemo2 {

    private static final String REGEX = "\\d";
    private static final String INPUT = "one9two4three7four1five";

    public static void main(String[] args) {
        Pattern p = Pattern.compile(REGEX);
        String[] items = p.split(INPUT);
        for(String s : items) {
            System.out.println(s);
        }
    }
}
  輸出:
one
two
three
four
five
8.5 其他有用的方法返回目錄
  你可以從下面的方法中找到比較好用的方法:
  [9]:返回指定字符串字面模式的字符串。此方法會產生一個字符串,能被用于構建一個與字符串 s 匹配的 Pattern,好像它是一個字面上的模式。輸入序列中的元字符和轉義序列將沒有特殊的意義了。
  :返回這個模式的字符串表現形式。這是一個編譯過的模式中的正則表達式。
8.6 在 java.lang.String 中等價的 Pattern 方法返回目錄
  java.lang.String 通過模擬 java.util.regex.Pattern 行為的幾個方法,也可以支持正則表達式。方便起見,下面主要摘錄了出現在 API 關鍵的方法。
  :告知字符串是否匹配給定的正則表達式。調用 str.matches(regex)方法所產生的結果與作為表達式的 Pattern.matches(regex, str)的結果是完全一致。
  :依照匹配給定的正則表達式來拆分字符串。調用 str.split(regex, n)方法所產生的結果與作為表達式的 Pattern.compile(regex).split(str, n) 的結果完全一致。
  :依照匹配給定的正則表達式來拆分字符串。這個方法與調用兩個參數的 split 方法是相同的,第一個參數使用給定的表達式,第二個參數限制為 0。在結果數組中不包括尾部的空字符串。
  還有一個替換方法,把一個 CharSequence 替換成另外一個:
  :將字符串中每一個匹配替換匹配字面目標序列的子字符串,替換成指定的字面替換序列。這個替換從字符串的開始處理直至結束,例如,把字符串“aaa”中的“aa”替換成“b”,結果是“ba”,而不是“ab”。
9 Matcher 類的方法返回目錄
  在這一節中來看看 Matcher 類中其他一些有用的方法。方便起見,下面列出的方法是按照功能來分組的。
索引方法
  索引方法(index methods)提供了一些正好在輸入字符串中發現匹配的索引值:
  :返回之前匹配的開始索引。
  :返回之前匹配操作中通過給定組所捕獲序列的開始索引。
  : 返回最后匹配字符后的偏移量。
  : 返回之前匹配操作中通過給定組所捕獲序列的最后字符之后的偏移量。
研究方法
  研究方法(study methods)回顧輸入的字符串,并且返回一個用于指示是否找到模式的布爾值。
  : 嘗試從區域開頭處開始,輸入序列與該模式匹配。
  : 嘗試地尋找輸入序列中,匹配模式的下一個子序列。
  : 重置匹配器,然后從指定的索引處開始,嘗試地尋找輸入序列中,匹配模式的下一個子序列。
  : 嘗試將整個區域與模式進行匹配
替換方法
  替換方法(replacement methods)用于在輸入的字符串中替換文本有用處的方法。
  :實現非結尾處的增加和替換操作。
  :實現結尾處的增加和替換操作。
  :使用給定的替換字符串來替換輸入序列中匹配模式的每一個子序列。
  :使用給定的替換字符串來替換輸入序列中匹配模式的第一個子序列。
  :返回指定字符串的字面值來替換字符串。這個方法會生成一個字符串,用作 Matcher 的 appendReplacement 方法中的字面值替換 s。所產生的字符串將與作為字面值序列的 s 中的字符序列匹配。斜線(\)和美元符號($)將不再有特殊意義了。
9.1 使用 start 和 end 方法返回目錄
  示例程序 MatcherDemo.java 用于計算輸入序列中單詞“dog”的出現次數。
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class MatcherDemo {

    private static final String REGEX = "\\bdog\\b";
    private static final String INPUT = "dog dog dog doggie dogg";

    public static void main(String[] args) {
       Pattern p = Pattern.compile(REGEX);
       Matcher m = p.matcher(INPUT);        // 獲得匹配器對象
       int count = 0;
       while (m.find()) {
           count++;
           System.out.println("Match number " + count);
           System.out.println("start(): " + m.start());
           System.out.println("end(): " + m.end());
       }
    }
}
  輸出:
Match number 1
start(): 0
end(): 3
Match number 2
start(): 4
end(): 7
Match number 3
start(): 8
end(): 11
  可以看出,這個例子使用了單詞邊界,用于確保更長單詞中的字母“d”“o”“g”就不是子串了。它也輸出了一些有用的信息,在輸入的字符串中什么地方有匹配。start 方法返回在以前的匹配操作期間,由給定組所捕獲子序列的開始處索引,end 方法返回匹配到最后一個字符索引加 1。
9.2 使用 matches 和 lookingAt 方法返回目錄
  matches 和 lookingAt 方法都是嘗試該模式匹配輸入序列。然而不同的是,matches 要求匹配整個輸入字符串,而 lookingAt 不是這樣。這兩個方法都是從輸入字符串的開頭開始的。下面是 MatchesLooking.java 完整的代碼:
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class MatchesLooking {

    private static final String REGEX = "foo";
    private static final String INPUT = "fooooooooooooooooo";
    private static Pattern pattern;
    private static Matcher matcher;

    public static void main(String[] args) {
   
        // 初始化
        pattern = Pattern.compile(REGEX);
        matcher = pattern.matcher(INPUT);

        System.out.println("Current REGEX is: " + REGEX);
        System.out.println("Current INPUT is: " + INPUT);

        System.out.println("lookingAt(): " + matcher.lookingAt());
        System.out.println("matches(): " + matcher.matches());
    }
}
  輸出:
Current REGEX is: foo
Current INPUT is: fooooooooooooooooo
lookingAt(): true
matches(): false
9.3 使用 replaceFirst(String) 和 replaceAll(String) 方法返回目錄
  replaceFirst 和 replaceAll 方法替換匹配給定正則表達式的文本。從它們的名字可以看出,replaceFirst 替換第一個匹配到的,而 replaceAll 替換所有匹配的。下面是 ReplaceDemo.java 的代碼:
import java.util.regex.Pattern; 
import java.util.regex.Matcher;

public class ReplaceDemo {
 
    private static String REGEX = "dog";
    private static String INPUT = "The dog says meow. All dogs say meow.";
    private static String REPLACE = "cat";
 
    public static void main(String[] args) {
        Pattern p = Pattern.compile(REGEX);
        Matcher m = p.matcher(INPUT);       // 獲得匹配器對象
        INPUT = m.replaceAll(REPLACE);
        System.out.println(INPUT);
    }
}
  輸出:
The cat says meow. All cats say meow.
  在上面的例子中,所有的 dog 都被替換成了 cat。但是為什么在這里停下來了呢?你可以替換匹配任何正則表達式的文本,這樣優于替換一個簡單的像 dog 一樣的文字。這個方法的 API 描述了“給定正則表達式a*b,在輸入‘aabfooaabfooabfoob’和替換的字符串是‘-’情況下,表達式的匹配器調用方法后,會產生成字符串‘-foo-foo-foo-’。”
  下面是 ReplaceDemo2.java 的代碼:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
 
public class ReplaceDemo2 {
 
    private static String REGEX = "a*b";
    private static String INPUT = "aabfooaabfooabfoob";
    private static String REPLACE = "-";
 
    public static void main(String[] args) {
        Pattern p = Pattern.compile(REGEX);
        Matcher m = p.matcher(INPUT);       // 獲得匹配器對象
        INPUT = m.replaceAll(REPLACE);
        System.out.println(INPUT);
    }
}
  輸出:
-foo-foo-foo-
  僅要替換模式一次時,可以簡單地調用 replaceFirst 用于取代 replaceAll,它接受相同的參數。
9.4 使用 appendReplacement(StringBuffer, String) 和
      appendTail(StringBuffer) 方法返回目錄
  Matcher 類也提供了 appendReplacement 和 appendTail 兩個方法用于文本替換。下面的這個例子(RegexDemo.java)使用了這兩個方法完成與 replaceAll 相同的功能。
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class RegexDemo {
 
    private static String REGEX = "a*b";
    private static String INPUT = "aabfooaabfooabfoob";
    private static String REPLACE = "-";
 
    public static void main(String[] args) {
        Pattern p = Pattern.compile(REGEX);
        Matcher m = p.matcher(INPUT);       // 獲得匹配器對象
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            m.appendReplacement(sb, REPLACE);
        }
        m.appendTail(sb);
        System.out.println(sb.toString());
    }
}
  輸出:
-foo-foo-foo-
9.5 在 java.lang.String 中等價的 Matcher 方法返回目錄
  為了使用方便,String 類看上去還不錯地模仿了 Matcher 的兩個方法:
  :使用給定的替換字符串替換該字符串中匹配了給定正則表達式的第一個子字符串。調用 str.replaceFirst(regex, repl)方法與使用 Pattern.compile(regex).matcher(str).replaceFirst(repl)產生的結果是完全相同的。
  :使用給定的替換字符串替換該字符串中匹配了給定正則表達式的每一個子字符串。調用 str.replaceAll(regex, repl)方法與使用 Pattern.compile(regex).matcher(str).replaceAll(repl)產生的結果是完全相同的。
10 PatternSyntaxException 類的方法返回目錄
  PatternSyntaxException 是未檢查異常,指示正則表達式模式中的語法錯誤。PatternSyntaxException 類提供了下面的一些方法,用于確定在什么地方發生了錯誤:
  :獲得錯誤描述。
  :獲得錯誤索引。
  :獲得字符串形式的錯誤正則表達式。
  :獲得一個多行的字符串,包括語法錯誤和錯誤的索引、錯誤的正則表達式模式,以及模式內可視化的索引指示。
  下面的源代碼(RegexTestHarness2.java[10])更新了測試用具,用于檢查不正確的正則表達式:
import java.io.Console;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.regex.PatternSyntaxException;

public class RegexTestHarness2 {

    public static void main(String[] args){
        Pattern pattern = null;
        Matcher matcher = null;

        Console console = System.console();
        if (console == null) {
            System.err.println("No console.");
            System.exit(1);
        }
        while (true) {
            try {
                pattern = Pattern.compile(console.readLine("%nEnter your regex: "));
                matcher = pattern.matcher(console.readLine("Enter input string to search: "));
            } catch (PatternSyntaxException pse){
                console.format("There is a problem with the regular expression!%n");
                console.format("The pattern in question is: %s%n", pse.getPattern());
                console.format("The description is: %s%n", pse.getDescription());
                console.format("The message is: %s%n", pse.getMessage());
                console.format("The index is: %s%n", pse.getIndex());
                System.exit(0);
            }
            boolean found = false;
            while (matcher.find()) {
                console.format("I found the text \"%s\" starting at " +
                        "index %d and ending at index %d.%n",
                        matcher.group(), matcher.start(), matcher.end()
                    );
                found = true;
            }
            if (!found){
                console.format("No match found.%n");
            }
        }
    }
}
  運行該測試,輸入?i)foo作為正則表達式。這是個臆想出來的錯誤,程序員在使用內嵌標志表達式(?i)時忘記輸入左括號了。這樣做會產生下面的結果:
Enter your regex: ?i)
There is a problem with the regular expression!
The pattern in question is: ?i)
The description is: Dangling meta character '?'
The message is: Dangling meta character '?' near index 0
?i)
^
The index is: 0
  從這個輸出中,可以看出在索引 0 處的元字符(?)附近有語法錯誤。缺少左括號是導致這個錯誤的最魁禍首。
11 更多的資源返回目錄
  現在已經結束了正則表達式的課程,你也許會發現,主要引用了 Pattern、Matcher 和 PatternSyntaxException 類的 API 文檔。
  構建正則表達式更詳細地描述,推薦閱讀 Jeffrey E.F.Friedl 的Mastering Regular Expressions[11]
12 問題和練習返回目錄
〖問題〗
1. 在 java.util.regex 包中有哪三個公共的類?描述一下它們的作用。
2. 考慮一下字符串“foo”,它的開始索引是多少?結束索引是多少?解釋一下這些編號的意思。
3. 普通字符和元字符有什么不同?各給出它們的一個例子。
4. 如何把元字符表現成像普通字符那樣?
5. 附有方括號的字符集稱為什么?它有什么作用?
6. 這里是三個預定義的字符類:\d\s\w。描述一下它們各表示什么?并使用方括號的形式將它們重寫。
7. 對于\d\s\w,寫出兩個簡單的表達式,匹配它們相反的字符集。
8. 思考正則表達式(dog){3},識別一下其中的兩個子表達式。這個表達式會匹配什么字符串?
〖練習〗
1. 使用反向引用寫一個表達式,用于匹配一個人的名字,假設這個人的 first 名字與 last 名字是相同的。
【問題答案】
1. 在 java.util.regex 包中有哪三個公共的類?描述一下它們的作用。
  • 編譯后的 Pattern 實例表示正則表達式。
  • Matcher 實例是解析模式和靠著輸入的字符串完成匹配操作的引擎。
  • PatternSyntaxException 定義一個未檢查異常,指示正則表達式中的語法錯誤。
2. 考慮一下字符串“foo”,它的開始索引是多少?結束索引是多少?解釋一下這些編號的意思。
字符串中的每一個字符位于其自身的單元格中。索引位置在兩個單元格之間。字符串“foo”開始于索引 0,結束于索引 3,即便是這些字符僅占用了 0、1 和 2 號單元格。
3. 普通字符和元字符有什么不同?各給出它們的一個例子。
正則表達式中的普通字符匹配其本身。元字符是一個特殊的字符,會影響被匹配模式的方式。字母A是一個普通字符。標點符號.是一個元字符,其匹配任意的單字符。
4. 如何把元字符表現成像普通字符那樣?
有兩種方法:
  • 在元字符前加上反斜線(\);
  • 把元字符置于\Q(開始)\E(結束)的引用表達式中。
5. 附有方括號的字符集稱為什么?它有什么作用?
是一個字符類。通過方括號間的表達式,匹配指定字符類中的任意一個字符。
6. 這里是三個預定義的字符類:\d\s\w。描述一下它們各表示什么?并使用方括號的形式將它們重寫。
\d 匹配任意數字[0-9]
  \s 匹配任意空白字符[ \t\n-x0B\f\r]
  \w 匹配任意單詞字符[a-zA-Z_0-9]
7. 對于\d\s\w,寫出兩個簡單的表達式,匹配它們相反的字符集。
\d \D [^\d]
  \s \S [^\s]
  \w \W [^\w]
8. 思考正則表達式(dog){3},識別一下其中的兩個子表達式。這個表達式會匹配什么字符串?
表達式由捕獲組(dog)和接著的貪婪量詞{3}所組成。它匹配字符串“dogdogdog”。
【練習答案】
1. 使用反向引用寫一個表達式,用于匹配一個人的名字,假設這個人的 first 名字與 last 名字是相同的。
([A-Z][a-zA-Z]*)\s\1

[1]  本文全文譯自 Java TutorialRegular Expressions,標題是譯者自擬的。——譯者注

[2]  Unix 工具,用于文件中的字符串查找,它是最早的正則表達式工具之一。——譯者注

[3]  若要退出可以使用 Ctrl + C 來中斷。——譯者注

[4]  圖中的“索引 3”指示是譯者所加,原文中并沒有。——譯者注

[5]  這種方式在 JDK 6.0 以前版本使用需要注意,在字符類中使用這種結構是有 bug 的,不過在 JDK 6.0 中已經修正。——譯者注

[6]  若\E前沒有\Q時會產生 PatternSyntaxException 異常指示語法錯誤。——譯者注

[7]  第一次匹配時僅匹配字符串的開始部分,與\A類似。(引自 Jeffrey E.F.Friedl, Mastering Regular Expressions, 3rd ed., §3.5.3.3, O'Reilly, 2006.)——譯者注

[8]  \u030A,即字符 å 上半部分的小圓圈( ̊ )(該字符在 IE 瀏覽器上無法正確顯示,在 Firefox 瀏覽器上可以正常地顯示)。——譯者注

[9]  JDK 5.0 新增的方法,JDK 1.4 中不能使用。——譯者注

[10]  JDK 1.4 和 JDK 5.0 適用的版本在所附的源代碼中。適用于 JDK 1.4 的文件名為 RegexTestHarness2V4.java,JDK 1.5 的文件名為 RegexTestHarness2V5.java。——譯者注

[11]  第三版是本書的最新版本。第三版的中譯本《精通正則表達式》已由電子工業出版社于 2007 年 7 月出版。——譯者注

譯后記返回目錄
  帶著忐忑不安的心情完成了我的第一篇譯篇,但愿這個教程能讓大家對 Java 中的正則表達式有更一步的認識。
  雖然這是一個關于 Java 正則表達式很好的一個入門教程,但這個教程也有其不足之處,其中僅僅涉及了最為簡單的正則表達式,對介紹到的有些問題并未完全展開,比如:字符類中的轉義、內嵌標志表達式具體的用法等。對有些常用的表達式,如|(選擇結構)也沒有涉及。對于非捕獲組來說,僅僅提到了內嵌標志表達式,對于諸如(?:X)(?=X)(?!X)(?<=X)(?<!X)(?>X)等等之類的非捕獲組結構完全沒有涉及。正如譯者在序中提到的,這篇文章只為今后學習更高級的正則表達式技術奠定良好的基礎。

譯者
2008 年 3 月 2 日
 
捕鱼达人小游戏