正則表示式文件邊學邊練,一小時輕鬆學會
最近在做爬蟲,一直很頭疼正則表示式,不會寫也不會認,這次終於下定決心來學習一下,並做一下總結整理。
到目前為止,正則表示式的功能類似於
- 常規的Find功能
- Java中的
String.indexOf()
函式 - PHP中的
strpos()
函式 - 等等
字元
任意字元
.
~~~
這個符號意味著可以匹配任意一個字元。
c.t : 匹配“以c開頭,之後是任意一個字元,緊跟著是字母t”的字串(如cat,cot,cct,但是不能是ct或者coot這樣的字串)
使用反斜槓“\”可以忽略元字元,使得元字元的功能與普通字元一樣。所以,
c.t : 匹配“以c開頭,然後是一個句號(“.”),緊跟著字母t”的字串
反斜槓本身也是一個元字元,這意味著反斜槓本身也可以通過相似的方法變回到普通字元的用途。因此,
c\t : 匹配“以字元c開頭,然後是一個反斜槓,緊跟著是字母t”的字串注意!在正則表示式的實現中,.是不能用於匹配換行符的。“換行符”的表示方法在不同實現中也不同。實際程式設計時,請參考相關文件。在本文中,預設“.”是可以匹配任意字元的。實現環境通常會提供一個Flag標誌位,來控制這一點。
~~~字元類
[]
~~~
字元類是一組在方括號內的字元,表示可以匹配其中的任何一個字元。正則表示式c[aeiou]t,表示可以匹配的字串是”以c開頭,接著是aeiou中的任何一個字元,最後以t結尾”。在文字的實際應用中,這樣的正則表示式可以匹配:cat,cet,cit,cot,cut五種字串。
正則表示式[0123456789]表示匹配任意一個整數。
正則表示式[a]表示匹配單字元a。
包含忽略字元的例子[a]表示匹配字串[a]
[[]\ab]表示匹配的字元為”[“或者”]”或者”a”,或者”b”
[\[]]表示匹配的字元為”\”或者 “[”或者”]”
在字元類中,字元的重複和出現順序並不重要。[dabaaabcc]與[abcd]是相同的重要提示:字元類中和字元類外的規則有時不同,一些字元在字元類中是元字元,在字元類外是普通字元。一些字元正好相反。還有一些字元在字元類中和字元類外都是元字元,這要視情況而定!
比如,.表示匹配任意一個字元,而[.]表示匹配一個全形句號。這不是一回事!
~~~字元類的範圍
-
在字符集中,你可以通過使用短橫線“-”來表示匹配字母或數字的範圍。 [b-f]與[b,c,d,e,f]相同,都是匹配一個字元”b”或”c”或”d”或”e”或”f” [A-Z]與[ABCDEFGHIJKLMNOPQRSTUVWXYZ]相同,都是匹配任意一個大寫字母。 [1-9]與[123456789]相同,都是匹配任意一個非零數字。 練習 使用目前我們已經講解的正則表示式相關知識,在字典中匹配找到含有最多連續母音的單詞,同時找到含有最多連續子音的單詞。 答案 [aeiou
字元類的反義
^
你可以在字元類的起始位放一個反義符。 [^a]表示匹配任何不是“a”的字元 [^a-zA-Z0-9]表示匹配任何不是字母也不是數字的字元 [\^abc]匹配一個為“^”或者a或者b或者c的字元 [^\^]表示匹配任何不為“^”的字元 練習 在字典中,找到一個不滿足“在e之前有i,但是沒有c”的例子。 答案 cie和[^c]ei都要可以找到很多這樣的例子,比如ancient,science,viel,weigh
轉義字元類
\
~~~
\d這個正則表示式與[0-9]作用相同,都是匹配任何一個數字。(要匹配\d,應該使用正則表示式\d)\w與[0-9A-Za-z]相同,都表示匹配一個數字或字母字元
\s意味著匹配一個空字元(空格,製表符,回車或者換行)
另外
\D與[^0-9]相同,表示匹配一個非數字字元。
\W與[^0-9A-Za-z]相同,表示匹配一個非數字同時不是字母的字元。
\S表示匹配一個非空字元。
這些是你必須掌握的字元。你可能已經注意到了,一個全形句號“.”也是一個字元類,可以匹配任意一個字元。很多正則表示式的實現中,提供了更多的字元類,或者是標誌位在ASCII碼的基礎上,擴充套件現有的字元類。
特別提示:統一字符集中包含除了0至9之外的更多數字字元,同樣的,也包含更多的空字元和字母字元。實際使用正則表示式時,請仔細檢視相關文件。
練習
簡化正則表示式 [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].答案
\d\d\d\d-\d\d-\d\d.
~~~重複
{ }
~~~
在字元或字符集之後,你可以使用{ }大括號來表示重複正則表示式a{1}與a意思相同,都表示匹配字母a
a{3}表示匹配字串“aaa”
a{0}表示匹配空字串。從這個正則表示式本身來看,它毫無意義。如果你對任何文字執行這樣的正則表示式,你可以定位到搜尋的起始位置,即使文字為空。
a{2}表示匹配字串“a{2}”
在字元類中,大括號沒有特殊含義。[{}]表示匹配一個左邊的大括號,或者一個右邊的大括號練習
簡化下面的正則表示式z…….z
\d\d\d\d-\d\d-\d\d
[aeiou][aeiou][aeiou][aeiou][aeiou][aeiou]
[bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz]
答案
z.{7}z
\d{4}-\d{2}-\d{2}
[aeiou]{6}
[bcdfghjklmnpqrstvwxyz]{10}
注意:重複字元是沒有記憶性的,比如[abc]{2}表示先匹配”a或者b或者c”,再匹配”a或者b或者c”,與匹配”aa或者ab或者ac或者ba或者bb或者bc或者ca或者cb或者cc“一樣。[abc]{2}並不能表示匹配”aa或者bb或者cc“
~~~指定重複次數範圍
{0,5}
~~~
重複次數是可以指定範圍的x{4,4}與x{4}相同
colou{0,1}r表示匹配colour或者color
a{3,5}表示匹配aaaaa或者aaaa或者aaa
注意這樣的正則表示式會優先匹配最長字串,比如輸入 I had an aaaaawful day會匹配單詞aaaaawful中的aaaaa,而不會匹配其中的aaa。重複次數是可以有範圍的,但是有時候這樣的方法也不能找到最佳答案。如果你的輸入文字是I had an aaawful daaaaay那麼在第一次匹配時,只能找到aaawful,只有再次執行匹配時才能找到daaaaay中的aaaaa.
重複次數的範圍可以是開區間
a{1,}表示匹配一個或一個以上的連續字元a。依然是匹配最長字串。當找到第一個a之後,正則表示式會嘗試匹配儘量多個的連續字母a。
.{0,}表示匹配任意內容。無論你輸入的文字是什麼,即使是一個空字串,這個正則表示式都會成功匹配全文並返回結果。
練習
使用正則表示式找到雙引號。要求輸入字串可能包含任意個字元。調整你的正則表示式使得在一對雙引號中間不再包含其他的雙引號。
答案
“.{0,}”, 然後 “[^”]{0,}”.
~~~關於重複的轉義字元
?*+
~~~
?與{0,1}相同,比如,colou?r表示匹配colour或者color與{0,}相同。比如,.表示匹配任意內容
+與{1,}相同。比如,\w+表示匹配一個詞。其中”一個詞”表示由一個或一個以上的字元組成的字串,比如_var或者AccountName1.
這些是你必須知道的常用轉義字元,除此之外還有:
1.\?*+ 表示匹配字串”?*+”
2.[?+]表示匹配一個問號,或者一個號,或者一個加號練習
簡化下列的正則表示式:“.{0,}” and “[^”]{0,}”
x?x?x?
y*y*
z+z+z+z+
答案
“.” and “[^”]”
x{0,3}
y*
z{4,}
練習
寫出正則表示式,尋找由非字母字元分隔的兩個單詞。如果是三個呢?六個呢?\w+\W+\w+, \w+\W+\w+\W+\w+, \w+\W+\w+\W+\w+\W+\w+\W+\w+\W+\w+.
下文中,我們將簡化這個正則表示式。
~~~非貪婪匹配
句尾加?
~~~
正則表示式 “.*” 表示匹配雙引號,之後是任意內容,之後再匹配一個雙引號。注意,其中匹配任意內容也可以是雙引號。通常情況下,這並不是很有用。通過在句尾加上一個問號,可以使得字串重複不再匹配最長字元。\d{4,5}?表示匹配\d\d\d\d或者\d\d\d\d\d。也就是和\d{4}一樣
colou??r與colou{0,1}r相同,表示找到color或者colour。這與colou?r一樣。
“.*?”表示先匹配一個雙引號,然後匹配最少的字元,然後是一個雙引號,與上面兩個例子不同,這很有用。
~~~選擇匹配
|
~~~
你可以使用|來分隔可以匹配的不同選擇:cat|dog表示匹配”cat”或者”dog”
red|blue|以及red||blue以及|red|blue都表示匹配red或者blue或者一個空字串
a|b|c與[abc]相同
cat|dog||表示匹配”cat”或者”dog”或者一個分隔符”|“
[cat|dog]表示匹配a或者c或者d或者g或者o或者t或者一個分隔符“|”練習
簡化下列正則表示式:s|t|u|v|w
aa|ab|ba|bb
[abc]|[^abc]
[^ab]|[^bc]
[ab][ab][ab]?[ab]?
答案[s-w]
[ab]{2}
.
[^b]
[ab]{2,4}
練習
使用正則表示式匹配1到31之間的整數,[1-31]不是正確答案!這樣的正則表示式不唯一. [1-9]|[12][0-9]|3[01] 是其中之一。
~~~分組
( )
~~~
你可以使用括號表示分組:通過使用 Mon|Tues|Wednes|Thurs|Fri|Satur|Sun)day 匹配一週中的某一天
(\w*)ility 與 \w*ility 相同。都是匹配一個由”ility”結尾的單詞。稍後我們會講解,為何第一種方法更加有用。
()表示匹配一對括號。
[()]表示匹配任意一個左括號或者一個右括號練習
在《時間機器中》找到一對括號中的內容,然後通過修改正則表示式,找到不含括號的內容。答案
(.). 然後是, ([^()]).分組可以包括空字串:
(red|blue)表示匹配red或者blue或者是一個空字串
abc()def與abcdef相同
你也可以在分組的基礎上使用重複:(red|blue)?與(red|blue|)相同
\w+(\s+\w+)表示匹配一個或多個由空格分隔的單詞
練習
簡化正則表示式 \w+\W+\w+\W+\w+ 以及 \w+\W+\w+\W+\w+\W+\w+\W+\w+\W+\w+.答案
\w+(\W+\w+){2}, \w+(\W+\w+){5}.
~~~單詞分隔符
\b
~~~
在單詞和非單詞之間有單詞分隔符。記住,一個單詞\w是[0-9A-Za-z_],而非單詞字元是\W(大寫),表示[^0-9A-Za-z_].在文字的開頭和結尾通常也有單詞分隔符。
在輸入文字it’s a cat中,實際有八個單詞分隔符。如果我們在cat之後在上一個空格,那就有九個單詞分隔符。.
\b表示匹配一個單詞分隔符
\b\w\w\w\b表示匹配一個三字母單詞
a\ba表示匹配兩個a中間有一個單詞分隔符。這個正則表示式永遠不會有匹配的字元,無論輸入怎樣的文字。
單詞分隔符本身並不是字元。它們的寬度為0。下列正則表示式的作用相同(\bcat)\b
(\bcat\b)
\b(cat)\b
\b(cat\b)
~~~換行符
^$
~~~
一篇文字中可以有一行或多行,行與行之間由換行符分隔,比如:Line一行文字
Line break換行符
Line一行文字
Line break換行符
…
Line break換行符
Line一行文字
注意,所有的文字都是以一行結束的,而不是以換行符結束。但是,任意一行都可能為空,包括最後一行。行的起始位置,是在換行符和下一行首字元之間的空間。考慮到單詞分隔符,文字的起始位置也可以當做是首行位置。
最後一行是最後一行的尾字元和換行符之間的空間。考慮到單詞分隔符,文字的結束也可以認為是行的結束。
那麼新的格式表示如下:
Start-of-line, line, end-of-line
Line break
Start-of-line, line, end-of-line
Line break
…
Line break
Start-of-line, line, end-of-line
基於上述概念:^表示匹配行的開始位置
$表示匹配行的結束位置
^&表示一個空行
^.& 表示匹配全文內容,因為行的開始符號也是一個字元,”.”會匹配這個符號。找到單獨的一行,可以使用 ^.?\^\$表示匹配字串“^”
[]表示匹配一個 。但是,[^]不是合法的正則表示式。記住在方括號中,字元有不同的特殊含義。要想在方括號內匹配^,必須用[\^]
與字元分隔符一樣,換行符也不是字元。它們寬度為0.如下所示的正則表示式作用相同:(^cat)
(cat )
^(cat)(cat )
練習
使用正則表示式在《時間機器》中找到最長的一行。答案
使用正則表示式^.{73,}$可以匹配長度為73的一行
~~~文字分界
\A\z
~~~
在很多的正則表示式實現中,將^和$作為文字的開始符號和結束符號。還有一些實現中,用\A和\z作為文字的開始和結束符號。
~~~
捕捉和替換
從這裡開始,正則表示式真正體現出了它的強大。
捕獲組
~~~
你已經知道了使用括號可以匹配一組符號。使用括號也可以捕獲子串。假設正則表示式是一個小型計算機程式,那麼捕獲子串就是它輸出的一部分。正則表示式(\w*)ility表示匹配以ility結尾的詞。第一個被捕獲的部分是由\w*控制的。比如,輸入的文字內容中有單詞accessibility,那麼首先被捕獲的部分是accessib。如果輸入的文字中有單獨的ility,則首先被捕獲的是一個空字串。
你可能會有很多的捕獲字串,它們可能靠得很近。捕獲組從左向右編號。也就是隻需要對左括號計數。
假設有這樣的正則表示式:(\w+) had a ((\w+) \w+)
輸入的內容是:I had a nice day
捕獲組1:I
捕獲組2:nice day
捕獲組3:nice
在一些正則表示式的實現中,你可以從零開始編號,編號零表示匹配整句話:I had a nice day.在其他的實現中,如果沒有制定捕獲組,那麼捕獲組1會自動地填入捕獲組0的資訊。
是的,這也意味著會有很多的括號。有一些正則表示式的實現中,提供了“非捕獲組”的語法,但是這樣的語法並不是標準語法,因此我們不會介紹。
從一個成功的匹配中返回的捕獲組個數,與使用原來的正則表示式獲得的捕獲組個數相同。記住這一點,你可以解釋一些奇怪的現象。.
正則表示式((cat)|dog)表示匹配cat或者dog。這裡有兩個捕獲組,如果輸入文字是dog,那麼捕獲組1是dog,捕獲組2為空。
正則表示式a(\w)*表示匹配一個以a開頭的單詞。這裡只有一個捕獲組
如果輸入文字為a,捕獲組1為空。
如果輸入文字為ad,捕獲組為d
如果輸入文字為avocado,捕獲組1為v。但是捕獲組0表示整個單詞avocado.
~~~替換
~~~
假如你使用了一個正則表示式去匹配字串,你可以描述另外一個字串來替換其中的匹配字元。用來替換的字串稱為替換表示式。它的功能類似於常規的Replace會話
Java中的String.replace()函式
PHP的str_replace()函式
等等
練習
將《時間機器》中所有的母音字母替換為r。答案
使用正則表示式[aeiou]以及[AEIOU],對應的替換字串分別為r,R.但是,你可以在替換表示式中引用捕獲組。這是在替換表示式中,你可以唯一操作的地方。這也是非常有效的,因為這樣你就不用重構你找到的字串。
假設你正在嘗試將美國風格的日期表示MM/DD/YY替換為ISO 8601日期表示YYYY-MM-DD
從正則表示式(\d\d)/(\d\d)/(\d\d)開始。注意,這其中有三個捕獲組:月份,日期和兩位的年份。
.捕獲組的內容和捕獲組編號之間用反斜槓分隔,因此你的替換表示式應該是20\3-\1-\2.
如果我們輸入的文字中包含03/04/05表示2005年3月4日那麼:
捕獲組1:03
捕獲組2:04
捕獲組3:05
替換字串2005-03-04.
在替換表示式中,你可以多次使用捕獲組對於雙母音,正則表示式為([aeiou]),替換表示式為\l\l
在替換表示式中不能使用反斜槓。比如,你在計算機程式中希望使用字串中使用部分文字。那麼,你必須在每個雙引號或者反斜槓之前加上反斜槓。
你的正則表示式可以是([\”])。捕獲組1是雙引號或者反斜槓
你的替換表示式應該是\\l
在某些實現中,採用美元符號$代替\練習
使用正則表示式和替換表示式,將23h59這樣的時間戳轉化為23:59.答案
正則表示式finds the timestamps, 替換表示式\1:\2
~~~反向引用
~~~
在一個正則表示式中,你也可以引用捕獲組。這稱作:反向引用比如,[abc]{2}表示匹配aa或者ab或者ac或者ba或者bb或者bc或者ca或者cb或者cc.但是{[abc]}\1表示只匹配aa或者bb或者cc.
練習
在字典中,找到包含兩次重複子串的最長單詞,比如papa, coco\b(.{6,})\1\b 匹配 chiquichiqui.
如果我們不在乎單詞的完整性,我們可以忽略單詞的分解,使用正則表示式 (.{7,})\1匹配countercountermeasure 以及 countercountermeasures.
~~~
使用正則表示式程式設計
特別提醒:
過度使用的反斜槓
~~~
在一些程式語言,比如Java中,對於包含正則表示式的字串沒有特殊標記。字串有著自己的過濾規則,這是優先於正則表示式規則的,這是頻繁使用反斜槓的原因。比如在Java中
匹配一個數字,使用的正則表示式從\d變為程式碼中的String re= “\d”
匹配雙引號字串的正則表示式從”[^”]” 變為String re = “\”[^\”]\””
匹配反斜槓或者是左邊方括號,或者右邊方括號的正則表示式從[]]變為Stringre=“[\[ ]”;
String re = “\s”; 和String re = “[ \t\r\n]”; 是等價的. 注意它們實際執行呼叫時的層次不同。
在其他的程式語言中,正則表示式是由特殊標明的,比如使用/。下面是JavaScript的例子:匹配一個數字,\d會簡單寫成 var regExp = /\d/;.
匹配一個反斜槓或者一個左邊的方括號或者一個右邊的方括號, var regExp = /[\[]]/;
var regExp = /\s/; 和 var regExp = /[ \t\r\n]/; 是等價的
當然,這意味著在使用/時必須重複兩次。比如找到URL必須使用var regExp = /https?:\/\//;.
我希望現在你能明白,我為什麼讓你特別注意反斜槓。
~~~動態正則表示式
~~~
當你動態建立一個正則表示式的時候請特別小心。如果你使用的字串不夠完善的話,可能會有意想不到的匹配結果。這可能導致語法錯誤,更糟糕的是,你的正則表示式語法正確,但是結果無法預料。錯誤的Java程式碼:
String sep = System.getProperty(“file.separator”);
String[] directories = filePath.split(sep);Bug:String.split() 認為sep是一個正則表示式。但是,在Windows中,Sep是表示匹配一個反斜槓,也就是與正則表示式”\”相同。這個正則表示式是正確的,但是會返回一個異常:PatternSyntaxException.
任何好的程式語言都會提供一種良好的機制來跳過字串中所有的元字元。在Java中,你可以這樣實現:
String sep = System.getProperty(“file.separator”);
String[] directories = filePath.split(Pattern.quote(sep));
~~~迴圈中的正則表示式
~
將正則表示式字串加入反覆執行的程式中,是一種開銷很大的操作。如果你可以在迴圈中避免使用正則表示式,你可以大大提高效率。
~
其他建議
輸入驗證
正則表示式可以用來進行輸入驗證。但是嚴格的輸入驗證會使得使用者體驗較差。比如:
- 信用卡號
~~~
在一個網站上,我輸入了我的卡號比如 1234 5678 8765 4321 網站拒絕接收。因為它使用了正則表示式\d{16}。正則表示式應該考慮到使用者輸入的空格和短橫線。
實際上,為什麼不先過濾掉所有的非數字字元,然後再進行有效性驗證呢?這樣做,可以先使用\D以及空的替換表示式。
練習
在不先過濾掉所有的非數字字元的情況下,使用正則表示式驗證卡號的正確性。答案
\D*(\d\D*){16}
~~~- 名字
~~~
不要使用正則表示式來驗證姓名。實際上,即使可以,也不要企圖驗證姓名。程式設計師對名字的錯誤看法:
名字中不含空格
名字中沒有連線符號
名字中只會使用ASCII碼字元
名字中出現的字都在特殊字符集中
名字至少要有M個字的長度
名字不會超過N個字的長度
人們只有一個名
人們只有一箇中間名
人們只有一個姓(最後三條是從英語的人名考慮)
~~~- 電子郵件地址
~~~
不要使用正則表示式驗證郵箱地址的正確性。首先,這樣的驗證很難是精確的。電子郵件地址是可以用正則表示式驗證的,但是表示式會非常的長並且複雜。
短的正則表示式會導致錯誤。(你知道嗎?電子郵箱地址中會有一些註釋)
第二,即使一個電子郵件地址可以成功匹配正則表示式,也不代表這個郵箱實際存在。郵箱的唯一驗證方法,是傳送驗證郵件。
~~~注意
~~~
在嚴格的應用場景中,不要使用正則表示式來解析HTML或者XML。解析HTML或者XML:
使用簡單的正則表示式不能完成
總體來說非常困難
已經有其他的方法解決
找到一個已經有的解析庫來完成這個工作
~~~
總結
- 字元:
a
b
c
d
1
2
3
4
etc. - 字元類:
.
[abc]
[a-z]
\d
\w
\s
- .代表任何字元
- \d 表示“數字”
- \w 表示”字母”, [0-9A-Za-z_]
- \s 表示 “空格, 製表符,回車或換行符”
- 否定字元類:
[^abc]
\D
\W
\S
- 重複:
{4}
{3,16}
{1,}
?
*
+
?
表示 “零次或一次”*
表示 “大於零次”+
表示 “一次或一次以上”- 如果不加上?,所有的重複都是最長匹配的(貪婪)
- 分組:
(Septem|Octo|Novem|Decem)ber
- 詞,行以及文字的分隔:
\b
^
$
\A
\z
轉義字元:
\1
\2
\3
etc. (在匹配表示式和替換表示式中都可用)元字元:
.
\
[
]
{
}
?
*
+
|
(
)
^
$
- 在字元類中使用元字元:
[
]
\
-
^
- 使用反斜槓可以忽略元字元:
\