正則表達式(一):糾結的轉義
用過正則表達式的人都知道,正則表達式中有一類叫做“元字符(meta-character)”的特殊符號,它們并不匹配自身對應的字符,而具有其他的含義。比如脫字符『^』表示“定位到字符串/行的開頭”,加號『+』表示“之前的元素重現1次以上。如果需要匹配這些字符本身,需要用反斜線來轉義,匹配『^』就應該用\^,匹配『+』就應該用\+。
看起來有點麻煩,但這樣的元字符并不多:^$()*+?.[\{|
元字符 |
說明 |
舉例 |
^ |
匹配整個字符串的起始位置,或者行的起始位置,如果在字符組內部,則表示排除型(negative)字符組 |
^Start |
$ |
匹配整個字符串的結束位置,或者行的結束位置 |
End$ |
() |
分組,提供反向引用(gourp1) \1或多選分支 |
(ab)+ |
* + ? |
量詞,限定之前元素出現的次數 |
a+ (ab)+ |
. |
默認情況下匹配換行符之外的任意字符,在多行模式下可以匹配換行符 |
|
[ |
字符組的起始符號 |
[0-9] |
\ |
反斜線用來表示轉義序列,或去掉元字符的轉義 |
\1 |
{ |
重現限定符的開始 |
{2, 6} |
| |
劃分多選分支(括號沒有出現時,可以想象括號出現在整個表達式最外層) |
Tom|Jerry |
你或許注意到了,這些元字符并不是“對稱”出現的,比如與開方括號 [ 對應的閉方括號 ],與開花括號 { 對應的閉花括號 } ,這兩個字符是否元字符,需要依據具體正則表達式的情況確定,我們以閉方括號]的情況為例(}的情況與此類似):如果之前能找到與之對應的元字符開方括號[,則]作為元字符出現,否則,作為普通字符出現。
字符串 \ 表達式 |
[ab]] |
ab] |
a] |
√ |
|
ab] |
√ |
另外,因為方括號本身可以表示字符組『[0-9]』,所以在字符組內部的閉方括號在任何情況下都要轉義,否則類似『[]]』的正則表達式會出現二義性,造成識別錯誤。
如果需要匹配方括號內(包括方括號),至少包含一個字符的字符串(比如[text]),所用的正則表達式就應該是:『\[[^\]]+]』。
看明白了嗎?『\[』匹配開方括號,然后用一個排除型字符組匹配“除閉方括號 ] 之外的任意字符(注意,在字符組內部,閉方括號 ] 一定需要轉義),用『+』表示它至少要出現一次以上,最后用一個『]』匹配閉方括號。
下面用代碼來驗證,以python為例:
import re #為使用正則表達式,必須首先導入re
>>> re.search('^\[[^\]]+]$', '[abcdefg]') #進行數據驗證時,在表達式首尾加上^和$是好習慣
<_sre.SRE_Match object at 0x7ff3bc5e75e0>
>>> re.search('^\[[^\]]+]$', '[]')
>>>
看來確實沒有問題,下面用Java試試。直接調用Java中的string.matches(regex)方法,觀察返回的boolean值:
"[]".matches("^\[[^\]]+]$")
但是卻出現了編譯錯誤:invalid escape sequence。這是為什么呢?在Python中我們并沒有使用raw string(如果使用raw string,就應該用r"^\[[^\]]+]$"),一切正常,可是在Java中為什么會出錯呢?
要回答這個問題,就得分清轉義的層次和規則。如果你留心觀察就會發現,上面我們講的都是“正則表達式的轉義”,比如『\[[^\]]+]』是正確轉義的正則表達式。僅僅用做正則表達式,它是絕對沒有問題的,但它“不僅僅”是正則表達式,而是“字符串形式給出的正則表達式”——注意到了嗎?在表達式兩端,各有一個雙引號。
回憶一下Java中字符串(String)的規則,其中轉義序列(escape sequence)用來表示特殊字符,比如\n表示換行符,\t表示制表符,而\[并不是Java能識別的轉義序列,當然要出錯了。為了表示“正則表達式中的\[”,我們傳遞給Pattern.compile()的字符串必須正確表示\[——在字符串中,[ 是不需要轉義的,而 \ 是需要轉義的,所以在字符串中,應該寫做 \\[。
總結一下:
字符串的表現層 |
\\[ |
字符串的概念層 |
\[ |
正則表達式的表現層 |
\[ |
正則表達式的概念層 |
[(非元字符) |
理解了這一點,就不難理解為什么正則表達式的轉義序列在正則表達式中要寫兩個反斜線了,比如 \+ 要寫成 \\+ 。但是 \n 之類的有點特殊,無論你寫成 \n 或是 \\n ,結果都是一樣,\t之類的情況與此類似。
字符串的表現層 |
\\n |
\n |
字符串的概念層 |
\n |
換行符 |
正則表達式的表現層 |
\[ |
換行符 |
正則表達式的概念層 |
換行符 |
換行符 |
如果字符串中表示反斜線字符本身(不是用來轉義的符號),則需要在正則表達式中寫四個反斜線字符。
"\".matches("\\\\"); //true
字符串的表現層 |
\\\\ |
字符串的概念層 |
\\ |
正則表達式的表現層 |
\\ |
正則表達式的概念層 |
\(非元字符) |
看起來,轉義問題似乎就是這樣,想明白了也很簡單。不過,如果你記憶力比較好,估計會問:為什么在Python中寫\[不會報錯,而Java中會報錯?這確實是個好問題,所以我們把它當成本文的結束。
照道理說,各種語言的轉義規則都一樣:\n表示換行符,\t表示制表符…… 事實也確實如此,只是Python對字符串的處理更復雜一些:如果一個轉義序列不能識別,會直接原樣保存到字符串中。也就是說,Python遇到無法識別字符串中的\[,不會報錯,而是將它原樣“轉交”給字符串:
字符串的表現層 |
\[ |
\\[ |
字符串的概念層 |
\[ |
\[ |
正則表達式的表現層 |
\[ |
\[ |
正則表達式的概念層 |
[(非元字符) |
[(非元字符) |
“無法識別的轉義序列直接轉交字符串”的做法不只Python有,PHP也會這樣處理,但是我并不推薦這樣使用,因為它往往會令不理解這特性的人困惑,正則表達式對應的字符串中出現\[如何不會報錯?\[和\\[為什么竟然是一樣的效果?
最好的辦法或許還是統一表示法,都寫成\\[,既方便與其它語言兼容,也方便大家閱讀和理解。
關于作者
余晟,程序員,曾任抓蝦網高級顧問,現就職于盛大創新院,感興趣的方向包括搜索和分布式算法等。翻譯愛好者,譯有《精通正則表達式》(第三版)和《技術領導之路》,目前正在寫作《正則表達式傻瓜書》(暫定名),希望為國內開發同行貢獻一本實用的正則表達式教程。