本文譯自 制作正則引擎的作者 Jan Goyvaerts 為工具 RegexBuddy 寫(xiě)的教程
版權(quán)歸原作者所有
注意:本頁(yè)面存在bug,有的字符被過(guò)濾。
表達(dá)式測(cè)試網(wǎng)站 https://regex101.com/
正則表達(dá)式
正則表達(dá)式是一種用來(lái)描述一定數(shù)量文本的模式。
RegEx 代表 Regular Express
正則表達(dá)式引擎
是一種可以處理正則表達(dá)式的軟件(通常引擎是更大的應(yīng)用程序的一部分)
本教程集中討論應(yīng)用最廣泛的 Perl 5 類(lèi)型的引擎。對(duì)比其他引擎的區(qū)別。
近代引擎都很類(lèi)似但不完全相同 如 .NET 正則庫(kù) JDK 正則包
文字符號(hào)
最簡(jiǎn)單的正則表達(dá)式由單個(gè)文字符號(hào)組成。
如a將匹配字符串中第一次出現(xiàn)的字符a。
匹配字符串Jack is a boy。
結(jié)果:J后的a將被匹配。而第二個(gè)a將不會(huì)被 匹配。
正則表達(dá)式也可以匹配第二個(gè)a,這必須是你告訴正則表達(dá)式引擎:從第一次匹配的地方開(kāi)始搜索。
(相當(dāng)于文本編輯器中的查找下一個(gè)。也相當(dāng)于編程中有一個(gè)函數(shù)從前一次匹配的位置開(kāi)始繼續(xù)向后搜索。)
cat會(huì)匹配About cats and dogs中的cat。
等于是告訴正則表達(dá)式引擎:
找到一個(gè)c,緊跟一個(gè)a,再跟一個(gè)t
正則表達(dá)式引擎默認(rèn)大小寫(xiě)敏感
除非你告訴引擎忽略大小寫(xiě),否則cat 不會(huì)匹配Cat。
元字符(metacharacter)
對(duì)于文字字符,有 12 個(gè)字符被保留作特殊用途,即 元字符:
[ ]\^ $. |? *+ ()
在正則表達(dá)式中,如果要將這些字符用作無(wú)特殊含義的文本字符
用反斜杠對(duì)其進(jìn)行轉(zhuǎn)義 (escape)
例如
表達(dá)式為1\+1=2
匹配1+1=2
注意正則表達(dá)式1+1=2是有效的 +表示重復(fù) 1 次到多次
它不會(huì)匹配文本1+1=2
會(huì)匹配文本123+111=234中的111=2
在編程語(yǔ)言中,一些特殊的字符會(huì)先被編譯器處理,然后再傳遞給正則引擎。
#因此正則表達(dá)式
1\+2=2
#在C++中要寫(xiě)成
1\\+1=2
#為了匹配到文本 C:\temp
#正則表達(dá)式 C:\\temp
#C++中的正則表達(dá)式 C:\\\\temp
不可顯示字符
某些不可顯示字符 使用它對(duì)應(yīng)的 特殊字符序列
\t Tab 0x09
\r 回車(chē)符 0x0D
\n 換行符 0x0A
#注意
Windows 中文本文件結(jié)束一行用\r\n
Unix 使用\n
正則表達(dá)式引擎的內(nèi)部工作機(jī)制
正則表達(dá)式引擎是如何工作的有助于你很快理解為何某個(gè)正則表達(dá)式不像你期望的那樣工作。
有2種類(lèi)型的引擎:
- 文本導(dǎo)向(text-directed)的引擎
- 正則導(dǎo)向(regex-directed)的引擎 (目前最流行,本文談的就是正則導(dǎo)向的引擎)
Jeffrey Friedl 把他們稱(chēng)作 DFA 和 NFA 引擎。
因?yàn)橐恍┓浅S杏玫奶匦灾荒茉谡齽t導(dǎo)向的引擎中實(shí)現(xiàn):
如
惰性量詞(lazy quantifiers)
反向引用(backreferences)
通過(guò)測(cè)試 判斷 某引擎 文本導(dǎo)向/正則導(dǎo)向:
如果 反向引用 或 惰性量詞,被實(shí)現(xiàn)則引擎必定是正則導(dǎo)向
正則表達(dá)式regex|regex not
對(duì)字符串regex not進(jìn)行匹配,得到結(jié)果:
結(jié)果1 regex not 則 該引擎是文本導(dǎo)向的。
結(jié)果2 regex則 該引擎是正則導(dǎo)向的。正則導(dǎo)向的引擎總是很急切地報(bào)告它找到的第一個(gè)匹配
正則導(dǎo)向的引擎 -> 急切的返回第1個(gè)(即最左邊)匹配結(jié)果
這是需要你理解的很重要的一點(diǎn),即使以后有可能發(fā)現(xiàn)更好的匹配,正則導(dǎo)向的引擎也總是返回最左邊的匹配。
例
正則表達(dá)式cat
文本He captured a catfish for his cat
詳細(xì)匹配過(guò)程:
引擎先比較正則表達(dá)式符號(hào)c和H,匹配失??;
于是引擎再比較正則表達(dá)式符號(hào)c和e,匹配失敗;
... 直到匹配第4個(gè)字符c,匹配成功;
正則表達(dá)式符號(hào)a匹配第5個(gè)字符a,匹配成功;
第六個(gè)字符t匹配p,匹配失敗;
引擎繼續(xù)從第5個(gè)字符重新檢查匹配性
...直到第15個(gè)字符開(kāi)始,cat匹配上了catfish 中的cat
就此結(jié)束。正則表達(dá)式引擎急切返回第1個(gè)匹配結(jié)果,而不會(huì)再繼續(xù)向后查找(無(wú)論是否有其他文本可以被匹配
字符集
字符集是由一對(duì)方括號(hào)[]括起來(lái)的字符集合。
使用字符集,告訴正則表達(dá)式引擎僅匹配多個(gè)字符中的一個(gè)。
使用正則表達(dá)式[ae]
可匹配一個(gè)a或一個(gè)e
字符集中的字符順序并沒(méi)有意義,結(jié)果都是相同的。
如
使用正則表達(dá)式gr[ae]y或等價(jià)的gr[ea]y
都可匹配 gray 或 grey (在你不確定待匹配的文本是 美/英 英語(yǔ)時(shí)特別有用)
都不會(huì)匹配 graay 或 graey
用帶連字符-的字符集 定義字符范圍
如 字符集 [0-9]匹配 0 到 9 之間的某個(gè)單個(gè)數(shù)字。
使用多個(gè)范圍的字符集
如
匹配某個(gè)單個(gè)的十六進(jìn)制的字符 且 大小寫(xiě)不敏感
[0-9a-fA-F]
同時(shí)使用:結(jié)合范圍定義 與 單個(gè)字符定義
例
[0-9a-fxA-FX]
如
匹配某個(gè)單個(gè)的十六進(jìn)制的字符 且 大小寫(xiě)不敏感,或字符X
再次強(qiáng)調(diào),字符和范圍定義的先后順序 對(duì)結(jié)果沒(méi)有影響。
字符集的一些應(yīng)用
查找一個(gè)可能有拼寫(xiě)錯(cuò)誤的單詞
比如sep[ae]r[ae]te 或 li[cs]en[cs]e
查找程序語(yǔ)言的標(biāo)識(shí)符
A-Za-z_][A-Za-z_0-9]*
*表示重復(fù) 0 或多次
查找 C 風(fēng)格的十六進(jìn)制數(shù)
0[xX][A-Fa-f0-9]+
+表示重復(fù)(上一個(gè)字符或表達(dá)式)一次或無(wú)數(shù)次
取反字符集
在左方括號(hào)[后面緊跟一個(gè)^將會(huì)對(duì)字符集取反,這樣的正則表達(dá)式表示將匹配任何不在方括號(hào)中的字符。
注意 取反字符集一定會(huì)要 匹配一個(gè)字符,可以匹配回車(chē)換行符
例1
匹配一個(gè) q,后面跟著一個(gè)非 u 的字符
正則表達(dá)式 q[^u]
文本
ffffq
fffff
結(jié)果
q 和一個(gè)換行符 (非 u 的字符回車(chē)換行符 是匹配結(jié)果中的一部分)
例2
正則表達(dá)式:q[^u]
文本: Iraq
進(jìn)行匹配 得不到任何結(jié)果
因?yàn)閝后沒(méi)有任何字符!單個(gè)q不會(huì)被匹配
如果只想匹配某些條件下的 q字符,如條件是 q 后面有一個(gè)非 u 的字符時(shí)才匹配這個(gè)q字符,則用后面講到的向前查看。
字符集中的元字符
注意,在字符集中只有 4 個(gè) 元字符] \ ^ -
]代表字符集定義的結(jié)束
\代表轉(zhuǎn)義
^代表取反
-代表范圍定義
除了這4個(gè)元字符外的其他常見(jiàn)元字符,在字符集定義內(nèi)部都是文本字符,不需要轉(zhuǎn)義(如果對(duì)那些通常的元字符進(jìn)行轉(zhuǎn)義,正則表達(dá)式一樣會(huì)工作得很好,但會(huì)降低人類(lèi)可讀性)
例,要匹配文本 星號(hào)*或加號(hào)+
正則表達(dá)式 [+*]
在字符集定義中,為了將反斜杠作為一個(gè)文字字符,而非特殊含義的字符,你需要用另一個(gè)反斜杠對(duì)它進(jìn)行轉(zhuǎn)義。
正則表達(dá)式[\\x]
會(huì)匹配到文本\x
元字符]^-都可以用反斜杠進(jìn)行轉(zhuǎn)義,或?qū)⑺鼈兎旁谝粋€(gè)不可能使用到他們特殊含義的位置(這樣可以增加可讀性)
比如對(duì)于字符^ 將它放在非左括號(hào)[后面的位置,使用的是文本字符含義,而非取反含義。
如
[x^]會(huì)匹配一個(gè)字符 x 或^
[]x]會(huì)匹配一個(gè)字符 ]或x
[-x]或[x-] 會(huì)匹配一個(gè)-或x
字符集的簡(jiǎn)寫(xiě)【記憶】
很常用的字符集有簡(jiǎn)寫(xiě)方式
\d代表數(shù)字[0-9]
\w代表單詞字符,隨正則表達(dá)式實(shí)現(xiàn)的不同,有些差異。絕大多數(shù)的正則表達(dá)式實(shí)現(xiàn)的單詞字符集都包含了A-Za-z0-9_
\s代表空白字符
隨正則表達(dá)式實(shí)現(xiàn)的不同而有些差異。在絕大多數(shù)的實(shí)現(xiàn)中,都包含了空格符和 Tab 符,以及回車(chē)換行符\r\n
方括號(hào)內(nèi)外都可以用字符集的縮寫(xiě)形式
\s\d匹配 某單個(gè)白字符后面緊跟一個(gè)數(shù)字
[\s\d]匹配 某單個(gè)白字符或數(shù)字
[\da-fA-F]匹配一個(gè)十六進(jìn)制數(shù)字
取反字符集的簡(jiǎn)寫(xiě)形式
[\S] 即 [^\s]
[\W] 即 [^\w]
[\D] 即 [^\d]
字符集的重復(fù)
如果用?*+操作符來(lái)重復(fù)一個(gè)字符集,將會(huì)重復(fù)整個(gè)字符集(而不是它匹配的那個(gè)字符)
例
正則表達(dá)式[0-9]+
會(huì)匹配文本 837 以及 222
如果你僅僅想重復(fù)被匹配的那個(gè)字符,可以用向后引用。以后說(shuō)
使用?*或+ 進(jìn)行重復(fù)
?
告訴引擎匹配前導(dǎo)字符 0 次或一次。事實(shí)上是表示前導(dǎo)字符是可選的。
告訴引擎匹配前導(dǎo)字符 1 次或多次
告訴引擎匹配前導(dǎo)字符 0 次或多次
例
<[A-Za-z][A-Za-z0-9]*>
匹配沒(méi)有屬性的 HTML 標(biāo)簽,<以及>是文字符號(hào)。
第一個(gè)字符集匹配一個(gè)字母,第二個(gè)字符集匹配一個(gè)字母或數(shù)字。
- 代表匹配前導(dǎo)字符0或無(wú)數(shù)次,0次時(shí)可以匹配像<p>這樣的標(biāo)簽。
我們似乎也可以用正則表達(dá)式 <[A-Za-z0-9]+>
但它會(huì)匹配<1>
限制重復(fù)次數(shù)
對(duì)前導(dǎo)子表達(dá)式重復(fù)多少次 語(yǔ)法{min,max}
min 和 max 都是非負(fù)整數(shù)(整數(shù)min>=0 整數(shù)max>=0)
例1
{0,1} 對(duì)前導(dǎo)子表達(dá)式重復(fù)0次 或 1次
{1} 對(duì)前導(dǎo)子表達(dá)式重復(fù)1次 如果逗號(hào)和max都省略時(shí)則重復(fù) min 次
{0,}等價(jià)于* 有逗號(hào) 不寫(xiě)max值 表示 max值 無(wú)限大
{1,}等價(jià)于+
例2
正則表達(dá)式zo+能匹配zo和zoo. 但不能匹配z
正則表達(dá)式\b[1-9][0-9]{3}\b (\b表示單詞邊界)
匹配某個(gè) 1000~9999 之間的數(shù)字。
正則表達(dá)式\b[1-9][0-9]{2,4}\b
匹配某個(gè) 100~99999 之間的數(shù)字。
注意: 懶惰指匹配結(jié)果盡可能短、貪婪是匹配結(jié)果盡可能長(zhǎng)
懶惰的? 匹配盡可能短的文本
貪婪的+ 匹配盡可能長(zhǎng)的文本
例
用一個(gè)正則表達(dá)式匹配一個(gè) HTML 標(biāo)簽。
輸入一個(gè)正常有效的 HTML 文件(不需要排除無(wú)效標(biāo)簽),所以如果是在兩個(gè)尖括號(hào)之間的內(nèi)容,就應(yīng)該是一個(gè) HTML 標(biāo)簽。
貪婪的正則表達(dá)式<.+> 匹配結(jié)果盡可能長(zhǎng)
許多新手用表達(dá)式<.+>
測(cè)試字符串This is a <EM>first</EM> test
得到匹配結(jié)果<EM>first</EM>很顯然不是我們想要的結(jié)果。
我們期望得到結(jié)果<EM> 繼續(xù)進(jìn)行匹配 得到</EM>
因?yàn)?code>+是貪婪的 即匹配盡量長(zhǎng)的文本!
+會(huì)導(dǎo)致正則表達(dá)式引擎試圖盡可能的重復(fù)它前面的那個(gè)字符(只有當(dāng)這種重復(fù)會(huì)引起整個(gè)正則表達(dá)式匹配失敗的情況下,引擎會(huì)進(jìn)行回溯,即放棄最后一次的重復(fù),然后處理正則表達(dá)式余下的部分)
和+類(lèi)似,?*的重復(fù)也是貪婪的。
詳細(xì)匹配過(guò)程:
< 匹配字符<,匹配成功;
.匹配了字符E,匹配成功;
+重復(fù)上一個(gè)(表達(dá)式符號(hào)),即表達(dá)式. 點(diǎn)號(hào) 可以一直匹配接下來(lái)的字符,直到碰到換行符,.不匹配換行符則.匹配失敗,即+匹配失??;
此時(shí)已匹配到的文本為<EM>first</EM> test
引擎對(duì)正則表達(dá)式最后一個(gè)符號(hào)>進(jìn)行匹配,試圖將>與換行符進(jìn)行匹配,>匹配失敗;
此時(shí)已匹配到的文本為<EM>first</EM> test
表達(dá)式最后一個(gè)符號(hào)>也匹配失敗了,引擎進(jìn)行回溯(從向后匹配改為向前匹配)
>試圖與t匹配,匹配失敗;
>繼續(xù)往前匹配,繼續(xù)失敗..
直到遇見(jiàn)<EM>first</EM后面的字符>,匹配成功!
第1個(gè)匹配結(jié)果為
<EM>first</EM>
正則導(dǎo)向的引擎是急切的,所以它會(huì)急著報(bào)告它找到的第一個(gè)匹配。而不是繼續(xù)回溯(所以匹配不到<EM>了)
能看出+的貪婪性使正則表達(dá)式引擎得到了一個(gè)“最長(zhǎng)”的匹配結(jié)果。
可行方案1 使用“懶惰的”正則表達(dá)式<.+?>匹配標(biāo)簽
在+后面緊跟一個(gè)問(wèn)號(hào)?實(shí)現(xiàn)懶惰性(匹配盡量短的文本)
*``{}``?表示的重復(fù)也可以用類(lèi)似的方案,使貪婪性變成懶惰性,匹配盡量短的文本。
? 匹配盡量短的文本
+? 匹配盡量短的文本!實(shí)現(xiàn)了"懶惰性" 帶上問(wèn)號(hào)的+是懶惰的!
+為了盡可能少地重復(fù)上一個(gè)字符. 所以匹配成功后立即用下一個(gè)正則表達(dá)式符號(hào)進(jìn)行匹配
正則表達(dá)式<.+?>詳細(xì)匹配過(guò)程:
正則表達(dá)式符號(hào).匹配文本中的下一個(gè)字符E 匹配成功;
懶惰的+?為了盡快得到匹配結(jié)果,用下一個(gè)正則表達(dá)式符號(hào)>匹配文本中下一個(gè)字符M,匹配失?。ㄒ孢M(jìn)行回溯)
用上一個(gè)正則表達(dá)式字符.匹配當(dāng)前字符M,匹配成功;
此時(shí)正則表達(dá)式進(jìn)行到<.+
此時(shí)已匹配到的文本為<EM
懶惰的+?為了盡快得到匹配結(jié)果,立即用下一個(gè)正則表達(dá)式符號(hào)>匹配文本中下一個(gè)字符>匹配成功;
正則表達(dá)式結(jié)束
引擎報(bào)告得到一個(gè)成功的匹配<EM>
更好的替代方案:取反字符集
正則導(dǎo)向的引擎
用一個(gè)取反字符集 跟 一個(gè)貪婪重復(fù) 加號(hào)<[^>]+>
之所以說(shuō)這是一個(gè)更好的方案,因?yàn)槭褂萌》醋址?,不需要進(jìn)行回溯
在于使用惰性重復(fù)時(shí),引擎會(huì)在找到一個(gè)成功匹配前對(duì)每一個(gè)字符進(jìn)行回溯。而
使用.匹配任意字符 (除了換行符)
在正則表達(dá)式中.是最常用也最容易被誤用的符號(hào)。
.匹配任意單個(gè)字符(唯一不匹換行符)
在本教程中談到的引擎,默認(rèn)都不匹配換行符
默認(rèn).等價(jià)于
字符集[^\n\r](Windows)
字符集[^\n](Unix)
不匹配換行符是因?yàn)闅v史的原因:
早期使用正則表達(dá)式的工具是基于行的。它們都是一行一行的讀入一個(gè)文件,將正則表達(dá)式分別應(yīng)用到每一行上去。
所以每一行的字符串里當(dāng)然沒(méi)有換行符。
所以.不匹配換行符。
現(xiàn)代的工具和語(yǔ)言能夠?qū)⒄齽t表達(dá)式應(yīng)用到很大的字符串,甚至整個(gè)文件,所有正則表達(dá)式實(shí)現(xiàn)都提供一個(gè)選項(xiàng):可讓.匹配所有的字符(包括換行符)
RegexBuddy,EditPad Pro 或 PowerGREP 等工具中,你可以簡(jiǎn)單的選中點(diǎn)號(hào)匹配換行符。
在 Perl 中,.可以匹配換行符的模式被稱(chēng)作單行模式,這是一個(gè)很容易混淆的名詞。因?yàn)檫€有多行模式。
多行模式只影響行首行尾的錨定(anchor),而單行模式只影響.
其他語(yǔ)言和正則表達(dá)式庫(kù)也采用了 Perl 的術(shù)語(yǔ)定義。
當(dāng)在.NET Framework 中使用正則表達(dá)式類(lèi)時(shí),可用類(lèi)似下面的語(yǔ)句來(lái)激活單行模式:
Regex.Match(string,regex,RegexOptions.SingleLine)
強(qiáng)大的點(diǎn)號(hào).
點(diǎn)號(hào)可以說(shuō)是最強(qiáng)大的元字符:用一個(gè)點(diǎn)號(hào),匹配幾乎所有的字符。
問(wèn)題是 它也常常會(huì)匹配不該匹配的字符!
例子
匹配一個(gè)具有mm/dd/yy格式的 月/天/年 日期
允許用戶來(lái)選擇分隔符。
不可行方案1
\d\d.\d\d.\d\d
它能匹配日期02/12/03
問(wèn)題是 02512703 也會(huì)被匹配
不可行方案2
\d\d[-/.]\d\d[-/.]\d\d
稍微好一點(diǎn),支持3種分隔符。
(點(diǎn)號(hào)在一個(gè)字符集里,不作元字符)
這個(gè)方案遠(yuǎn)不夠完善,它會(huì)匹配99/99/99
方案3
[0-1]\d[-/.][0-3]\d[-/.]\d\d
更好一點(diǎn),盡管會(huì)匹配19/39/99
...
如果你想校驗(yàn)用戶輸入,則需要盡可能的完美。
如果你只是想分析一個(gè)已知的源,并且我們知道沒(méi)有錯(cuò)誤的數(shù)據(jù),用一個(gè)比較好的正則表達(dá)式,來(lái)匹配你想要搜尋的字符就已經(jīng)足夠。
字符串開(kāi)始的錨定^ 和 結(jié)束的錨定$
錨定和一般的正則表達(dá)式符號(hào)不同,錨定不匹配任何字符。
錨定匹配的是字符之前或之后的位置
正則表達(dá)式^會(huì)匹配一行字符串第一個(gè)字符前的位置
正則表達(dá)式^a會(huì)匹配字符串abc中的 a
正則表達(dá)式^b不會(huì)匹配abc中的任何字符
類(lèi)似的,$匹配字符串中最后一個(gè)字符的后面的位置
#正則表達(dá)式
c$
#匹配文本abc中的c
錨定的應(yīng)用
使用錨定在編程語(yǔ)言中校驗(yàn)用戶輸入 非常重要的
校驗(yàn)用戶的輸入為整數(shù) 用正則表達(dá)式^\d+$
匹配用戶輸入中常常存在的多余的前導(dǎo)空格或結(jié)束空格
^\s*匹配字符串前面的空格
\s*$匹配字符串末尾的空格
如果你有一個(gè)包含了多行的字符串 如
first line\n\rsecond line
(其中\(zhòng)n\r 表示一個(gè)換行符)
經(jīng)常需要按行處理,而不是整個(gè)字符串。
幾乎所有的正則表達(dá)式引擎都提供一個(gè)選項(xiàng),可以擴(kuò)展這兩種錨定的含義。
^可以匹配字串的開(kāi)始位置(在 f 之前),以及每一個(gè)換行符后面的位置(這個(gè)位置在\n\r 和 s 之間)
$會(huì)匹配字串的結(jié)束位置(最后一個(gè) e之后),以及每個(gè)新行符之前的位置(這個(gè)位置在 e 與\n\r 之間)
在.NET 中使用如下代碼定義錨定,匹配每一個(gè)換行符的前面和后面位置:
Regex.Match("string", "regex", RegexOptions.Multiline)
例
在每行的行首插入文本字符>
string str = Regex.Replace(Original, "^", ">", RegexOptions.Multiline)
絕對(duì)錨定
\A只匹配整個(gè)字符串的開(kāi)始位置
\Z只匹配整個(gè)字符串的結(jié)束位置
即使你使用了多行模式,這兩個(gè)也絕不匹配換行符。
例外
如果字符串以換行符(newline)結(jié)束,則\Z和$將會(huì)匹配這個(gè)最后的換行符前面的位置,而不是整個(gè)字符串的最后面。
這個(gè)改進(jìn)是由 Perl 引進(jìn)的,然后被許多的正則表達(dá)式實(shí)現(xiàn)所遵循,包括 Java,.NET 等。
例
正則表達(dá)式:^[a-z]+$
文本:joe\n
匹配結(jié)果:joe而不是joe\n