C++正則表達式


若要判斷一個輸入的QQ號是否有效,你會如何處呢?

首先你得分析一下其對應規則,依次列出:

  1. 長度大於5,小於等於11;
  2. 首位不能為0;
  3. 是否為純數字?

規則既列,接着就該嘗試實現了,那么用什么來表示字符串呢?在C++中,最容易想到的就是string了,其中提供了許多成員函數可以處理字符串,所以有了如下實現:

std::string qq;
std::cin >> qq;

// 1. 判斷位數是否合法
if (qq.length() >= 5 && qq.length() <= 11)
{
	// 2. 判斷是否非'0'開頭
	if (qq[0] != '0')
	{
		// 3. 判斷是否為純數字
		auto pos = std::find_if(qq.begin(), qq.end(), [](const char& ch) {
			return ch < '0' || ch > '9';
		});
		if (pos == qq.end())
			std::cout << "valid.\n";
	}
}

雖然寫出來了,但是有沒有感到異常繁瑣?這還僅僅是一個對應規則較少的處理,便如此麻煩,若是要檢測IP地址、身份證號,或是解析一段HTML數據,或是其它更復雜的字串,那豈非更令人叫苦不迭?

當然,也有許多擴展庫對字符串處理提供了方便,其中比較好用的是boost中的string_algo庫(已於C17納入了標准庫,並改名為string_view),但本篇主要說C11的regex庫,其對復雜數據的處理能力非常強,比如可以用它來檢測QQ號:

std::regex qq_reg("[1-9]\\d{4,11}");
bool ret = std::regex_match(qq, qq_reg);
std::cout << (ret ? "valid" : "invalid") << std::endl;

是不是超級方便呢?那么接下來便來看看如何使用「正則表達式」。

正則程序庫(regex)

「正則表達式」就是一套表示規則的式子,專門用來處理各種復雜的操作。

stdregex是C用來表示「正則表達式」(regular expression)的庫,於C11加入,它是class stdbasic_regex<>針對char類型的一個特化,還有一個針對wchar_t類型的特化為std::wregex。

正則文法(regex syntaxes)

std::regex默認使用是ECMAScript文法,這種文法比較好用,且威力強大,常用符號的意義如下:

符號 意義
^ 匹配行的開頭
$ 匹配行的結尾
. 匹配任意單個字符
[…] 匹配[]中的任意一個字符
(…) 設定分組
\ 轉義字符
\d 匹配數字[0-9]
\D \d 取反
\w 匹配字母[a-z],數字,下划線
\W \w 取反
\s 匹配空格
\S \s 取反
+ 前面的元素重復1次或多次
* 前面的元素重復任意次
? 前面的元素重復0次或1次
前面的元素重復n次
{n,} 前面的元素重復至少n次
{n,m} 前面的元素重復至少n次,至多m次
| 邏輯或

上面列出的這些都是非常常用的符號,靠這些便足以解決絕大多數問題了。

匹配(Match)

字符串處理常用的一個操作是「匹配」,即字符串和規則恰好對應,而用於匹配的函數為std::regex_match(),它是個函數模板,我們直接來看例子:

std::regex reg("<.*>.*</.*>");
bool ret = std::regex_match("<html>value</html>", reg);
assert(ret);

ret = std::regex_match("<xml>value<xml>", reg);
assert(!ret);

std::regex reg1("<(.*)>.*</\\1>");
ret = std::regex_match("<xml>value</xml>", reg1);
assert(ret);

ret = std::regex_match("<header>value</header>", std::regex("<(.*)>value</\\1>"));
assert(ret);

// 使用basic文法
std::regex reg2("<\\(.*\\)>.*</\\1>", std::regex_constants::basic);
ret = std::regex_match("<title>value</title>", reg2);
assert(ret);

這個小例子使用regex_match()來匹配xml格式(或是html格式)的字符串,匹配成功則會返回true,意思非常簡單,若是不懂其中意思,可參照前面的文法部分。

對於語句中出現\\,是因為\需要轉義,C++11以后支持原生字符,所以也可以這樣使用:

std::regex reg1(R"(<(.*)>.*</\1>)");
auto ret = std::regex_match("<xml>value</xml>", reg1);
assert(ret);

但C++03之前並不支持,所以使用時要需要留意。

若是想得到匹配的結果,可以使用regex_match()的另一個重載形式:

std::cmatch m;
auto ret = std::regex_match("<xml>value</xml>", m, std::regex("<(.*)>(.*)</(\\1)>"));
if (ret)
{
	std::cout << m.str() << std::endl;
	std::cout << m.length() << std::endl;
	std::cout << m.position() << std::endl;
}

