正則表達式使用文檔

通過網站 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. 在表達式後面可以進行引用
  2. 在匹配結果中, 會將匹配的分組同時提取出來

正則引用分組

在正則表達式中, 可以通過 \1 來引用分組. 其中的數字是分組的編號, 從1開始. 從左往右依次遞增.

比如正則表達式 (ab)(cd)\2\1 會匹配字符串 abcdcdab 同時會在結果中將2個分組提取出來.

image-20230910204040697

這裏注意, 在有些編程語言的實現上, 通過$符號引用分組(比如 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

匹配過程大致如下:

  1. 0-0: 匹配第一個字符, 發現不是 a, 輸出空
  2. 1-4: 一直匹配到字母c發現匹配不上了, 輸出bbb
  3. 剩下的同理

懶惰模式

匹配過程與貪婪模式相似, 區別是隻要有能夠匹配到的, 就輸出, 會輸出符合規則的最短子串.

還用上面相同的例子舉例, 使用正則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"時:

  • ".*" : 貪婪模式下, 匹配到的是字符串"hi mom" and "hi son"
  • ".*?": 懶惰模式下, 則會分別匹配到字符串"hi mom""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在前後組合中重複出現了, 導致大量回溯後的重複判斷 . 如果所有的字符都能夠成功匹配到前一個組合, 問題還不大, 但一旦有一個字符不匹配就會發生回溯, 且不匹配的字符越靠後, 回溯的層級越深. 查看回溯匹配

對於這種情況如何修改呢?

  1. 將重複匹配去掉, 改爲 ^[a-z]+$
  2. 使用獨佔模式防止回溯. ^([a-z]|[a-z])++$

回溯的介紹, 也可參考此文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章