天天看點

Java正則速成秘籍(二)之心法篇導讀概述元字元分組構造貪婪與懶惰附錄參考

導讀

正規表達式是什麼?有什麼用?

正規表達式(Regular Expression)是一種文本規則,可以用來校驗、查找、替換與規則比對的文本。

又愛又恨的正則

正規表達式是一個強大的文本比對工具,但是它的規則實在很繁瑣,而且了解起來也頗為蛋疼,容易讓人望而生畏。

如何學習正則

剛接觸正則時,我看了一堆正則的語義說明,但是仍然不明是以。後來,我多接觸一些正則的應用執行個體,漸漸有了感覺,再結合語義說明,終有領悟。我覺得正規表達式和武俠修練武功差不多,應該先練招式,再練心法。如果一開始就直接看正則的規則,保證你會懵逼。

當你熟悉基本招式(正則基本使用案例)後,也該修煉修煉心法(正則文法)了。真正的高手不能隻靠死記硬背那麼幾招把式。就像張三豐教張無忌太極拳一樣,領悟心法,融會貫通,少俠你就可以無招勝有招,成為傳說中的絕世高手。

以上閑話可歸納為一句:學習正則應該從執行個體去了解規則。

Java正則速成秘籍(二)之心法篇導讀概述元字元分組構造貪婪與懶惰附錄參考

打開秘籍:欲練神功,必先自宮!沒有蛋,也就不會蛋疼了。

Java正則速成秘籍分三篇:

展示Java對于正規表達式的支援。

介紹正規表達式的文法規則。

從實戰出發,介紹正則的常用案例。

一文,我們學習了Java支援正則功能的API。

本文是Java正則速成秘籍的心法篇。主要介紹正規表達式的文法規則。正則文法規則是一種标準,主流開發語言對于正則文法的支援大體相同。

分組構造、貪婪與懶惰屬于正規表達式中較為複雜的應用,建議了解完基本元字元後再去了解。

本文案例中使用的checkMatches、findAll方法請見附錄。

本文涉及的所有案例代碼,可以在

我的github 找到,如有需要,可以參考。

概述

為了了解下面章節的内容,你需要先了解一些基本概念。

正規表達式

正規表達式是對字元串操作的一種邏輯公式,就是用事先定義好的一些特定字元、及這些特定字元的組合,組成一個“規則字元串”,這個“規則字元串”用來表達對字元串的一種過濾邏輯。

元字元

元字元(metacharacters)就是正規表達式中具有特殊意義的專用字元。

普通字元

普通字元包括沒有顯式指定為元字元的所有可列印和不可列印字元。這包括所有大寫和小寫字母、所有數字、所有标點符号和一些其他符号。

基本元字元

正規表達式的元字元難以記憶,很大程度上是因為有很多為了簡化表達而出現的等價字元。

而實際上最基本的元字元,并沒有那麼多。對于大部分的場景,基本元字元都可以搞定。

讓我們從一個個執行個體出發,由淺入深的去體會正則的奧妙。

多選 - |

例 比對一個确定的字元串

checkMatches("abc", "abc");           

如果要比對一個确定的字元串,非常簡單,如例1所示。

如果你不确定要比對的字元串,希望有多個選擇,怎麼辦?

答案是:使用元字元

|

,它的含義是或。

例 比對多個可選的字元串

// 測試正規表達式字元:|
Assert.assertTrue(checkMatches("yes|no", "yes"));
Assert.assertTrue(checkMatches("yes|no", "no"));
Assert.assertFalse(checkMatches("yes|no", "right"));           

輸出

yes matches: yes|no
no  matches: yes|no
right   not matches: yes|no           

分組 - ()

如果你希望表達式由多個子表達式組成,你可以使用

()

例 比對組合字元串

Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "ended"));
Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "ending"));
Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "playing"));
Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "played"));           
ended   matches: (play|end)(ing|ed)
ending  matches: (play|end)(ing|ed)
playing matches: (play|end)(ing|ed)
played  matches: (play|end)(ing|ed)           

指定單字元有效範圍 - []

前面展示了如何比對字元串,但是很多時候你需要精确的比對一個字元,這時可以使用

[]

例 字元在指定範圍