std::cout << "----------------" << std::endl;

// 遍歷匹配內容
for (auto i = 0; i < m.size(); ++i)
{
	// 兩種方式都可以
	std::cout << m[i].str() << " " << m.str(i) << std::endl;
}

std::cout << "----------------" << std::endl;

// 使用迭代器遍歷
for (auto pos = m.begin(); pos != m.end(); ++pos)
{
	std::cout << *pos << std::endl;
}

輸出結果為:

<xml>value</xml>
16
0
----------------
<xml>value</xml> <xml>value</xml>
xml xml
value value
xml xml
----------------
<xml>value</xml>
xml
value
xml

cmatch是class template std::match_result<>針對C字符的一個特化版本,若是string,便得用針對string的特化版本smatch。同時還支持其相應的寬字符版本wcmatch和wsmatch。

在regex_match()的第二個參數傳入match_result便可獲取匹配的結果,在例子中便將結果儲存到了cmatch中,而cmatch又提供了許多函數可以對這些結果進行操作,大多方法都和string的方法類似,所以使用起來比較容易。

m[0]保存着匹配結果的所有字符,若想在匹配結果中保存有子串,則得在「正則表達式」中用()標出子串,所以這里多加了幾個括號:

std::regex("<(.*)>(.*)</(\\1)>")

這樣這些子串就會依次保存在m[0]的后面,即可通過m[1],m[2],...依次訪問到各個子串。

「搜索」與「匹配」非常相像,其對應的函數為std::regex_search,也是個函數模板,用法和regex_match一樣,不同之處在於「搜索」只要字符串中有目標出現就會返回,而非完全「匹配」。

還是以例子來看:

std::regex reg("<(.*)>(.*)</(\\1)>");
std::cmatch m;
auto ret = std::regex_search("123<xml>value</xml>456", m, reg);
if (ret)
{
	for (auto& elem : m)
		std::cout << elem << std::endl;
}

std::cout << "prefix:" << m.prefix() << std::endl;
std::cout << "suffix:" << m.suffix() << std::endl;

輸出為:

<xml>value</xml>
xml
value
xml
prefix:123
suffix:456

這兒若換成regex_match匹配就會失敗,因為regex_match是完全匹配的,而此處字符串前后卻多加了幾個字符。

對於「搜索」,在匹配結果中可以分別通過prefix和suffix來獲取前綴和后綴,前綴即是匹配內容前面的內容,后綴則是匹配內容后面的內容。

那么若有多組符合條件的內容又如何得到其全部信息呢?這里依舊通過一個小例子來看:

std::regex reg("<(.*)>(.*)</(\\1)>");
std::string content("123<xml>value</xml>456<widget>center</widget>hahaha<vertical>window</vertical>the end");
std::smatch m;
auto pos = content.cbegin();
auto end = content.cend();
for (; std::regex_search(pos, end, m, reg); pos = m.suffix().first)
{
	std::cout << "----------------" << std::endl;
	std::cout << m.str() << std::endl;
	std::cout << m.str(1) << std::endl;
	std::cout << m.str(2) << std::endl;
	std::cout << m.str(3) << std::endl;
}

輸出結果為:

----------------
<xml>value</xml>
xml
value
xml
----------------
<widget>center</widget>
widget
center
widget
----------------
<vertical>window</vertical>
vertical
window
vertical

此處使用了regex_search函數的另一個重載形式(regex_match函數亦有同樣的重載形式),實際上所有的子串對象都是從std::pair<>派生的,其first(即此處的prefix)即為第一個字符的位置,second(即此處的suffix)則為最末字符的下一個位置。

一組查找完成后,便可從suffix處接着查找,這樣就能獲取到所有符合內容的信息了。

分詞(Tokenize)

還有一種操作叫做「切割」,例如有一組數據保存着許多郵箱賬號,並以逗號分隔,那就可以指定以逗號為分割符來切割這些內容,從而得到每個賬號。

而在C++的正則中,把這種操作稱為Tokenize,用模板類regex_token_iterator<>提供分詞迭代器,依舊通過例子來看:

std::string mail("123@qq.vip.com,456@gmail.com,789@163.com,abcd@my.com");
std::regex reg(",");
std::sregex_token_iterator pos(mail.begin(), mail.end(), reg, -1);
decltype(pos) end;
for (; pos != end; ++pos)
{
	std::cout << pos->str() << std::endl;
}

這樣,就能通過逗號分割得到所有的郵箱:

123@qq.vip.com
456@gmail.com
789@163.com
abcd@my.com

