這不是入門級文章,但如果你對正則表達式有了解,或使用過,則能幫助你快速回憶。閱讀此文需要你之前使用過正則表達式或者有些瞭解,因爲我沒有寫很多的例子。總結正則表達式,只是因爲個人在幾年的積累之後,想通俗簡單的概述正則表達式中各種符號和用法。網上有很多關於正則表達式,但總感覺專業術語太多,讀起來拗口或繞彎子。最後兩小節來自正則表達式30分鐘入門教程(如果你完全不瞭解正則表達式,還是先去這看看,因爲他的例子較多,兩相比較有助深入理解),有部分修繕。
正則表達式字符串由兩種基本字符組成:原義文本字符和元字符。所謂元字符就是正則表達式中具有特殊意義的專用字符,元字符可能是一個字符,也可能是多個字符組成的一個基本單元。
元字符
一個元字符可能代表的是一個數字、字母、位置或數量。
代表字符
代碼 | 說明 |
. | 匹配除換行符以外的任意字符 |
\w | 匹配字母或數字或下劃線或漢字 |
\s | 匹配任意的空白符 |
\d | 匹配數字 |
\S | 與\s相反 |
\D | 與\d相反 |
\W | 與\w相反 |
代表數量
代碼 | 說明 |
* | 重複零次或更多次 |
+ | 重複一次或更多次 |
? | 重複零次或一次 |
{n} | 重複n次 |
{n,} | 重複n次或更多次 |
{n,m} | 重複n到m次 |
代表位置
代碼 | 說明 |
\b | 匹配單詞的開始或結束 |
\B | 不是單詞開頭或結束的開始 |
^ | 匹配字符串的開始 |
$ | 匹配字符串的結束 |
轉義字符
既然元字符在正則表達式中有特殊意義,如果現在要把它們當成一般原義文本字符,怎麼辦?在元字符前添加反斜槓\,表示當前元字符已經失去了正則表達式中的特殊意義,變成了一個原義文本字符。
預定義字符集
元字符匹配範圍太廣,如果只是想匹配某小範圍內的字符,該怎麼辦?
很簡單,使用方括號列出來即可。如[aeiou]或[.?!],表示只能匹配方括號中出現的字符。
需要注意的是:在這個方括號中,代斜槓的元字符依然是元字符,但那些沒有斜槓前綴的則都不再是元字符,但多了一個元字符,就是連字符-。
- 如果連字符在兩個字符之間,那麼就表示範圍,連字符本身不算,如[0-9], [a-z]等;
- 如果連字符後面沒有字符,則表示連字符也是預定義的字符集的字符之一,如[*%-];
在預定義字符集中,可以使用轉義字符。
反義(antonymy)
有時只需要查找不屬於某個簡單定義的字符集的字符,這就是反義。
代碼 | 說明 |
\W | 匹配任意不是字母,數字,下劃線,漢字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非數字的字符 |
\B | 匹配不是單詞開頭或結束的位置 |
[^x] | 匹配除了x以外的任意字符 |
[^aeiou] | 匹配除了aeiou這幾個字母以外的任意字符 |
或 | 分支
這相當於或語句,具體方法是用垂直線|把不同的規則分隔開。如0\d{2}-\d{8}|0\d{3}-\d{7}這個表達式能匹配兩種以連字號分隔的電話號碼:一種是三位區號,8位本地號(如010-12345678),一種是4位區號,7位本地號(0376-2233445)。
分組(group)
所謂分組,其實就是把小括號()中正則表達式作爲一個小的匹配單元。
分組的作用有兩個:
- 默認情況下,正則表達式會解析器給每一個小組分配一個組號,通過這種方式,後面的正則表達式就可以通過組號引用這個小組匹配的內容。
- 小組後可以跟代表數量的元字符,簡化正則表達式書寫;比如,(\d{1,3}\.){3}\d{1,3}是一個簡單的IP地址匹配表達式。
小組後跟代表數量的元字符有一個問題需要注意:感覺應該是生成了若干個小組,但其實就是一個分組,因爲組號是解析器解析的時候給的,代表小組的字符串在整個正則表達式字符串中只出現了一次,所以只分配了一個唯一的組號。因此,正則表達式執行完後,這個組匹配的內容是最後一次匹配的內容。
比如:
/(\d{1,3}\.){3}\d{1,3}/g.exec("201.202.203.204");
結果:["201.202.203.204", "203."]
後面的正則表達式通過組號引用前面分組匹配的內容,這個就叫後向引用。組號的規則是:從左向右,以分組的左括號爲標誌,第一個出現的分組的組號爲1,第二個爲2,依此類推。引用的時候記得在組號前加上斜槓。
比如:
/\b(\w+)\b\s+\1\b/.test("hellohello"); // true
/\b(\w+)\b\s+\1\b/.test("hellohell"); // false
分組的語法:
代碼/語法 | 說明 |
(exp) | 匹配exp,並捕獲文本到自動命名的組裏 |
(?<name>exp) | 匹配exp,並捕獲文本到名稱爲name的組裏,也可以寫成(?'name'exp) |
(?:exp) | 匹配exp,不捕獲匹配的文本,也不給此分組分配組號 |
零寬斷言
零寬斷言像\b、^、$一樣用於指定一個位置,但這個位置應該滿足一定的條件,這個條件就被稱之爲斷言。因爲這個斷言不像分組一樣有組號,不消耗匹配的字符串,所以稱之爲零寬斷言。
代碼/語法 | 說明 |
(?=exp) | 斷言這個位置的後面,一定有匹配exp的內容; |
(?<=exp) | 斷言這個位置的前面,一定有匹配exp的內容; |
(?!exp) | 斷言這個位置的後面,一定沒有匹配exp的內容 |
(?<!exp) | 斷言這個位置的前面,一定沒有匹配exp的內容 |
註釋
註釋用(?#comment)表示。
貪婪與懶惰(greedy and idle)
一段固定的正則表達式,它所能匹配的可能是整個字符串,也可能只是整個字符串的一部分。假如出現一段正則表達式既能匹配整個字符串,也能只匹配部分該如何處理?
/h.*o/.exec("hello ho"); // ["hello ho"]
/h.*?o/.exec("helloho"); // ["hello"]
以上這個例子中,匹配hello使用的是懶惰匹配,而沒有問號限定的則是使用的貪婪匹配。
所謂貪婪匹配就是儘可能多的重複,以匹配儘可能多的字符。反之,懶惰匹配就是匹配儘可能少的字符。默認是貪婪算法,只有在表示數量的元字符後使用了懶惰限定符(也就是問號)才表示這個地方使用懶惰算法。
懶惰限定符 | |
語法 | 說明 |
*? | 重複任意次,但儘可能少重複 |
+? | 重複1次或更多次,但儘可能少重複 |
?? | 重複0次或1次,但儘可能少重複 |
{n,m}? | 重複n到m次,但儘可能少重複 |
{n,}? | 重複n次以上,但儘可能少重複 |
平衡組(balance group)/遞歸匹配(recursive match)
前面講的所有匹配都是線性的,對於匹配像( 100 * ( 50 + 15 ) )這樣的可嵌套的層次性結構,則上面的一切方法都沒用。因爲你不可能知道括號什麼時候出現,而且如果左括號和右括號出現的次數不相等怎麼辦?如何匹配到最長的,配對的括號之間的內容?
這裏需要用到以下的語法構造:
- (?'group'exp) 把捕獲的內容命名爲group,並壓入堆棧(Stack)
- (?'-group'exp) 從堆棧上彈出最後壓入堆棧的名爲group的捕獲內容,如果堆棧本來爲空,則本分組的匹配失敗
- (?(group)yes|no) 如果堆棧上存在以名爲group的捕獲內容的話,繼續匹配yes部分的表達式,否則繼續匹配no部分
- (?!) 零寬負向先行斷言,由於沒有exp,試圖匹配總是失敗
爲了避免(和\(把你的大腦徹底搞糊塗,我們還是用尖括號代替圓括號吧。現在我們的問題變成了如何把xx <aa <bbb> <bbb> aa> yy這樣的字符串裏,最長的配對的尖括號內的內容捕獲出來?
< #最外層的左括號
[^<>]* #最外層的左括號後面的不是括號的內容
(
(
(?'Open'<) #碰到了左括號,在黑板上寫一個"Open"
[^<>]* #匹配左括號後面的不是括號的內容
)+
(
(?'-Open'>) #碰到了右括號,擦掉一個"Open"
[^<>]* #匹配右括號後面不是括號的內容
)+
)*
(?(Open)(?!)) #在遇到最外層的右括號前面,判斷黑板上還有沒有沒擦掉的"Open";如果還有,則匹配失敗
> #最外層的右括號
更多
常用轉義字符及其他部分元字符 | |
代碼/語法 | 說明 |
\a | 報警字符(打印它的效果是電腦嘀一聲) |
\b | 通常是單詞分界位置,但如果在字符類裏使用代表退格 |
\t | 製表符,Tab |
\r | 回車 |
\v | 豎向製表符 |
\f | 換頁符 |
\n | 換行符 |
\e | Escape |
\0nn | ASCII代碼中八進制代碼爲nn的字符 |
\xnn | ASCII代碼中十六進制代碼爲nn的字符 |
\unnnn | Unicode代碼中十六進制代碼爲nnnn的字符 |
\cN | ASCII控制字符。比如\cC代表Ctrl+C |
\A | 字符串開頭(類似^,但不受處理多行選項的影響) |
\Z | 字符串結尾或行尾(不受處理多行選項的影響) |
\z | 字符串結尾(類似$,但不受處理多行選項的影響) |
\G | 當前搜索的開頭 |
\p{name} | Unicode中命名爲name的字符類,例如\p{IsGreek} |