// 測試正規表達式字元:[]
Assert.assertTrue(checkMatches("[abc]", "b"));  // 字元隻能是a、b、c
Assert.assertTrue(checkMatches("[a-z]", "m")); // 字元隻能是a - z
Assert.assertTrue(checkMatches("[A-Z]", "O")); // 字元隻能是A - Z
Assert.assertTrue(checkMatches("[a-zA-Z]", "K")); // 字元隻能是a - z和A - Z
Assert.assertTrue(checkMatches("[a-zA-Z]", "k"));
Assert.assertTrue(checkMatches("[0-9]", "5")); // 字元隻能是0 - 9           
b   matches: [abc]
m   matches: [a-z]
O   matches: [A-Z]
K   matches: [a-zA-Z]
k   matches: [a-zA-Z]
5   matches: [0-9]           

指定單字元無效範圍 - [^]

例 字元不能在指定範圍

如果需要比對一個字元的逆操作,即字元不能在指定範圍,可以使用

[^]

// 測試正規表達式字元:[^]
Assert.assertFalse(checkMatches("[^abc]", "b")); // 字元不能是a、b、c
Assert.assertFalse(checkMatches("[^a-z]", "m")); // 字元不能是a - z
Assert.assertFalse(checkMatches("[^A-Z]", "O")); // 字元不能是A - Z
Assert.assertFalse(checkMatches("[^a-zA-Z]", "K")); // 字元不能是a - z和A - Z
Assert.assertFalse(checkMatches("[^a-zA-Z]", "k"));
Assert.assertFalse(checkMatches("[^0-9]", "5")); // 字元不能是0 - 9           
b   not matches: [^abc]
m   not matches: [^a-z]
O   not matches: [^A-Z]
K   not matches: [^a-zA-Z]
k   not matches: [^a-zA-Z]
5   not matches: [^0-9]           

限制字元數量 - {}

如果想要控制字元出現的次數,可以使用

{}

字元 描述

{n}

n 是一個非負整數。比對确定的 n 次。

{n,}

n 是一個非負整數。至少比對 n 次。

{n,m}

m 和 n 均為非負整數,其中n <= m。最少比對 n 次且最多比對 m 次。

例 限制字元出現次數

// {n}: n 是一個非負整數。比對确定的 n 次。
checkMatches("ap{1}", "a");
checkMatches("ap{1}", "ap");
checkMatches("ap{1}", "app");
checkMatches("ap{1}", "apppppppppp");

// {n,}: n 是一個非負整數。至少比對 n 次。
checkMatches("ap{1,}", "a");
checkMatches("ap{1,}", "ap");
checkMatches("ap{1,}", "app");
checkMatches("ap{1,}", "apppppppppp");

// {n,m}: m 和 n 均為非負整數,其中 n <= m。最少比對 n 次且最多比對 m 次。
checkMatches("ap{2,5}", "a");
checkMatches("ap{2,5}", "ap");
checkMatches("ap{2,5}", "app");
checkMatches("ap{2,5}", "apppppppppp");           
a   not matches: ap{1}
ap  matches: ap{1}
app not matches: ap{1}
apppppppppp not matches: ap{1}
a   not matches: ap{1,}
ap  matches: ap{1,}
app matches: ap{1,}
apppppppppp matches: ap{1,}
a   not matches: ap{2,5}
ap  not matches: ap{2,5}
app matches: ap{2,5}
apppppppppp not matches: ap{2,5}           

轉義字元 - /

如果想要查找元字元本身,你需要使用轉義符,使得正則引擎将其視作一個普通字元,而不是一個元字元去處理。

* 的轉義字元:\*
+ 的轉義字元:\+
? 的轉義字元:\?
^ 的轉義字元:\^
$ 的轉義字元:\$
. 的轉義字元:\.           

如果是轉義符

\

本身,你也需要使用

\\

指定表達式字元串的開始和結尾 - ^、$

如果希望比對的字元串必須以特定字元串開頭,可以使用

^

注:請特别留意,這裡的

^

一定要和

[^]

中的 “^” 區分。

例 限制字元串頭部

Assert.assertTrue(checkMatches("^app[a-z]{0,}", "apple")); // 字元串必須以app開頭
Assert.assertFalse(checkMatches("^app[a-z]{0,}", "aplause"));           
apple   matches: ^app[a-z]{0,}
aplause not matches: ^app[a-z]{0,}           