sregex_token_iterator是針對string類型的特化,需要注意的是最后一個參數,這個參數可以指定一系列整數值,用來表示你感興趣的內容,此處的-1表示對於匹配的正則表達式之前的子序列感興趣;而若指定0,則表示對於匹配的正則表達式感興趣,這里就會得到“,";還可對正則表達式進行分組,之后便能輸入任意數字對應指定的分組,大家可以動手試試。

替換(Replace)

最后一種操作稱為「替換」,即將正則表達式內容替換為指定內容,regex庫用模板函數std::regex_replace提供「替換」操作。

現在,給定一個數據為"he...ll..o, worl..d!", 思考一下,如何去掉其中誤敲的“.”?

有思路了嗎?來看看正則的解法:

char data[] = "he...ll..o, worl..d!";
std::regex reg("\\.");
// output: hello, world!
std::cout << std::regex_replace(data, reg, "");

我們還可以使用分組功能:

char data[] = "001-Neo,002-Lucia";
std::regex reg("(\\d+)-(\\w+)");
// output: 001 name=Neo,002 name=Lucia
std::cout << std::regex_replace(data, reg, "$1 name=$2");

當使用分組功能后,可以通過$N來得到分組內容,這個功能挺有用的。

實例(Examples)

1. 驗證郵箱

這個需求在注冊登錄時常有用到,用於檢測用戶輸入的合法性。

若是對匹配精確度要求不高,那么可以這么寫:

std::string data = "123@qq.vip.com,456@gmail.com,789@163.com,abcd@my.com";
std::regex reg("\\w+@\\w+(\\.\\w+)+");

std::sregex_iterator pos(data.cbegin(), data.cend(), reg);
decltype(pos) end;
for (; pos != end; ++pos)
{
	std::cout << pos->str() << std::endl;
}

這里使用了另外一種遍歷正則查找的方法,這種方法使用regex iterator來迭代,效率要比使用match高。這里的正則是一個弱匹配,但對於一般用戶的輸入來說沒有什么問題,關鍵是簡單,輸出為:

123@qq.vip.com
456@gmail.com
789@163.com
abcd@my.com

但若我輸入一個“Abc0_@aAa1.123.456.789”,它依舊能匹配成功,這明顯是個非法郵箱,更精確的正則應該這樣寫:

std::string data = "123@qq.vip.com, \
	       456@gmail.com, \
           789@163.com.cn.mail, \
           abcd@my.com, \
           Abc0_@aAa1.123.456.789 \
           haha@163.com.cn.com.cn";
std::regex reg("[a-zA-z0-9_]+@[a-zA-z0-9]+(\\.[a-zA-z]+){1,3}");

std::sregex_iterator pos(data.cbegin(), data.cend(), reg);
decltype(pos) end;
for (; pos != end; ++pos)
{
	std::cout << pos->str() << std::endl;
}

輸出為:

123@qq.vip.com
456@gmail.com
789@163.com.cn.mail
abcd@my.com
haha@163.com.cn.com

2. 匹配IP

有這樣一串IP地址,192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30, 要求:取出其中的IP地址,並按地址段順序輸出IP地址。

有點晚了,便不詳細解釋了,這里直接給出答案,可供大家參考:

std::string ip("192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30");

std::cout << "原內容為:\n" << ip << std::endl;

// 1. 位數對齊
ip = std::regex_replace(ip, std::regex("(\\d+)"), "00$1");

std::cout << "位數對齊后為:\n" << ip << std::endl;

// 2. 有0的去掉
ip = std::regex_replace(ip, std::regex("0*(\\d{3})"), "$1");

std::cout << "去掉0后為:\n" << ip << std::endl;

// 3. 取出IP
std::regex reg("\\s");
std::sregex_token_iterator pos(ip.begin(), ip.end(), reg, -1);
decltype(pos) end;

std::set<std::string> ip_set;
for (; pos != end; ++pos)
{
	ip_set.insert(pos->str());
}

std::cout << "------\n最終結果:\n";

// 4. 輸出排序后的數組
for (auto elem : ip_set)
{
	// 5. 去掉多余的0
	std::cout << std::regex_replace(elem, 
		std::regex("0*(\\d+)"), "$1") << std::endl;
}

輸出結果為:

原內容為:
192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30
位數對齊后為:
00192.0068.001.00254 00102.0049.0023.00013 0010.0010.0010.0010 002.002.002.002 008.00109.0090.0030
去掉0后為:
192.068.001.254 102.049.023.013 010.010.010.010 002.002.002.002 008.109.090.030
------
最終結果:
2.2.2.2
8.109.90.30
10.10.10.10
102.49.23.13
192.68.1.254


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM