正則表達(dá)式可以說是軟件開發(fā)中最常用的功能之一。本文將以C 語言為例,介紹其中的正則表達(dá)式相關(guān)知識。
前言 當(dāng)你想要判斷許多字符串是否符合某個特定格式;當(dāng)你想在一大段文本中查找出所有的日期和時間;當(dāng)你想要修改大量日志中所有的時間格式,在這些情況下,正則表達(dá)式都能幫上忙。簡單來說,正則表達(dá)式描述了一系列規(guī)則,通過這些規(guī)則,可以在字符串中找到相關(guān)的內(nèi)容,規(guī)則使得搜索的能力更加強(qiáng)大。匹配的過程由正則表達(dá)式引擎完成。開發(fā)者通常不需要關(guān)心正則表達(dá)式引擎的實(shí)現(xiàn)細(xì)節(jié),直接使用其提供的能力即可。正則表達(dá)式非常的常用,但真正精通它的人卻不多。本文試圖給大家講解一些對于C 語言使用正則表達(dá)式的基礎(chǔ)知識。
代碼示例 本文中所貼出的代碼示例可以到我的Github上獲?。?span>paulQuei/cpp-regex
[1] 。或者,你也可以直接通過下面這條命令獲取所有源碼:
git clone https://github.com/paulQuei/cpp-regex.git
C 中正則表達(dá)式的API基本上都位于頭文件中。為了簡化書寫,本文中給出的代碼都已經(jīng)默認(rèn)做了以下操作:
#include? #include? using?namespace?std;
入門示例 為了使大家有一個直觀的感受,文章的開頭先通過一些入門示例給大家一個直觀的感受。在這個基礎(chǔ)之上,再詳細(xì)講解其中的細(xì)節(jié)。使用正則表達(dá)式的大致流程如下:首先你有一段需要處理的文本。這可能是一個字符串對象,也可能是一個文本文件,或者是一大堆日志。接下來你會有特定的目標(biāo),例如:找出文本中所有的時間和日期。這個時候你就需要根據(jù)可能的格式寫出具體的正則表達(dá)式,例如,日期的格式是:2020-01-01,那么你的正則表達(dá)式可能是這樣:
\d{4}-\d{2}-\d{2}
。(你現(xiàn)在不必糾結(jié)與這個正則表達(dá)式是什么意思,因?yàn)檫@是本文接下來要講解的內(nèi)容。)有了正則表達(dá)式之后,你需要將你的文本和正則表達(dá)式交給正則表達(dá)式引擎 – 由C 語言(或者其他語言)提供。引擎會在文本中搜索到匹配的結(jié)果。這個結(jié)果的格式可能是包含了多個組,例如:你可能需要分離出年份和月份。有了引擎返回的結(jié)果之后,你就可以進(jìn)一步處理了。
img 使用正則表達(dá)式的流程大體都是一致的,下面是最常見的三種使用方式。
匹配 匹配是判斷給定的字符串是否符合某個正則表達(dá)式。例如:你想判斷當(dāng)前文本是否全部由數(shù)字構(gòu)成。下面是一段代碼示例:
string?s1?=?"ab123cdef" ;?//?① string?s2?=?"123456789" ;?//?② regex?ex("\\d " );?//?③ cout?<"?is?all?digit:?"?<cout?<"?is?all?digit:?"?<
在這段代碼中:
這是一個包含了數(shù)字和字母的字符串 這是一個只包含了數(shù)字的字符串 這是我們的正則表達(dá)式,它表示:有多個數(shù)字 通過regex_match
判斷第一個字符串是否匹配,這里將返回false 通過regex_match
判斷第二個字符串是否匹配,這里將返回true 這段代碼輸出如下:
ab123cdef?is?all?digit:?0 123456789?is?all?digit:?1
請注意,正則表達(dá)式有它自身的語法。這與C 的語法是兩回事。C 編譯器只會檢查C 代碼的語法。因此,即便你的代碼通過了C 編譯器的語法檢查,但在運(yùn)行的時候,由于正則表達(dá)式的語義,還可能出現(xiàn)正則表達(dá)式的錯誤。正則表達(dá)式的錯看起來類似這樣:terminating with uncaught exception of type std::__1::regex_error: The expression contained an invalid escaped character, or a trailing escape.
。 搜索 還有一些時候,我們要判斷的并非是文本的全體是否匹配。而是在一大段文本中搜索匹配的目標(biāo)。下面是一段代碼示例,這段示例演示了在一個字符串中查找數(shù)字:
string?s?=?"ab123cdef" ;?//?① regex?ex("\\d " );????//?② smatch?match;?//?③ regex_search(s,?match,?ex);?//?④ cout?<"?contains?digit:?"?<
這是一個包含了數(shù)字和字母的字符串 和前面一樣的正則表達(dá)式 通過std::smatch
來保存匹配的結(jié)果。除了std::smatch
,還有std::cmatch
也很常用。前者是以std::string
的形式返回結(jié)果,后者是以const char*
的形式返回結(jié)果。 通過regex_search
函數(shù)搜索結(jié)果 打印出匹配的結(jié)果 這段代碼輸出如下:
ab123cdef?contains?digit:?123
替換 最后,使用正則表達(dá)式的還有一個常見功能是文本替換。很多的編輯器都有這樣的功能。例如,下圖是我的Sublime編譯器,在搜索替換文本的時候,可以使用正則表達(dá)式,這時搜索的能力就更加強(qiáng)大了?!癋ind:”部分可以通過正則表達(dá)式來描述待替換的字符串,“Replace:”部分填寫替換的字符串。
img 下面是在C 中使用正則表達(dá)式完成字符串替換的代碼示例:
string?s?=?"ab123cdef" ;?//?① regex?ex("\\d " );????//?② string?r?=?regex_replace(s,?ex,?"xxx" );?//?③ cout?<
仍然是前面這個字符串 仍然是同樣的正則表達(dá)式 通過regex_replace
完成替換 通過cout
輸出結(jié)果 最終輸出的字符串如下:
abxxxcdef
通過上面的三個示例我們看到,
regex_match
,
regex_search
和
regex_replace
三個函數(shù)是正則表達(dá)式的核心,它們會運(yùn)行正則表達(dá)式引擎完成匹配,查找和替換任務(wù)。
正則表達(dá)式文法 文法 C 中內(nèi)置了多種正則表達(dá)式文法,在創(chuàng)建正則表達(dá)式的時候可以通過參數(shù)來選擇。它們?nèi)缦卤硭荆?/p>
文法 說明 ECMAScript ECMAScript正則表達(dá)式語法 [2] ,默認(rèn)選項(xiàng)basic 基礎(chǔ)POSIX正則表達(dá)式語法 [3] extended 擴(kuò)展POSIX正則表達(dá)式語法 [4] awk awk工具的正則表達(dá)式語法 [5] grep grep工具的正則表達(dá)式語法 [6] egrep grep工具的正則表達(dá)式語法 [7]
不同的文法在表達(dá)上有一些不同,如果你原先已經(jīng)很熟悉awk或者egrep文法的正則表達(dá)式,你可以直接使用它們。對于其他人來說,我們直接使用默認(rèn)的ECMAScript文法即可(Javascript的正則表達(dá)式也是使用ECMAScript文法)。
grep的全稱是Global Regular Expression Print。這個名字是在提示我們,它本身與正則表達(dá)式的歷史有著特定的聯(lián)系。 C 中的 ECMAScript 正則表達(dá)式文法是?
ECMA-262 文法 [8] ,你可以點(diǎn)擊鏈接查看詳細(xì)內(nèi)容。下文中,我們僅會講解該標(biāo)準(zhǔn)下的正則表達(dá)式。
Raw string literal 在代碼中寫字符串有時候是比較麻煩的,因?yàn)楹芏嘧址枰ㄟ^反斜杠轉(zhuǎn)義。當(dāng)有多個反斜杠連在一起時,就很容易寫錯或者理解錯了。當(dāng)通過字符串來寫正則表達(dá)式時,這個問題就更嚴(yán)重了。因?yàn)檎齽t表達(dá)式本身也有一些字符需要轉(zhuǎn)義。例如,對于這樣一個字符串?
"('(?:[^\\\\']|\\\\.)*'|\"(?:[^\\\\\"]|\\\\.)*\")|"
?大部分人恐怕很難一眼看出其含義了。在正則表達(dá)式很復(fù)雜的時候,推薦大家使用Raw string literal來表達(dá)。這種表達(dá)式是告訴編譯器:這里的內(nèi)容是純字符串,因此不再需要增加反斜杠來轉(zhuǎn)義特殊字符。Raw string literal的格式如下:
R"delimiter(raw_characters)delimiter"
這其中:
delimiter是可選的分隔符,通常不用寫 raw_characters是具體的字符串 也就是說,
R"(content)"
中的
content
是你需要的字符串本身。下面是一個代碼示例:
string?s?=?R"(" \w\\w\\\w)";cout?<
它將輸出:
"\w\\w\\\w
可以看到,這里的雙引號和反斜杠不會被解釋成轉(zhuǎn)義字符,而是當(dāng)成字符串內(nèi)容本身,因此會原樣輸出。這樣就減少了轉(zhuǎn)義字符的復(fù)雜度,于是更容易理解了。
特殊字符 正則表達(dá)式本身定義了一些特殊的字符,這些字符有著特殊的含義。它們?nèi)缦卤硭尽?/p>
字符 說明 . 匹配任意字符 [ 字符類的開始 ] 字符類的結(jié)束 { 量詞重復(fù)數(shù)開始 } 量詞重復(fù)數(shù)結(jié)束 ( 分組開始 ) 分組結(jié)束 \ 轉(zhuǎn)義字符 \ 轉(zhuǎn)義字符自身 * 量詞,0個或者多個 量詞,1個或者多個 ? 量詞,0個或者1個 | 或 ^ 行開始;否定 $ 行結(jié)束 \n 換行 \t Tab符 \xhh hh表示兩位十六進(jìn)展表示的Unicode字符 \xhhhh hhhh表示四位十六進(jìn)制表示的Unicode字符串
這些字符并不少,剛開始接觸可能記不住,但隨著下文的講解,相信你會逐漸熟悉它們。
字符類 字符類,顧名思義:是對字符的分類。例如:1234567890這些都屬于數(shù)字字符類。除此之外,還有其他的分類,它們?nèi)缦卤硭荆?/p>
字符類 簡寫 說明 [[:alnum:]] 字母和數(shù)字 [_[:alnum:]] \w 字母,數(shù)字以及下劃線 [^_[:alnum:]] \W 非字母,數(shù)字以及下劃線 [[:digit:]] \d 數(shù)字 [^[:digit:]] \D 非數(shù)字 [[:space:]] \s 空白字符 [^[:space:]] \S 非空白字符 [[:lower:]] 小寫字母 [[:upper:]] 大寫字母 [[:alpha:]] 任意字母 [[:blank:]] 非換行符的空白字符 [[:cntrl:]] 控制字符 [[:graph:]] 圖形字符 [[:print:]] 可打印字符 [[:punct:]] 標(biāo)點(diǎn)字符 [[:xdigit:]] 十六進(jìn)制的數(shù)字字符
這里我們可以看到:
字符類通過[]
作為標(biāo)識,因此這兩個字符是正則表達(dá)式的中的特殊字符。如果是想使用這兩個字符本身,需要對它們進(jìn)行轉(zhuǎn)義。 在[]
內(nèi)部,通過[:xxx:]
來描述字符類的名稱。 []
中可以通過^
表示否定,即:字符類的反面。字母,數(shù)字和空白字符由于這些字符類非常常用,因此它們有簡寫的方法。簡寫使得正則表達(dá)式更加簡潔,但表達(dá)的含義是一樣的。 接下來我們看一個代碼示例:
#include? #include? using?namespace?std; static?void?search_string(const?string