$

例 限制字元串尾部

Assert.assertTrue(checkMatches("[a-z]{0,}ing$", "playing")); // 字元串必須以ing結尾
Assert.assertFalse(checkMatches("[a-z]{0,}ing$", "long"));           
playing matches: [a-z]{0,}ing$
long    not matches: [a-z]{0,}ing$           

等價字元

等價字元,顧名思義,就是對于基本元字元表達的一種簡化(等價字元的功能都可以通過基本元字元來實作)。

在沒有掌握基本元字元之前,可以先不用理會,因為很容易把人繞暈。

等價字元的好處在于簡化了基本元字元的寫法。

表示某一類型字元的等價字元

下表中的等價字元都表示某一類型的字元。

.

比對除“\n”之外的任何單個字元。

\d

比對一個數字字元。等價于[0-9]。

\D

比對一個非數字字元。等價于[^0-9]。

\w

比對包括下劃線的任何單詞字元。類似但不等價于“[A-Za-z0-9_]”,這裡的單詞字元指的是Unicode字元集。

\W

比對任何非單詞字元。

\s

比對任何不可見字元,包括空格、制表符、換頁符等等。等價于[ \f\n\r\t\v]。

\S

比對任何可見字元。等價于[ \f\n\r\t\v]。

案例 基本等價字元的用法

