通過網站 https://regex101.com/ 可以測試正則表達式的匹配結果及匹配過程.
本文章拋開各個編程語言實現差異, 僅做正則本身的介紹, 會盡量將正則這玩意說明白, 使得你看完這邊文章後對正則基本可以運用自如.
溫馨提示, 這篇文章會比較長, 大致瀏覽即可. 正確的方式是收藏起來, 等到使用正則的時候翻看
語法介紹
在平常進行字符串匹配的時候如何做呢? 比如希望從字符串Hello Word
中匹配到第一個單詞, 我們就會拿着Hello
子串進行匹配.
正則表達式的表示與其相同, 區別是在子串匹配時每個字符都會進行原樣匹配, 而正則表達式中會存在一些特殊符號, 這些符號會代表一些特殊的含義, 如a+
的意思是匹配任意多個連續的a
字符, 是不是十分簡單?
如此說來, 要使用正則表達式, 關鍵點就在於瞭解其中的特殊字符上.
分類 | 字符 | 含義 |
---|---|---|
單字符 | . | 任意字符(換行符除外) |
\d | 任意數字 | |
\D | 非數字 | |
\w | 字母數字下劃線 | |
\W | 非字母數字下劃線 | |
$ | 字符串結束位置 | |
^ | 字符串開始位置 | |
空白符 | \s | 任意空白字符 |
\S | 非空白字符(包括空格) | |
\r | 回車 | |
\n | 換行 | |
\f | 換頁 | |
\t | 製表符 | |
\v | 垂直製表符 | |
量詞 | 前面內容出現 m 次 | |
>=m次 | ||
m-n 次 | ||
* | 同 | |
+ | 同 | |
? | 同 | |
範圍選擇 | | | 或. eg: `ab |
[...] | 單字符多選一. eg: [abcd] msg: a或b或c或d |
|
[a-c] | ASCII 表範圍. eg: [a-z] msg: 所有小寫字母 若其中的 - 是需要匹配的單字符, 需使用\- 進行轉義 |
|
[^...] | 取反. [] 中標識的字符外的任意字符 |
|
以上所有均可以任意嵌套使用, 如:
https?|ftp
: 可匹配http https ftp
高級用法
分組
有這樣一個正則表達式 ab{3}
, 它會匹配字符串 abbb
. 如果我們想要匹配字符串 ababab
, 如何在正則表達式中指定令ab
重複3次呢?
用程序員的通俗思維想一下, 沒錯, 加括號. (ab){3}
的意思就是匹配字符串 ababab
. 而這, 就是分組.
有的小朋友會問題了, 這不就是指定下優先級嘛, 和分組有什麼關係? 爲什麼叫分組呢? 別急, 往下看
在正則表達式中, 分組有如下作用:
- 在表達式後面可以進行引用
- 在匹配結果中, 會將匹配的分組同時提取出來
正則引用分組
在正則表達式中, 可以通過 \1
來引用分組. 其中的數字是分組的編號, 從1開始. 從左往右依次遞增.
比如正則表達式 (ab)(cd)\2\1
會匹配字符串 abcdcdab
同時會在結果中將2個分組提取出來.
這裏注意, 在有些編程語言的實現上, 通過$
符號引用分組(比如 js), 用的時候再搜就行.
不引用分組
有的時候我們只是希望將多個字符合並, 並不需要引用分組. 這時可以通過 (?:...)
來指定不需要引用的分組.
嵌套分組
對於比較複雜的場景, 會存在括號嵌套括號的情況, 此時分組編號是全局的. 簡單說, 左括號是第幾個, 分組編號就是幾.
比如正則表達式 (a(bc)d)\2\1
會匹配字符串 abcdbcabcd
分組應用場景
簡單介紹幾種可能預見的應用場景:
匹配重複單詞
比如正則表達式: (\w+) \1
文本替換
比如這段python
代碼:
import re
test_str = 'hello, hujingnb is good, haha'
pattern = r'(\w+) is good'
repl = r'\1 is bad'
# hello, hujingnb is bad, haha
print(re.sub(pattern, repl, test_str))
比如常用的sublime
工具中, 也可通過類似操作進行文本替換.
匹配模式
在匹配的時候, 可以通過設置(?i)
來修改匹配模式爲忽略大小寫, 使用方式如下:
(?i)hello
: 放在正則表達式最前面, 整個正則表達式均爲忽略大小寫模式h(?i)hello
: 放在正則表達式中間, 即從某處開始, 改爲忽略大小寫模式. 注意, 不是所有語言均可用((?i)hello) \1
: 放在分組開頭, 標識分某個分組改爲忽略大小寫模式. 注意, 不是所有語言均可用
可以調整的匹配模式如下, 匹配模式格式均爲(?<model>)
, 使用方法相同, 多個模式可以放在一起使用, 如 (?is)
:
a
: 測試 僅匹配 ASCII 字符, unicode 編碼字符不進行匹配i
: 測試 忽略大小寫m
: 測試 多行模式. 修改^$
的行爲, 改爲匹配每一行的開頭結尾n
: 測試 開啓後,(...)
這種普通分組不會做爲分組存在, 僅(?<name>...)
這種命名分組會進行捕獲s
: 測試.
可以匹配任意符號, 包括換行符u
: 測試 匹配完整的 unicode 編碼, 默認行爲, 基本不需要設置U
: 測試 懶惰模式. 開啓懶惰模式, 在此模式下, 量詞後面加?
爲恢復貪婪模式x
: 測試 詳細模式. 將正則表達式中的所有空格及換行均忽略, 且每行#
後爲註釋內容. 匹配規則中的空格可使用\
轉義 (或者放到分組中使用, 也可以通過[ ]
使用)
注意: 大部分語言都可以直接在正則表達式中修改匹配模式, 但部分語言不行, 比如:
js
:/<正則>/<model>
邊界匹配
在正則使用中, 可能會想要進行位置匹配, 但並不希望匹配內容出現在結果中. 於是就出現了這樣一組符號, 僅用於匹配位置, 比如前面出現過的 ^
和 $
用於匹配邊界的符號有如下幾種:
符號 | demo | 含義 |
---|---|---|
^ |
匹配 不匹配 | 匹配字符串的開始位置 |
` | 符號 | demo |
---- | ------------------------------------------------------------ | -------------------------------- |
^ |
匹配 不匹配 | 匹配字符串的開始位置 |
匹配 不匹配 | 匹配字符串的結束位置 | |
\b |
匹配 | 匹配單詞邊界, 邊界包括 空格.- 等等符號, 注意_ 不是單詞邊界 |
\B |
匹配 不匹配 | 匹配非單詞邊界, 與\b 相反 |
\A |
匹配 不匹配 ^ 差異 |
匹配字符串的開始位置, 與^ 相似. 但多行模式下, ^ 的行爲會改爲匹配行開始位置, \A 行爲不會改變 |
\Z |
匹配 $ 差異 |
匹配字符串的結束位置, 與$ 相似. 同樣, 在多行模式下, 行爲不會改變 |
(?=...) |
匹配 不匹配 | 前向肯定斷言. 簡單說, 右邊匹配 ... |
(?!...) |
匹配 不匹配 | 前向否定斷言. 簡單說, 右邊不匹配 ... |
(?<=...) |
匹配 不匹配 | 後向肯定斷言. 簡單說, 左邊匹配 ... |
(?<!...) |
匹配 不匹配 | 後向否定斷言. 簡單說, 左邊不匹配 ... |
擴展語法
所有擴展語法格式均爲爲(?...)
. (反過來不成立)
注意, 擴展語法並不是所有編程語言都支持的, 在使用前可前往網站測試是否支持.
命名分組
使用編號引用分組的方式並不友好, 甚至有時候改了下正則表達式, 後面編號都要改一遍. 因此, 我們可以給分組起個名.
(?P<xxx>...)
: 給分組起名爲xxx
(?P=xxx)
: 引用xxx
分組
比如正則表達式 (?P<reg_name>ab)(?P=reg_name)
會匹配字符串 abab
注意 命名分組也會佔用分組的編號哦, 也就是說 (?P<reg_name>ab)(?P=reg_name)
和 (?P<reg_name>ab)\1
效果是一樣的.
分支判斷
根據前面是否匹配到分組信息, 來使得後面能夠有不同的匹配行爲.
語法爲: (?(<group_id>/<group_name>)<yes-pattern>|<no-pattern>)
, 前面指定分組編號或者名字, 如果分組存在, 則使用 <yes-patterm>
進行匹配, 否則使用 <no-pattern>
進行匹配.
比如這個例子, ^(<)?(\w+)(?(1)>|$)$
, 可以匹配到字符串 <aaa>
和 aaa
, 也就是<
開頭的必須由 >
結尾.
註釋
語法 (?#這部分是註釋)
匹配規則
下面介紹下幾種匹配規則:
- 貪婪模式: 儘可能多的匹配. 是正則匹配時的默認模式
- 懶惰模式: 儘可能少的匹配. 通過在量詞後添加
?
指定. 如a*?
就是a*
的懶惰版本 - 獨佔模式: 在匹配過程中不進行回溯. 通過在量詞後添加
+
指定. 如a++
就是a+
的獨佔版本
簡單對着幾種規則進行說明
貪婪模式
貪婪模式其實就是我們平常最進場使用的模式. 其原則是向後查找儘量長的字符串進行匹配.
比如使用正則b*
來匹配字符串abbbc
, 能夠匹配到如下內容:
位置(前閉後開) | 匹配內容 |
---|---|
0-0 | 空 |
1-4 | bbb |
4-4 | 空 |
5-5 | 空 |
匹配過程大致如下:
0-0
: 匹配第一個字符, 發現不是 a, 輸出空1-4
: 一直匹配到字母c
發現匹配不上了, 輸出bbb
- 剩下的同理
懶惰模式
匹配過程與貪婪模式
相似, 區別是隻要有能夠匹配到的, 就輸出, 會輸出符合規則的最短子串.
還用上面相同的例子舉例, 使用正則b*?
匹配字符串abbbc
, 能夠匹配到如下內容:
位置(前閉後開) | 匹配內容 |
---|---|
0-0 | 空 |
1-1 | 空 |
1-2 | b |
2-2 | 空 |
2-3 | b |
3-3 | 空 |
3-4 | b |
4-4 | 空 |
5-5 | 空 |
與上面的一對比, 區別是不是就很明顯了? 這次匹配到的是沒單個b
字符, 就連每個b
字符左面的空字符都匹配上了.
匹配過程就不再贅述了. 這裏用一個更加明顯且常用的場景來說明貪婪模式
與懶惰模式
的區別, 當我們匹配字符串"hi mom" and "hi son"
時:
獨佔模式
要說明獨佔模式, 得先簡單說一下正則匹配的回溯現象.
比如, 使用正則 ab+bc
匹配字符串 abbbc
的時候, 在貪婪模式下(下方的括號爲了標明當前匹配的位置):
正則匹配位置 | 字符串匹配位置 | 說明 |
---|---|---|
(a)b+bc | (a)bbbc | 字符 a 完成匹配, 繼續下一個匹配 |
a(b+)bc | a(b)bbc | 此時, 要匹配字符 b 的數量爲 >= 1, 因此會繼續向後匹配 正則位置不變, 字符串匹配後移(後續相同操作忽略) |
a(b+)bc | abbb(c) | 當匹配到這個位置的時候, 發現已經匹配不上了. 則字符串會向前移動, 以繼續完成匹配. |
ab+(b)c | abb(b)c | 此時, 字符串將已經匹配過的字符又吐出來了. 這個過程就被稱爲回溯 |
... | ... | 後續完成匹配, 不再贅述 |
如果正則是ab+bbc
的話, 匹配相同的字符串甚至會發生多次回溯. (懶惰模式也存在回溯現象, 不再贅述)
而回溯現象是及其影響性能的. 而獨佔模式則是將回溯直接關掉, 匹配性能更好, 但是對正則書寫的要求也更高些.
比如匹配abbbc
字符串時, 開啓獨佔模式的匹配規則ab++bc
會匹配不到任何數據.
而ab++c
則是能夠匹配到字符串abbbc
的, 因爲匹配過程無需回溯,
這裏說句題外話, 在有些編程語言中不支持回溯模式, 比如Go
, Python
中使用也需要通過regex
庫.
回溯現象真實場景
你以爲回溯現象造成的性能微乎其微麼? 不, 有這樣一篇文章一個由正則表達式引發的血案, 講述了因爲正則回溯而導致的 CPU 爆滿, 感興趣的去原文看一下.
簡單來說, 就是正則表達式 ^([a-z]|[a-z])+$
在進行匹配的時候,因爲a-z
在前後組合中重複出現了, 導致大量回溯後的重複判斷 . 如果所有的字符都能夠成功匹配到前一個組合, 問題還不大, 但一旦有一個字符不匹配就會發生回溯, 且不匹配的字符越靠後, 回溯的層級越深. 查看回溯匹配
對於這種情況如何修改呢?
- 將重複匹配去掉, 改爲
^[a-z]+$
- 使用獨佔模式防止回溯.
^([a-z]|[a-z])++$
回溯的介紹, 也可參考此文章