正則表達式(regular expression)是一種可以在許多現代應用程序和編程語言中使用的特殊形式的代碼模式。可以使用它們來驗證輸入是否符合給定的文本模式,在一大段文字中查找該模式的文本,用其它文本來替換匹配該模式的文本或者重新組織匹配文本的一部分,把一塊文本劃分成一系列更小的文本。
在Java語言中,從jdk1.4中加入了java.util.regex包提供對正則表達式的支持,而且Java.lang.String類中的replaceAll和split函數也是調用的正則表達式來實現的。在java.util.regex包中,常用到的類是Pattern和Matcher。典型的調用順序為:
Pattern p = Pattern.compile("a*b"); // a*b是被編譯的正則表達式
Matcher m = p.matcher("aaaaab"); // aaaaab為要匹配表達式
boolean b = m.matches(); //b為匹配結果
其等價于:
boolean b = Pattern.matches("a*b", "aaaaab");//對于重復匹配效率不高
說明一下,使用Matcher 類中的matches()方法是進行完全匹配,使用find()方法可以進行部分匹配。String類的matches()方法是調用Pattern類中的matches();String 類中的contains()方法也可以判斷一個字符串中是否包含某一個子串。
1.基本語法
1.1元字符
正則表達式之所以擁有巨大的魔力,就是因為有12個標點字符才產生的,
$ ( ) * + . ? [ \ ^ { |
它們被稱作元字符,如果想要在下正則表達式中匹配它們,那么就需要在它們前面用一個反斜杠\來進行轉義。特別應該注意的是在這個列表中并不包含右方括號]、連字符-和右花括號},前兩個字符只有在它們位于一個沒有轉義的[之后才成為元字符,而}只有在一個沒有轉義的{之后才是元字符。在任何時候都沒有必要對}進行轉義。
對任意其它非字母數字的字符進行轉義不會轉變你的正則表達式的規則----至少在.NET、Java、JavaScript、PCRE、Perl、Python、Ruby都不會這樣,而對一個字母數字字符進行轉義則會給它一個特殊含義,或者出現一個語法錯誤。
如果想要匹配$()*+.?[\^{|,(匹配單個元字符,當這些元字符混合成其它語法結構另當別論)則對應的正則表達式應該為:\$\(\)\*\+\.\?\[\\\^\{\|。但是在代碼里面應該這樣寫:
Pattern p = Pattern.compile("\\$\\(\\)\\*\\+\\.\\?\\[\\\\\\^\\{\\|");
Matcher m = p.matcher("$()*+.?[\\^{|");
boolean b = m.matches();
注意:在Java里面,要匹配反斜杠\,需要使用“\\\\”,因為在字符串里面不允許出現單個反斜杠\,所以“\\”表示一個反斜杠\,“\\\\”才能正確匹配到反斜杠\。
Java 6、PCRE、Perl支持使用正則記號<\Q>和<\E>。<\Q>會抑制所有元字符的含義,直到出現<\E>為止。如果漏掉了<\E>,那么在<\Q>之后直到正則表達式結束之前的所有字符都會被當作字符文本來對待。所以上面的正則表達式可以用如下代碼匹配:
Pattern p = Pattern.compile("\\Q$()*+.?[\\^{|\\E");
Matcher m = p.matcher("$()*+.?[\\^{|");
在字符串里面還有一個特殊的字符,雙引號”,如果想要匹配一個字符器里面是否包含有雙引號是,可以使用如下代碼:
Pattern p = Pattern.compile("\"");
Matcher m = p.matcher("\"");
注:在字符串里面雙引號需要轉義
1.2匹配單個字符
除了元字符之外,匹配單個字符直接使用對應的字符來匹配,當然也有一些特殊的字符,如匹配一個包含ASCII控制字符的字符串:響鈴、退出、換頁、換行、回車、水平制表符和垂直制表符,對應的地十六進制ASCII分別是:07、1B、0C、0A、0D、09、0B。對應的參照表1如下:
x | 字符 x |
\\ | 反斜線字符 |
\0n | 帶有八進制值 0 的字符 n (0 <= n <= 7) |
\0nn | 帶有八進制值 0 的字符 nn (0 <= n <= 7) |
\0mnn | 帶有八進制值 0 的字符 mnn(0 <= m <= 3、0 <= n <= 7) |
\xhh | 帶有十六進制值 0x 的字符 hh |
\uhhhh | 帶有十六進制值 0x 的字符 hhhh |
\t | 制表符 ('\u0009') |
\n | 新行(換行)符 ('\u000A') |
\r | 回車符 ('\u000D') |
\f | 換頁符 ('\u000C') |
\a | 報警 (bell) 符 ('\u0007') |
\e | 轉義符 ('\u001B') |
\cx | 對應于 x 的控制符 |
表1 單個字符匹配
1.3匹配字符類
使用的方括號[]的表示法被稱作是一個字符類(character class)。一個字符類匹配在一個可能的字符列表中的單個字符。首先看一下預定義的字符類有哪些,如表2所示:
預定義字符類 |
. | 任何字符(與行結束符可能匹配也可能不匹配) |
\d | 數字:[0-9] |
\D | 非數字: [^0-9] |
\s | 空白字符:[ \t\n\x0B\f\r] |
\S | 非空白字符:[^\s] |
\w | 單詞字符:[a-zA-Z_0-9] |
\W | 非單詞字符:[^\w] |
表2預定義字符類
在字符類之外,上面的12個標點字符是元字符。在一個字符類中,只有其中4個字符擁有特殊功能:\、^、-和]。(也就是說,在字符類里面,除了那4個特殊字符,其它的字符都不需要使用轉義符。)如果使用的是Java或者是.NET,那么左方括號[在字符類也是一個元字符,所有的其它字符都是字面量,只是把它們自身加入到了字符類中。
點號是最古老也是最簡單的正則表達式特性之一。它的含義永遠是匹配任意單個字符。點號是最經常被濫用的正則表達式特性,最好只有當你確實想要允許出現任意字符是地,才使用點號,而在任何場合,都應當使用一個字符類或者是否定字符類來實現。
在.NET、Java、PCRE、Perl、Python中,<(?s)>是用于“點號匹配換行符”模式的模式修飾符。
反斜杠總是會對緊跟其后的字符進行轉義,這與它在字符類之外的作用一樣。被轉義的字符可以是單個字符,也可以是一個范圍的開始或結束。另外4個元字符只有當它們被放置在特定位置時才擁有特殊含義。在使用中總是對這些元字符進行轉義會使你的正則表達式更加容易讓人理解。
字符類 |
[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]] | a 到 z,除了 b 和 c:[ad-z](減去) |
[a-z&&[^m-p]] | a 到 z,而非 m 到 p:[a-lq-z](減去) |
表3 一般字符類匹配
字母數字字符則不能使用反斜杠來轉義。
如果緊跟著左括號后面是一個脫字符(^),那么就會對整個字符類取否。也就是就它會匹配不屬于該字符類列表中的任意字符。一個否定字符類會匹配換行符號,除非把換行也加入到否定字符類中。
連字符(-)被放在兩個字符之間的時候就會創建一個范圍。該范圍所組成的字符類包含連字符之前的字符、連字符之后的字符,以及按照字母表順序位于這兩個字符之間的所有字符。
<\d>和<[\d]>都會匹配單個數字,每個小寫的簡寫都擁有一個相關聯的大寫簡定字符,其含義正好相反。因此<\D>會匹配不是數字的任意字符,所以同<[^\d]>是等價的。
<\w>會匹配單個的單詞字符(word character),所謂的單詞字符指的是能夠出現在一個單詞中的字符,這包括了字母、數字和下劃線。<\W>則會匹配不屬于上述字符集合中的任意字符。在Java、JavaScript、PCRE和Ruby中,<\w>總是和<[a-zA-Z0-9_]>的含義完全相同,而在.NET和Perl中,會包含其它字母表(泰語等)的字母和數字。
<\s>匹配任意的空白字符,其中包括了空格、制表符和換行符。在.NET、Perl和JavaScript中,<\s>也會匹配杜撰Unicode標準定義這空白號的字符。在JavaScript中對于<\s>使用的是Unicode,對<\d>和<\w>則使用ASCII標準。<\S>會匹配<\s>不能匹配的任意字符。
1.4量詞
當我們要匹配的正則表達式里面有一部分重復多次時,比如說匹配手機號或固話時,我們可以使用量詞來進行匹配固定次數或不定次數的重復。
如下面的例子:匹配一個10位的十進制數,可以使用如下正則表達式:
Matcher m = Pattern.compile("\\d{10}").matcher("0123456789");
while (m.find()) {
System.out.println(m.group());
}
運行結果為:
0123456789
正則表達式中的數量詞有Greedy (貪心)量詞、Reluctant(懶惰)量詞和Possessive(占有)量詞三種。
首先來看一個貪心量詞,如下表所示:
Greedy 數量詞 |
X? | X,一次或一次也沒有 |
X* | X,零次或多次 |
X+ | X,一次或多次 |
X{n} | X,恰好 n 次 |
X{n,} | X,至少 n 次 |
X{n,m} | X,至少 n 次,但是不超過 m 次 |
表4 貪心量詞
量詞<{n}>,其中n是一個正整數,用來重復之前的正則記號n次。
對于固定次數的重復,使用量詞<{n}>。<{1}>這樣和沒有任何量詞是等價的,<ab{1}c>和<abc>是等價的,<{0}>是重復之前的記號0次,<ab{0}c>和<ac>是同樣的正則表達式。
對于可變次數重復,我們使用量詞<{n,m}>,其中n是一個正整數,并且m大于n,至少 n 次,但是不超過 m 次。對于可變次數重復的情形,其中所有選擇分析重復的順序就會產生影響。如果n和m是相等的,那就是固定次數的重復。
量詞<{n, }>,其中n是一個正整數,支持無限次數重復。<\d{1,}>匹配一個或多個數字,<\d+>也一樣,在一個不是量詞的正則記號之后添加一個“+”,意味著一次或多次;<\d{0,}>匹配零個或多個數字,<\d*>也一樣,“*”意味著0次或多次。<h?>與<h{0,1}>的效果是一樣的,在一個合法和完整的非量詞正則記號之后的“?”意味著0或1次。
量詞還可以嵌套。<(e\d+)?>會匹配一個e之后跟著一個或是多個數字,或者是一個長度為0的匹配。
Reluctant(懶惰)量詞和Possessive(占有)量詞與Greedy (貪心)量詞基本語法類似,見下表:
Reluctant 數量詞 |
X?? | X,一次或一次也沒有 |
X*? | X,零次或多次 |
X+? | X,一次或多次 |
X{n}? | X,恰好 n 次 |
X{n,}? | X,至少 n 次 |
X{n,m}? | X,至少 n 次,但是不超過 m 次 |
Possessive 數量詞 |
X?+ | X,一次或一次也沒有 |
X*+ | X,零次或多次 |
X++ | X,一次或多次 |
X{n}+ | X,恰好 n 次 |
X{n,}+ | X,至少 n 次 |
X{n,m}+ | X,至少 n 次,但是不超過 m 次 |
表5 Reluctant(懶惰)量詞和Possessive(占有)量詞
在貪心量詞后面加上一個問號“?”可以使任何量詞變為懶惰量詞;同理在貪心量詞后面加上一個加號“+”可以使任何量詞變為占有量詞。下面來講一下幾種量詞的區別:
greedy量詞是最常用的,被看作“貪婪的”,因為它第一次就讀入整個被模糊匹配的字符串。如果第一個匹配嘗試(整個輸入字符串)失敗,匹配器就會在被匹配字符串中的最后一位后退 一個字符并且再次嘗試,重復這個過程,直到找到匹配或者沒有更多剩下的字符可以后退為止。根據表達式中使用的量詞,它最后試圖匹配的內容是1 個或者0個字符。因為總是從最大匹配開始匹配,故稱貪婪。
reluctant量詞采取相反的方式:它們從被匹配字符串的開頭開始,然后逐步地一次讀取一個字符搜索匹配,直到找到匹配或將整個字符串吞入。因為總是從最小匹配開始,故稱懶惰
possessive量詞總是讀完整個輸入字符串,嘗試一次(而且只有一次)匹配。和greedy量詞不同,possessive從不后退。
貪心量詞會找到最長的可能匹配,懶惰量詞則會找到最短的可能匹配,兩者都會進行回退,但是占有量詞不進行回退。
使用如下代碼進行驗證:
Matcher m = Pattern.compile("1.*a").matcher("12a34abcd");
System.out.println("Greedy 貪心量詞");
while (m.find()) {
System.out.println(m.group());
}
Matcher m1 = Pattern.compile("1.*?a").matcher("12a34abc");
System.out.println("Reluctant 懶惰量詞");
while (m1.find()) {
System.out.println(m1.group());
}
Matcher m2 = Pattern.compile("1.*+a").matcher("12a34abc");
System.out.println("Possessive 占有量詞");
while (m2.find()) {
System.out.println(m2.group());
}
得到的結果為:
Greedy 貪心量詞
12a34a
Reluctant 懶惰量詞
12a
Possessive 占有量詞
1.5邏輯運算符、分組與邊界匹配器
1.5.1邊界匹配器
首先我們來看一個問題,匹配My cat is brown中的cat,但是不會匹配category或是bobcat,看下面的正則表達式:
Matcher m = Pattern.compile("\\bcat\\b").matcher("My cat is brown");
while (m.find()) {
System.out.println(m.group());
}
運行結果為:
cat
在上面的正則表達式里面,我們使用到了單詞邊界匹配器。在Java里面,邊界匹配器如下表:
邊界匹配器 |
^ | 行的開頭 |
$ | 行的結尾 |
\b | 單詞邊界 |
\B | 非單詞邊界 |
\A | 輸入的開頭 |
\G | 上一個匹配的結尾 |
\Z | 輸入的結尾,僅用于最后的結束符(如果有的話) |
\z | 輸入的結尾 |
表6 邊界匹配器
正則表達式記號<\b>被稱作是一個單詞邊界,它會匹配一個單詞的開始或結束,就它自身而言,所產生的一個長度為0的匹配。
嚴格來講,<\b>會匹配如下3種位置:
在目標文本的第一個字符之前,如果第一個字符是單詞字符
在目標文本的最后一個字符之后,如果最后一個字符是單詞字符
在目標文本的兩個字符之間,其中一個是單詞字符,而另外一個不是單詞字符
<\B>會匹配在目標文本中的<\b>不匹配的第一個位置。換句話說,<\B>會匹配不屬于單詞開始或結束的每一個位置。
單詞字符就是可以在單詞中出現的字符。
JavaScript、PCRE和Ruby只把ASCII字符看做是單詞字符。<\w>因此與<[a-zA-z0-9]>是完全等同的;.NET和Perl把所有語言字母表中的字母和數字都當作單詞字符;Python則為你提供了一個選項,只有在創建正則表達式時傳遞了UNICODE或是U選項,非ASCII的字符才會被包括起來;Java則表現得不是很一致,<\w>只匹配ASCII字符,但是<\b>則是支持Unicode的,因此可以支持任何字母表。
正則表達式中的記號<^>,<$>,<\A>,<\Z>和<\z>被稱為定位符(anchor),它們并不匹配任意字符。事實上,它們匹配的特定的位置,也就是說把正則表達式這些位置來進行匹配。
JavaScript不支持<\A>。定位符<^>和<\A>是等價的,前提是不能打開“^和$匹配換行處”這個選項。對于除了Ruby之外的所有其它正則表達式流派來說,該選項都是默認關閉的。除非使用JavaScript,一般都推薦使用<\A>。<\A>的含義問題保持不變的,因此可以避免由于正則選項設置而造成的混淆或錯誤。
.NET、Java、PCRE、Perl、Ruby同時支持<\Z>和<\z>,Python只支持<\Z>,JavaScript則根本不提供對<\Z>和<\z>的支持。<\Z>和<\z>的唯一區別是當目標文本的最后一個字符是換行符的時候,在這種情形下,<\Z>可以匹配到目標文本的最結尾處,也就是在最后的換行符之后 的位置,同時也可以匹配緊跟這個換行符之前的位置;<\z>則只會匹配目標文本的最末尾處,因此如果存在一個多余的換行符,那么它無法匹配。
定位符<$>和<\Z>是等價的,前提是不能打開“^和$匹配換行處”這個選項。對于除了Ruby之外的所有其它正則表達式流派來說,該選項都是默認關閉的。
1.5.2邊界匹配器
當匹配多個選擇分支時,如匹配Mary,Jane and Sue went to Mary’s house中的Mary,Jane或Sue,使用的正則表達式為:
Matcher m = Pattern.compile("Mary|Jane|Sue").matcher("Mary,Jane and Sue went to Mary’s house ");
while (m.find()) {
System.out.println(m.group());
}
執行結果為:
Mary
Jane
Sue
Mary
豎線,或是稱作管道符號,會把正則表達式拆分成多個選擇分支,每個只會匹配一個名字,但是每次卻可以匹配不同的名字。
正則表達式里面的邏輯運算符如下表表示:
Logical 運算符 |
XY | X 后跟 Y |
X|Y | X 或 Y |
(X) | X,作為捕獲組 |
表7 Logical 運算符
上面的正則表達式還有以下問題:在匹配的過程中會匹配DJanet中的Jane。如下面的的正則表達式所示:
Matcher m = Pattern.compile("Mary|Jane|Sue").matcher("Mary,Jane and Sue went to Mary’s house DJanet");
while (m.find()) {
System.out.println(m.group());
}
執行結果為:
Mary
Jane
Sue
Mary
Jane
這個時候,可能會想到之前使用到的單詞邊界匹配器,把正則表達式修改一下,添加單詞邊界匹配:
Matcher m = Pattern.compile("\\bMary|Jane|Sue\\b").matcher("Mary,Jane and Sue went to Mary’s house DJanet");
while (m.find()) {
System.out.println(m.group());
}
執行結果為:
Mary
Jane
Sue
Mary
Jane
執行結果有也不是我們想要的答案,上面的正則表達式寫法上面有問題,應該寫成下面這樣:
Matcher m = Pattern.compile("\\bMary\\b|\\bJane\\b|\\b Sue\\b").matcher("Mary,Jane and Sue went to Mary’s house DJanet");
“|”在所有正則操作符中擁有最低的優先級。如果想要正則表達式中的一些內容不受替代操作影響的話,那么就需要把這些選擇分支進行分組,分組是通過圓括號來實現的,括號擁有在所有正則操作符中的最高優先級。
Matcher m = Pattern.compile("\\b(Mary|Jane|Sue)\\b").matcher("Mary,Jane and Sue went to Mary’s house DJanet");
while (m.find()) {
System.out.println(m.group());
}
執行結果為:
Mary
Jane
Sue
Mary
一組圓括號不僅僅是一個分組,它還是一個捕獲分組,正則表達式\b(\d\d\d\d)-(\d\d)-(\d\d)\b擁有三個捕獲分組,分組是按照左括號的順序從左到右進行編號的,(\d\d\d\d)、(\d\d)、(\d\d)分別為3個分組。
分組分為捕獲性分組和非捕獲性分組,簡單的說捕獲性分組就是捕獲分組所匹配的內容暫且存儲在某個地方,以便下次使用,捕獲性分組以(...)表示,有些地方將取得捕獲性分組所匹配結果的過程稱之為"反向引用",非捕獲性分組不捕獲分組所匹配的內容,當然也就得不到匹配的結果,非捕獲性分組以(?:...)表示,在一些只需要分組匹配但是并不需要得到各個分組匹配的結果時,使用非捕獲性分組可以提高匹配速度。
在匹配過程中,當正則表達式引擎到達右括號而退出分組的時候,它會把該捕獲分組所匹配到的文本的子串存儲起來。當我們匹配2008-08-05時,2008被保存到第一個捕獲中,08在第2個捕獲中,而05則在第3個捕獲中。
使用\b\d\d(\d\d)-\1-\1\b可以匹配像2008-08-08這樣的日期(年減去世紀、月份和該月的天數都是相同的數字),在這個正則表達式中,我們使用反向引用來在該正則表達式中的任何地方匹配相同的文本。可以使用反斜杠之后跟一個單個數字(1~9)來引用前9個捕獲分組,而第(10~99)則要使用\10~\99。
注意,不要使用\01。它或者是一個八進制的轉義,或者會產生一個錯誤。在JavaScript中此正則表達式還會匹配12—34。因為在JavaScript中,對一個還沒有參與匹配的分組的反向引用總是會匹配成功,這同捕獲了長度為0的匹配的分組的反向引用是一樣的。
1.6常用的正則表達式
正則表達式通常用于兩種任務:1.驗證,2.搜索/替換。用于驗證時,通常需要在前后分別加上^和$,以匹配整個待驗證字符串;搜索/替換時是否加上此限定則根據搜索的要求而定,此外,也有可能要在前后加上\b而不是^和$。此表所列的常用正則表達式,除個別外均未在前后加上任何限定,請根據需要,自行處理。
說明 | 正則表達式 |
網址(URL) | [a-zA-z]+://[^\s]* |
IP地址(IP Address) | ((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?) |
電子郵件(Email) | \w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)* |
QQ號碼 | [1-9]\d{4,} |
HTML標記(包含內容或自閉合) | <(.*)(.*)>.*<\/\1>|<(.*) \/> |
密碼(由數字/大寫字母/小寫字母/標點符號組成,四種都必有,8位以上) | (?=^.{8,}$)(?=.*\d)(?=.*\W+)(?=.*[A-Z])(?=.*[a-z])(?!.*\n).*$ |
日期(年-月-日) | (\d{4}|\d{2})-((0?([1-9]))|(1[1|2]))-((0?[1-9])|([12]([1-9]))|(3[0|1])) |
日期(月/日/年) | ((0?[1-9]{1})|(1[1|2]))/(0?[1-9]|([12][1-9])|(3[0|1]))/(\d{4}|\d{2}) |
時間(小時:分鐘, 24小時制) | ((1|0?)[0-9]|2[0-3]):([0-5][0-9]) |
漢字(字符) | [\u4e00-\u9fa5] |
中文及全角標點符號(字符) | [\u3000-\u301e\ufe10-\ufe19\ufe30-\ufe44\ufe50-\ufe6b\uff01-\uffee] |
中國大陸固定電話號碼 | (\d{4}-|\d{3}-)?(\d{8}|\d{7}) |
中國大陸手機號碼 | 1[358]\d{10} |
中國大陸郵政編碼 | [1-9]\d{5} |
中國大陸身份證號(15位或18位) | \d{15}(\d\d[0-9xX])? |
非負整數(正整數或零) | \d+ |
正整數 | [0-9]*[1-9][0-9]* |
負整數 | -[0-9]*[1-9][0-9]* |
整數 | -?\d+ |
小數 | (-?\d+)(\.\d+)? |