// 比對除“\n”之外的任何單個字元
Assert.assertTrue(checkMatches(".{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"));
Assert.assertTrue(checkMatches(".{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\"));
Assert.assertFalse(checkMatches(".", "\n"));
Assert.assertFalse(checkMatches("[^\n]", "\n"));

// 比對一個數字字元。等價于[0-9]
Assert.assertTrue(checkMatches("\\d{1,}", "0123456789"));
// 比對一個非數字字元。等價于[^0-9]
Assert.assertFalse(checkMatches("\\D{1,}", "0123456789"));

// 比對包括下劃線的任何單詞字元。類似但不等價于“[A-Za-z0-9_]”,這裡的單詞字元指的是Unicode字元集
Assert.assertTrue(checkMatches("\\w{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"));
Assert.assertFalse(checkMatches("\\w{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\"));
// 比對任何非單詞字元
Assert.assertFalse(checkMatches("\\W{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"));
Assert.assertTrue(checkMatches("\\W{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\"));

// 比對任何不可見字元,包括空格、制表符、換頁符等等。等價于[ \f\n\r\t\v]
Assert.assertTrue(checkMatches("\\s{1,}", " \f\r\n\t"));
// 比對任何可見字元。等價于[^ \f\n\r\t\v]
Assert.assertFalse(checkMatches("\\S{1,}", " \f\r\n\t"));           
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ matches: .{1,}
~!@#$%^&*()+`-=[]{};:<>,./?|\\  matches: .{1,}
\n  not matches: .
\n  not matches: [^\n]
0123456789  matches: \\d{1,}
0123456789  not matches: \\D{1,}
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ matches: \\w{1,}
~!@#$%^&*()+`-=[]{};:<>,./?|\\  not matches: \\w{1,}
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ not matches: \\W{1,}
~!@#$%^&*()+`-=[]{};:<>,./?|\\  matches: \\W{1,}
 \f\r\n\t   matches: \\s{1,}
 \f\r\n\t   not matches: \\S{1,}           

限制字元數量的等價字元

在基本元字元章節中,已經介紹了限制字元數量的基本元字元 -

{}

此外,還有

*

+

?

這個三個為了簡化寫法而出現的等價字元,我們來認識一下。

*

比對前面的子表達式零次或多次。等價于{0,}。

+

比對前面的子表達式一次或多次。等價于{1,}。

?

比對前面的子表達式零次或一次。等價于 {0,1}。

案例 限制字元數量的等價字元

// *: 比對前面的子表達式零次或多次。* 等價于{0,}。
checkMatches("ap*", "a");
checkMatches("ap*", "ap");
checkMatches("ap*", "app");
checkMatches("ap*", "apppppppppp");

// +: 比對前面的子表達式一次或多次。+ 等價于 {1,}。
checkMatches("ap+", "a");
checkMatches("ap+", "ap");
checkMatches("ap+", "app");
checkMatches("ap+", "apppppppppp");

// ?: 比對前面的子表達式零次或一次。? 等價于 {0,1}。
checkMatches("ap?", "a");
checkMatches("ap?", "ap");
checkMatches("ap?", "app");
checkMatches("ap?", "apppppppppp");           
a   matches: ap*
ap  matches: ap*
app matches: ap*
apppppppppp matches: ap*
a   not matches: ap+
ap  matches: ap+
app matches: ap+
apppppppppp matches: ap+
a   matches: ap?
ap  matches: ap?
app not matches: ap?
apppppppppp not matches: ap?           

元字元優先級順序

正規表達式從左到右進行計算,并遵循優先級順序,這與算術表達式非常類似。

下表從最高到最低說明了各種正規表達式運算符的優先級順序:

運算符 說明
\ 轉義符
(), (?:), (?=), [] 括号和中括号
*, +, ?, {n}, {n,}, {n,m} 限定符
^, $, *任何元字元、任何字元* 定位點和序列
| 替換

字元具有高于替換運算符的優先級,使得“m|food”比對“m”或“food”。若要比對“mood”或“food”,請使用括号建立子表達式,進而産生“(m|f)ood”。

分組構造

在基本元字元章節,提到了

()

字元可以用來對表達式分組。實際上分組還有更多複雜的用法。

所謂分組構造,是用來描述正規表達式的子表達式,用于捕獲字元串中的子字元串。

捕獲與非捕獲

下表為分組構造中的捕獲和非捕獲分類。

表達式 捕獲或非捕獲

(exp)

比對的子表達式 捕獲

(?<name>exp)

命名的反向引用

(?:exp)

非捕獲組 非捕獲

(?=exp)

零寬度正預測先行斷言

(?!exp)

零寬度負預測先行斷言

(?<=exp)

零寬度正回顧後發斷言

(?<!exp)

零寬度負回顧後發斷言

注:Java正則引擎不支援平衡組。

反向引用

帶編号的反向引用

帶編号的反向引用使用以下文法:

\number

其中number 是正規表達式中捕獲組的序号位置。 例如,\4 比對第四個捕獲組的内容。 如果正規表達式模式中未定義number,則将發生分析錯誤

例 比對重複的單詞和緊随每個重複的單詞的單詞(不命名子表達式)

// (\w+)\s\1\W(\w+) 比對重複的單詞和緊随每個重複的單詞的單詞
Assert.assertTrue(findAll("(\\w+)\\s\\1\\W(\\w+)",
        "He said that that was the the correct answer.") > 0);           
regex = (\w+)\s\1\W(\w+), content: He said that that was the the correct answer.
[1th] start: 8, end: 21, group: that that was
[2th] start: 22, end: 37, group: the the correct           

(\w+): 比對一個或多個單詞字元。

\s: 與空白字元比對。

\1: 比對第一個組,即(\w+)。

\W: 比對包括空格和标點符号的一個非單詞字元。 這樣可以防止正規表達式模式比對從第一個捕獲組的單詞開頭的單詞。

命名後向引用通過使用下面的文法進行定義:

\k<name >

例 比對重複的單詞和緊随每個重複的單詞的單詞(命名子表達式)

// (?<duplicateWord>\w+)\s\k<duplicateWord>\W(?<nextWord>\w+) 比對重複的單詞和緊随每個重複的單詞的單詞
Assert.assertTrue(findAll("(?<duplicateWord>\\w+)\\s\\k<duplicateWord>\\W(?<nextWord>\\w+)",
        "He said that that was the the correct answer.") > 0);           
regex = (?<duplicateWord>\w+)\s\k<duplicateWord>\W(?<nextWord>\w+), content: He said that that was the the correct answer.
[1th] start: 8, end: 21, group: that that was
[2th] start: 22, end: 37, group: the the correct           

(?\w+): 比對一個或多個單詞字元。 命名此捕獲組 duplicateWord。

\k: 比對名為 duplicateWord 的捕獲的組。

(?\w+): 比對一個或多個單詞字元。 命名此捕獲組 nextWord。

(?:exp)

表示當一個限定符應用到一個組,但組捕獲的子字元串并非所需時,通常會使用非捕獲組構造。

例 比對以.結束的語句。

// 比對由句号終止的語句。
Assert.assertTrue(findAll("(?:\\b(?:\\w+)\\W*)+\\.", "This is a short sentence. Never end") > 0);           
regex = (?:\b(?:\w+)\W*)+\., content: This is a short sentence. Never end
[1th] start: 0, end: 25, group: This is a short sentence.           

零寬斷言

用于查找在某些内容(但并不包括這些内容)之前或之後的東西,也就是說它們像\b,^,$那樣用于指定一個位置,這個位置應該滿足一定的條件(即斷言),是以它們也被稱為零寬斷言。

(?=exp)

比對exp前面的位置

(?<=exp)

比對exp後面的位置

(?!exp)

比對後面跟的不是exp的位置

(?<!exp)

比對前面不是exp的位置

(?=exp)

表示輸入字元串必須比對子表達式中的正規表達式模式,盡管比對的子字元串未包含在比對結果中。

// \b\w+(?=\sis\b) 表示要捕獲is之前的單詞
Assert.assertTrue(findAll("\\b\\w+(?=\\sis\\b)", "The dog is a Malamute.") > 0);
Assert.assertFalse(findAll("\\b\\w+(?=\\sis\\b)", "The island has beautiful birds.") > 0);
Assert.assertFalse(findAll("\\b\\w+(?=\\sis\\b)", "The pitch missed home plate.") > 0);
Assert.assertTrue(findAll("\\b\\w+(?=\\sis\\b)", "Sunday is a weekend day.") > 0);           
regex = \b\w+(?=\sis\b), content: The dog is a Malamute.
[1th] start: 4, end: 7, group: dog
regex = \b\w+(?=\sis\b), content: The island has beautiful birds.
not found
regex = \b\w+(?=\sis\b), content: The pitch missed home plate.
not found
regex = \b\w+(?=\sis\b), content: Sunday is a weekend day.
[1th] start: 0, end: 6, group: Sunday           

\b: 在單詞邊界處開始比對。

\w+: 比對一個或多個單詞字元。

(?=\sis\b): 确定單詞字元是否後接空白字元和字元串“is”,其在單詞邊界處結束。 如果如此,則比對成功。

(?<=exp)

表示子表達式不得在輸入字元串目前位置左側出現,盡管子表達式未包含在比對結果中。零寬度正回顧後發斷言不會回溯。

// (?<=\b20)\d{2}\b 表示要捕獲以20開頭的數字的後面部分
Assert.assertTrue(findAll("(?<=\\b20)\\d{2}\\b", "2010 1999 1861 2140 2009") > 0);           
regex = (?<=\b20)\d{2}\b, content: 2010 1999 1861 2140 2009
[1th] start: 2, end: 4, group: 10
[2th] start: 22, end: 24, group: 09           

\d{2}: 比對兩個十進制數字。

{?<=\b20): 如果兩個十進制數字的字邊界以小數位數“20”開頭,則繼續比對。

\b: 在單詞邊界處結束比對。

(?!exp)

表示輸入字元串不得比對子表達式中的正規表達式模式,盡管比對的子字元串未包含在比對結果中。

例 捕獲未以“un”開頭的單詞

// \b(?!un)\w+\b 表示要捕獲未以“un”開頭的單詞
Assert.assertTrue(findAll("\\b(?!un)\\w+\\b", "unite one unethical ethics use untie ultimate") > 0);           
regex = \b(?!un)\w+\b, content: unite one unethical ethics use untie ultimate
[1th] start: 6, end: 9, group: one
[2th] start: 20, end: 26, group: ethics
[3th] start: 27, end: 30, group: use
[4th] start: 37, end: 45, group: ultimate           

(?!un): 确定接下來的兩個的字元是否為“un”。 如果沒有,則可能比對。

(?<!exp)

表示子表達式不得在輸入字元串目前位置的左側出現。 但是,任何不比對子表達式 的子字元串不包含在比對結果中。

例 捕獲任意工作日

// (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b 表示要捕獲任意工作日(即周一到周五)
Assert.assertTrue(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Monday February 1, 2010") > 0);
Assert.assertTrue(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Wednesday February 3, 2010") > 0);
Assert.assertFalse(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Saturday February 6, 2010") > 0);
Assert.assertFalse(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Sunday February 7, 2010") > 0);
Assert.assertTrue(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Monday, February 8, 2010") > 0);           
regex = (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b, content: Monday February 1, 2010
[1th] start: 7, end: 23, group: February 1, 2010
regex = (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b, content: Wednesday February 3, 2010
[1th] start: 10, end: 26, group: February 3, 2010
regex = (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b, content: Saturday February 6, 2010
not found
regex = (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b, content: Sunday February 7, 2010
not found
regex = (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b, content: Monday, February 8, 2010
[1th] start: 8, end: 24, group: February 8, 2010           

貪婪與懶惰

當正規表達式中包含能接受重複的限定符時,通常的行為是(在使整個表達式能得到比對的前提下)比對盡可能多的字元。以這個表達式為例:a.*b,它将會比對最長的以a開始,以b結束的字元串。如果用它來搜尋aabab的話,它會比對整個字元串aabab。這被稱為貪婪比對。

有時,我們更需要懶惰比對,也就是比對盡可能少的字元。前面給出的限定符都可以被轉化為懶惰比對模式,隻要在它後面加上一個問号?。這樣.*?就意味着比對任意數量的重複,但是在能使整個比對成功的前提下使用最少的重複。

*?

重複任意次,但盡可能少重複

+?

重複1次或更多次,但盡可能少重複

??

重複0次或1次,但盡可能少重複

{n,m}?

重複n到m次,但盡可能少重複

{n,}?

重複n次以上,但盡可能少重複

例 Java正則中貪婪與懶惰的示例

// 貪婪比對
Assert.assertTrue(findAll("a\\w*b", "abaabaaabaaaab") > 0);

// 懶惰比對
Assert.assertTrue(findAll("a\\w*?b", "abaabaaabaaaab") > 0);
Assert.assertTrue(findAll("a\\w+?b", "abaabaaabaaaab") > 0);
Assert.assertTrue(findAll("a\\w??b", "abaabaaabaaaab") > 0);
Assert.assertTrue(findAll("a\\w{0,4}?b", "abaabaaabaaaab") > 0);
Assert.assertTrue(findAll("a\\w{3,}?b", "abaabaaabaaaab") > 0);           
regex = a\w*b, content: abaabaaabaaaab
[1th] start: 0, end: 14, group: abaabaaabaaaab
regex = a\w*?b, content: abaabaaabaaaab
[1th] start: 0, end: 2, group: ab
[2th] start: 2, end: 5, group: aab
[3th] start: 5, end: 9, group: aaab
[4th] start: 9, end: 14, group: aaaab
regex = a\w+?b, content: abaabaaabaaaab
[1th] start: 0, end: 5, group: abaab
[2th] start: 5, end: 9, group: aaab
[3th] start: 9, end: 14, group: aaaab
regex = a\w??b, content: abaabaaabaaaab
[1th] start: 0, end: 2, group: ab
[2th] start: 2, end: 5, group: aab
[3th] start: 6, end: 9, group: aab
[4th] start: 11, end: 14, group: aab
regex = a\w{0,4}?b, content: abaabaaabaaaab
[1th] start: 0, end: 2, group: ab
[2th] start: 2, end: 5, group: aab
[3th] start: 5, end: 9, group: aaab
[4th] start: 9, end: 14, group: aaaab
regex = a\w{3,}?b, content: abaabaaabaaaab
[1th] start: 0, end: 5, group: abaab
[2th] start: 5, end: 14, group: aaabaaaab           

本例中代碼展示的是使用不同貪婪或懶惰政策去查找字元串"abaabaaabaaaab" 中比對以"a"開頭,以"b"結尾的所有子字元串。

請從輸出結果中,細細體味使用不同的貪婪或懶惰政策,對于比對子字元串有什麼影響。

附錄

比對正則字元串的方法

由于正規表達式中很多元字元本身就是轉義字元,在Java字元串的規則中不會被顯示出來。

為此,可以使用一個工具類

org.apache.commons.lang3.StringEscapeUtils

來做特殊處理,使得轉義字元可以列印。這個工具類提供的都是靜态方法,從方法命名大緻也可以猜出用法,這裡不多做說明。

如果你了解maven,可以直接引入依賴

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>${commons-lang3.version}</version>
</dependency>           

本文為了展示正則比對規則用到的方法

private boolean checkMatches(String regex, String content) {
    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(content);
    boolean flag = m.matches();
    if (m.matches()) {
        System.out.println(StringEscapeUtils.escapeJava(content) + "\tmatches: " + StringEscapeUtils.escapeJava(regex));
    } else {
        System.out.println(StringEscapeUtils.escapeJava(content) + "\tnot matches: " + StringEscapeUtils.escapeJava(regex));
    }
    return flag;
}

public int findAll(String regex, String content) {
    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(content);
    System.out.println("regex = " + regex + ", content: " + content);

    int count = 0;
    while (m.find()) {
        count++;
        System.out.println("[" + count + "th] " + "start: " + m.start() + ", end: " + m.end()
                + ", group: " + m.group());
    }
    if (0 == count) {
        System.out.println("not found");
    }
    return count;
}           

速查元字元字典

為了友善快查正則的元字元含義,在本節根據元字元的功能集中羅列正則的各種元字元。

*

比對前面的子表達式零次或多次。例如,zo* 能比對 "z" 以及 "zoo"。* 等價于{0,}。

+

比對前面的子表達式一次或多次。例如,'zo+' 能比對 "zo" 以及 "zoo",但不能比對 "z"。+ 等價于 {1,}。

?

比對前面的子表達式零次或一次。例如,"do(es)?" 可以比對 "do" 或 "does" 中的"do" 。? 等價于 {0,1}。

{n}

n 是一個非負整數。比對确定的 n 次。例如,'o{2}' 不能比對 "Bob" 中的 'o',但是能比對 "food" 中的兩個 o。

{n,}

n 是一個非負整數。至少比對n 次。例如,'o{2,}' 不能比對 "Bob" 中的 'o',但能比對 "foooood" 中的所有 o。'o{1,}' 等價于 'o+'。'o{0,}' 則等價于 'o*'。

{n,m}

m 和 n 均為非負整數,其中n <= m。最少比對 n 次且最多比對 m 次。例如,"o{1,3}" 将比對 "fooooood" 中的前三個 o。'o{0,1}' 等價于 'o?'。請注意在逗号和兩個數之間不能有空格。

定位符

^

比對輸入字元串開始的位置。如果設定了 RegExp 對象的 Multiline 屬性,^ 還會與 \n 或 \r 之後的位置比對。

$

比對輸入字元串結尾的位置。如果設定了 RegExp 對象的 Multiline 屬性,$ 還會與 \n 或 \r 之前的位置比對。

\b

比對一個字邊界,即字與空格間的位置。

\B

非字邊界比對。

非列印字元

\cx

比對由x指明的控制字元。例如, \cM 比對一個 Control-M 或回車符。x 的值必須為 A-Z 或 a-z 之一。否則,将 c 視為一個原義的 'c' 字元。

\f

比對一個換頁符。等價于 \x0c 和 \cL。

\n

比對一個換行符。等價于 \x0a 和 \cJ。

\r

比對一個回車符。等價于 \x0d 和 \cM。

\s

比對任何空白字元,包括空格、制表符、換頁符等等。等價于 [ \f\n\r\t\v]。

\S

比對任何非空白字元。等價于 [^\f\n\r\t\v]。

\t

比對一個制表符。等價于 \x09 和 \cI。

\v

比對一個垂直制表符。等價于 \x0b 和 \cK。

分組

(exp)

比對的子表達式。()中的内容就是子表達式。

(?<name>exp)

命名的子表達式(反向引用)。

(?:exp)

非捕獲組,表示當一個限定符應用到一個組,但組捕獲的子字元串并非所需時,通常會使用非捕獲組構造。

(?=exp)

比對exp前面的位置。

(?<=exp)

比對exp後面的位置。

(?!exp)

比對後面跟的不是exp的位置。

(?<!exp)

比對前面不是exp的位置。

特殊符号

\

将下一個字元标記為或特殊字元、或原義字元、或向後引用、或八進制轉義符。例如, 'n' 比對字元 'n'。'\n' 比對換行符。序列 '\' 比對 "",而 '(' 則比對 "("。

\|

指明兩項之間的一個選擇。

[]

比對方括号範圍内的任意一個字元。形式如:[xyz]、[^xyz]、[a-z]、[^a-z]、[x,y,z]

參考

正規表達式30分鐘入門教程 msdn 正規表達式教程