C++ Primer学习笔记 - 第17章 标准库特殊设施(正则表达式)
17.3 正则表达式
正则表达式(regular expression)是一种描述字符序列的方法,一种强大的计算工具。这里重点介绍如何使用C++正则表达式库(RE库)。
头文件:<regex>。
组件
正则表达式组件:
regex表示有一个正则表达式的类regex_match()将一个字符序列与一个正则表达式匹配regex_replace()使用给定格式替换一个正则表达式sregex_iterator迭代器适配器,调用regex_search来遍历一个string中的所有匹配的子串smatch容器类,保存在string中搜索的结果ssub_match存放string中匹配的子表达式的结果
regex类表示一个正则表达式。函数regex_match,regex_search确定一个给定字符序列与一个给定regex是否匹配。
1)如果整个输入序列与表达式匹配,则regex_match函数返回true;
2)如果输入序列中一个子串与表达式匹配,则regex_search函数返回true;
3)如果想要将找到的序列提问为另一个序列时,使用regex_replace。
下表列出regex_search和regex_match的参数,都返回bool且都被重载为多个版本:其中之一接受一个smatch类型的附加参数。如果匹配成功,函数将成功匹配的相关信息保存在给定的smatch对象中。
regex_search和regex_match的参数:
(seq, r, mft) 在字符序列seq中查找regex对象r中的正则表达式。seq可以是一个string、表示范围的一对迭代器以及一个指向空字符结尾的字符数组的指针。
(seq, m, r, mft) m是一个match对象,用来保存匹配结果的相关细节。m和seq必须具有兼容的类型。mft是一个可选的regex_constants::match_flag_type的值,会影响匹配过程。
regex_search部分原型:
// 在字符串str中,查找pattern对象中的正则表达式
bool regex_search(const string& str, const regex& pattern);
// 在字符串str中,查找pattern对象中的正则表达式,匹配细节存放到matches中
bool regex_search(const string& str, smatch& matches, const regex& pattern);
regex_match部分原型:
// 在字符串str中,查找pattern对象中的正则表达式
bool regex_match(const string& str, const regex& pattern);
// 在字符串str中,查找pattern对象中的正则表达式,匹配细节存放到matches中
bool regex_match(const string& str, smatch& matches, const regex& pattern);
regex_match与regex_search区别:
regex_match必须匹配整个字符串,速度更快,常用于验证输入格式;
regex_search可以匹配子字符串,速度更慢,常用于查找模式出现.
语法
基本匹配
- 普通字符:字母、数字、下划线等直接匹配自身
a 匹配字符 "a";3 匹配数字 "3"
- 特殊字符:需要转义的元字符
. * + ? ^ $ \ | ( ) [ ] { }
\. 匹配点号(.);\\ 匹配反斜杠(\)
字符类
[abc]:匹配 a、b 或 c
[^abc]:匹配不是 a、b 或 c 的字符
[a-z]:匹配 a 到 z 的任意小写字母
[A-Z0-9]:匹配大写字母或数字
预定义字符类:
\d = [0-9] (数字)
\D = [^0-9] (非数字)
\w = [a-zA-Z0-9_] (单词字符)
\W = [^\w] (非单词字符)
\s = [ \t\r\n\f] (空白字符)
\S = [^\s] (非空白字符)
重复量词
*:0次或多次
+:1次或多次
?:0次或1次
{n}:恰好n次
{n,}:至少n次
{n,m}:n到m次
e.g.
a*:匹配 "", "a", "aa", "aaa", ...
a{3}:只匹配 "aaa"
a{2,4}:匹配 "aa", "aaa", "aaaa"
位置锚点
^:字符串开始(或多行模式下的行首)
$:字符串结束(或多行模式下的行尾)
\b:单词边界
\B:非单词边界
e.g.
^Hello:匹配以 "Hello" 开头的字符串
world$:匹配以 "world" 结尾的字符串
\bcat\b:匹配整个单词 "cat"
分组和捕获
(exp):捕获分组并分配组号
(?:exp):非捕获分组
(?<name>exp):命名捕获组(部分语言支持)
\n:引用第n个捕获组
# \d: 匹配[0-9]数字; {4}: 重复4次
(\d{4})-(\d{2})-(\d{2}) # 匹配日期,捕获年、月、日
(?:www\.)?example\.com # 匹配带或不带www的域名
选择分支
|:或操作符
cat|dog 匹配 "cat" 或 "dog"
gr(a|e)y 匹配 "gray" 或 "grey"
特殊构造
(?=exp):正向先行断言(后面是exp)
(?!exp):负向先行断言(后面不是exp)
(?<=exp):正向后行断言(前面是exp)
(?<!exp):负向后行断言(前面不是exp)
e.g.
\d+(?=px):匹配后面跟着"px"的数字
(?<!\$)\d+:匹配前面没有$的数字
原始字符串字面量(R"")
用于简化正则表达式和其他包含大量特殊字符(如\,"等)的字符串定义。主要用来 取消字符串中的转义机制,使字符串内容保持原始的形式。
e.g. 下面用2个等价的正则表达式,对使用R""和未使用的情形进行对比:
// 未使用R""
// 普通字符串:匹配一个数字(\d 需要写成 \\d)
std::regex pattern("\\d+"); // 可读性差
// 使用R""
// 原始字符串:\d 直接书写,无需转义
std::regex pattern(R"(\d+)"); // 更清晰
17.3.1 使用正则表达式
一个简单例子
查找“不在c之后的ei”的单词。
// 查找不在字符c之后的字符串ei
string pattern("[^c]ei");
// 包含pattern的整个单词
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
regex r(pattern); // 构造一个用于查找模式的regex
smatch results; // 定义一个对象保存搜索结果
// 定义一个string保存与模式匹配和不匹配的文本
string test_str = "receipt freind theif receive";
// 用r在test_str中查找与pattern匹配的字符串
if (regex_search(test_str, results, r)) // 如果有匹配子串
cout << results.str() << endl; // 打印匹配的单词
regex使用正则表达式语言ECMAScript,模式[[:alpha:]]匹配任意字母(仅在支持POSIX语法环境中有效),符号+,*分别表示表示希望“一个或多个”,“零个或多个”匹配。因此,[[:alpha:]]*将匹配零个或多个字母。
运行结果:
freind
regex选项
当我们定义一个regex对象,或对其调用assign赋予新值时,可指定一些标志来影响regex如何操作。共有6个标志,可用于指出编写正则表达式所用的语言,而且必须设置其中之一。默认设置ECMAScript标志。
regex和wregex选项:
| regex r(re) regex r(re, f) |
re表示一个正则表达式,可以是一个string、表示字符范围的迭代器对、指向空字符结尾的字符数组的指针、字符指针、计数器,或"{ }"包围的字符列表。 可选标志f,用于控制语法和匹配行为。 如果未设置f,则默认值为ECMAScript |
| r1 = re | 将r1的正则表达式替换为re。re表示一个正则表达式,可以是另一个regex对象、string、指向空字符结尾的字符数组的指针,或"{ }"包围的字符列表 |
| r1.assign(re, f) | 与使用赋值运算符(=)效果相同;可选的标志f也与regex的ctor对应的参数含义相同 |
| r.mark_count() | r中子表达式的数目 |
| r.flag() | 返回r的标志集 |
注:构造函数和赋值操作可能抛出类型为regex_error的异常。
定义regex时指定的标志f,位于regex和regex_constants::syntax_option_type。
可选标志f可以是下面标志之一或者它们的组合:
| 标志 | 说明 |
|---|---|
| icase | 在匹配过程中忽略大小写 |
| nosubs | 不保存匹配的子表达式 |
| optimize | 执行速度优先于构造速度 |
| ECMAScript | 使用ECMA-262指定的语法 |
| basic | 使用POSIX基本的正则表达式语法 |
| extended | 使用POSIX扩展的正则表达式语法 |
| awk | 使用POSIX版本的awk语言的语法 |
| grep | 使用POSIX版本的grep的语法 |
| egrep | 使用POSIX版本的egrep的语法 |
e.g. 匹配以"hello"开头的字符串,不区分大小写
std::regex pattern("^hello", std::regex_constants::icase | std::regex_constants::ECMAScript);
前面3个标志(icase、nosubs、optimize),允许我们指定正则表达式处理过程中与语言无关的方面。
例:用icase标志,查找C++源码文件(后缀名.cc/.cpp/.cxx)
// 一个或多个字母或数字字符后接一个'.'再接"cpp"或"cxx"或"cc"
// 标志icase表示匹配忽略大小写
regex r("[[:alnum:]]+\\.(cpp|cxx|cc)$", regex::icase);
smatch results; // 存放匹配结果
string filename;
while (cin >> filename)
if (regex_search(filename, results, r))
cout << "matched: " << results.str() << endl; // 打印匹配结果
- 正则表达式错误
正则表达式是在运行时解析(非C++编译器),可能存在错误。如果存在错误,运行时标准库会抛出一个类型为regex_error的异常。类似标准异常类型,regex_error.what()用来描述发生了什么错误,.code用来返回某个错误类型对应编码。code返回值由具体实现定义。
RE库能抛出的标准错误(正则表达式错误类型):
| 标准错误编码code | 描述 |
|---|---|
| error_collate | 无效的元素校对请求 |
| error_ctype | 无效的字符类 |
| error_escape | 无效的转义字符或无效的尾置转义 |
| error_backref | 无效的向后引用 |
| error_brack | 不匹配的方括号([或]) |
| error_paren | 不匹配的小括号((或)) |
| error_brace | 不匹配的花括号({或}) |
| error_badbrace | {}中无效的范围 |
| error_range | 无效的字符范围(如[z-a]) |
| error_space | 内存不足,无法处理此正则表达式 |
| error_badrepeat | 重复字符(*、?、+或{)之前没有有效的正则表达式 |
| error_complexity | 要求的匹配过于复杂 |
| error_stack | 栈空间不足,无法处理匹配 |
例:在模式中意外遇到一个方括号缺失异常
try {
// 错误: alnum 漏掉了右括号, 构造函数会抛出异常
regex r("[[:alnum:]+\\.(cpp|cxx|cc)$", regex::icase);
}
catch (regex_error e) {
cout << e.what() << "\ncode: " << e.code() << endl;
}
运行结果(GNU g++ 9.4.0):
Unexpected character in bracket expression.
code: 4
tips:避免创建不必要的正则表达式。因为正则表达式运行时编译,速度非常慢,特别在使用了扩展的正则表达式语法或者复杂的正则表达式时。耗时步骤在于:构造一个regex对象,以及向一个已存在的regex赋予一个新的正则表达式。
正则表达式类和输入序列类型
输入支持普通char、wchar_t数据,字符可以保存在std::string或char数组(或宽字符版本,wstring或wchar_t数组中)。如regex类保存类型char的正则表达式,wregex类保存类型wchar_t,操作完全相同。
匹配和迭代器类型较特殊,差异不仅在于字符类型,还在于序列是在标准string中还是数组中:smatch表示string类型的输入序列;cmatch表示字符数组序列;wsmatch表示宽字符(wstring)输入;而wcmatch表示宽字符数组。
例如,下面的代码会编译失败,因为match参数的类型与输入序列的类型不匹配:
regex r("[[:alnum:]]+\\.(cpp|cxx|cc)$", regex::icase);
smatch results; // 将匹配string输入序列, 而不是char*
if (regex_search("myfile.cc", results, r)) // 错误:输入为char*
cout << results.str() << endl;
如果不改变输入序列"myfile.cc"的类型(char*),那么就需要将match的类型由smatch改为cmatch。
cmatch results;
if (regex_search("myfile.cc", results, r))
cout << results.str() << endl; // 打印当前匹配
输入序列类型和对应的RE库组件关系:
| 如果输入序列类型 | 则使用正则表达式类 |
|---|---|
| string | regex、smatch、ssub_match和sregex_iterator |
| const char* | regex、cmatch、csub_match和cregex_iterator |
| wstring | wregex、wsmatch、wssub_match和wsregex_iterator |
| const wchar_t* | wregex、wcmatch、wcsub_match和wcregex_iterator |
17.3.2 匹配与Regex迭代器类型
当输入序列类型为string时,可以使用sregex_iterator(regex迭代器)来获取所有匹配。regex迭代器是一种迭代器适配器,被绑定到一个输入序列和一个regex对象上。每种输入序列类型,都对应不同的regex迭代器类型,如下表:
sregex_iterator操作(也适用于cregex_iterator, wsregex_iterator, wcregex_iterator)
| sregex_iterator it(b, e, r) | 一个sregex_iterator,遍历迭代器b和e表示的string,它调用sregex_search(b, e, r)将it定位到输入中第一个匹配的位置 |
| sregex_iterator end; | sregex_iterator的尾后迭代器 |
| *it, it-> | 根据最好一个调用regex_search的结果,返回一个smatch对象的引用或一个指向smatch对象的指针 |
| ++it, it++ | 从输入序列当前匹配位置开始调用regex_search,前置版本返回递增后迭代器;后置版本返回旧值 |
| it1 == it2, it1 != it2 | 如果两个sregex_iterator都是尾后迭代器,则它们相等;两个非尾后迭代器都是从相同点输入序列和regex对象构造,则它们相等 |
当我们将一个sregex_iterator绑定到一个string和一个regex对象时,迭代器自动定位到给定string中第一个匹配位置。sregex_iterator构造函数对给定string和regex调用regex_search。当我们解引用迭代器时,会得到一个对应最近一次搜索结果的smatch对象。当我们递增迭代器时,它调用regex_searchz输入string中查找下一个匹配。
- 使用sregex_iterator
// 查找前一个字符不是c的字符串ei
string pattern("[^c]ei");
// 想要包含pattern的单词的全部内容
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
regex r(pattern, regex::icase); // 在进行匹配时将忽略大小写
// 它将反复调用regex_search来寻找文件中的所有匹配
for (sregex_iterator it(file.begin(), file.end()), r, end_it;
it != end_it, ++it)
cout << it->str() << endl; // 匹配的单词
上面代码中,for循环遍历file中每个与r匹配的子串。for语句中的初始值定义了it和end_it。定义it时,sregex_iterator的构造函数调用regex_search将it定位到file中第一个与r匹配的位置。end_it是一个空sregex_iterator,起到尾后迭代器的作用。for语句中的递增运算通过regex_search来“推进”迭代器。解引用迭代器时,会得到一个表示当前匹配结果的smatch对象。
- 使用匹配数据
到目前为止,我们通过正则表达式获得的是匹配结果smatch是单词,但可能希望看到匹配单词上下文,该如何处理?
例如,我们喜欢看到匹配的单词,会这样上下文:
hey read or write according to the type
>>> begin <<<
handled. The input operatiors ignore whi
可以使用ssub_match类型的内容,来获取匹配的上下文。匹配类型有2个名为prefix和suffix的成员,分别表示输入序列中当前匹配之前和之后部分的ssub_match对象。一个ssub_match对象有两个名为str和length的成员,分别返回匹配的string和该string的大小。
// 循环头与之前一样
for (sregex_iterator it(file.begin(), file.end(), r), end_it;
it != end_it; ++it) {
auto pos = it->prefiex().length(); // 前缀的大小
pos = pos > 40 ? pos - 40 : 0; // 我们想要最多40个字符
cout << it->prefix().str().substr(pos) // 前缀的最后一部分
<< "\n\t\t>>>" << it->str() << " <<<\n" // 匹配的单词
<< it->suffix().str().substr(0, 40) // 后缀的第一部分
<< endl;
}
it->prefix() 返回一个ssub_match对象,表示file中当前匹配之前的部分。对该ssub_match对象调用length,能获得前缀部分的字符数目。
像prefix这样属于ssub_match的操作,还有很多(也适用于cmatch、wsmatch、wcmatch和对应的csub_match、wssub_match和wcsub_match):
| m.ready() | 如果已经通过调用regex_search或regex_match设置了m,则返回true;否则返回false。如果ready返回false,则对m进行操作是未定义的 |
| m.size() | 如果匹配失败,则返回0;否则返回最近一次匹配的正则表达式中子表达式的数目 |
| m.empty() | 若m.size()为0,则返回true |
| m.prefix() | 一个ssub_match对象,表示当前匹配之前的序列 |
| m.suffixe() | 一个ssub_match对象,表示当前匹配之后的序列 |
| m.format(...) | 见下文regex_replace章节 |
| m.length(n) | 第n个匹配的子表达式的大小 |
| m.position(n) | 第n个子表达式距序列开始的距离 |
| m.str(n) | 第n个子表达式匹配的string |
| m[n] | 对应第n个子表达式的ssub_match对象 |
| m.begin(), m.end(), m.cbegin(), m.cend() | 表示m中sub_match元素范围的迭代器。cbegin和cend返回const_iterator |
17.3.3 使用子表达式
正则表达式中的模式通常包含一个或者多个子表达式(subexpression)。子表达式是模式的一部分,本身也有意义。正则表达式通常用括号表示子表达式,例如:
// r有2个子表达式:
// ([[:alnum:]]+) 匹配一个活多个字符的序列
// (cpp|cxx|cc) 匹配文件扩展名
regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$", regex:icase);
在前面,我们通过regex_search搜索字符串匹配模式的部分,用smatch对象保存结果,用smatch::str()得到匹配字符串;在子表达式也能单独匹配,不过是通过smatch::str(1)来访问第一个子表达式匹配的结果,str(2)访问第二个子表达式匹配结果。例如:
// filename 是要搜索的文件名,string类型
// results 是保存匹配结果,smatch类型
// r是上面的regex对象,包含子表达式
if (regex_search(filename, results, r))
cout << results.str(1) << endl; // 打印第一个子表达式匹配的部分
- 子表达式用于数据验证
子表达式常见用途:验证特定格式的数据。例如,美国电话号码有10位,形如“001-2223333”,包含一个区号(001)和一个七位的本地号码(2223333)。区号通常放在括号里,但不是必须的,区号跟剩余7位数字用短横线、一个点或一个空格分隔,但也可以不用分隔。
我们希望接受任何这种格式的数据,拒绝任何其他格式的数。我们将分两步实现:1)用一个正则表达式找到可能是电话号码的序列;2)调用一个函数来完成数据验证。
ECMAScript正则表达式特性:
\{d}表示单个数字,而\{d}{n}则表示一个n个数字的序列。(如,\{d}{3}匹配三个数字的序列)- 在方括号中的字符集合表示匹配这些字符中任意一个。(如,
[-. ]匹配一个短横线或一个点或一个空格。注意,点在括号中没有特殊含义) - 后接'?'的组件是可选的。(如,
\{d}{3}[-. ]?\{d}{4}匹配这样的序列:开始3个数字,后接一个可选的短横线或点或空格,然后是4个数字。此模式可匹配555-0132或555.0132或555 0132或5550132) - 类似C++,ECMAScript用反斜线(
\)表示一个字符本身而不是其特殊含义。由于我们的模式包含括号,而括号(不包括中括号、大括号)是ECMAScript中的特殊字符,因此我们用(和)来表示括号是模式的一部分而不是特殊字符。
注意:C++中模式中,\\{d}{3},其中\告诉编译器这是一个普通反斜线字符\,即对应ECMAScript \{d}{3}
如果区号加了左括号,也需要验证右括号。该如何处理?
可以使用子表达式。每个子表达式用一对括号包围:
// 整个表达式含7个子表达式:( ddd )分隔符ddd分隔符ddd 注意这里的: "(" "ddd" ")" 都是1个子表达式
// 子表达式1、3、4、6用?结尾,表示可选;2、5、7保存号码
"(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})";
7个子表达式:
(\\()? (\\d{3}) (\\))? ([-. ])? (\\d{3}) ([-. ])? (\\d{4})
↓ ↓ ↓ ↓ ↓ ↓ ↓
1.可选左括号 2.3位区号 3.可选右括号 4.可选分隔符 5.3位号码 6.可选分隔符 7.4位号码
每个捕获组:
1)(\\()? 匹配可选的左括号,放到区号前,其中第1个左括号(跟最后一个右括号)配对,?表示匹配0次或1次;
2)(\\d{3}) 匹配3位数字,作为区号;
3)(\\))? 匹配可选的右括号,其中第1个左括号(跟第一个右括号)配对;
4)([-. ])? 匹配可选的分隔符,短横线-、点.或空格 ;
5)(\\d{3}) 匹配3位数字,作为号码的前三位;
6)([-. ]?) 匹配可选的分隔符;
7)(\\d{4}) 匹配4位数字,作为号码的后四位;
下面用 “正则表达式+函数验证” 方式,查找与完整的电话号码模式匹配的数据。
// 模式串,用于匹配电话号码,但不能确保左右括号同时存在
string phone = "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})";
regex r(phone); // regex对象,用于查找我们的模式
smatch m;
string s;
// 从输入文件中读取每条记录
while (getline(cin, s)) {
// 对每个匹配的电话号码
for (sregex_iterator it(s.begin(), s.end(), r), end_it; it != end_it; ++it)
{
// 检查号码格式是否合法
if (valid(*it))
cout << "valid: " << it->str() << endl;
else
cout << "not valid: " << it->str() << endl;
}
}
- 使用子匹配操作
上面例子中,pattern有7个子表达式,每个smatch对象(表示匹配结果)包含8个ssub_match元素:
位置[0]元素:整个匹配;元素[1]..[7]:每个对应的子表达式。
如果要为上面例子编写valid函数,就需要针对不同的子表达式匹配结果进行操作。子匹配操作:
| ssub_match对象操作 | 描述 |
|---|---|
| matched | 一个public bool数据成员,指出此ssub_match是否匹配了 |
| first, second | public数据成员,指向匹配序列首元素和尾后位置的迭代器。如果未匹配,则first和second是相等的 |
| length() | 匹配的大小。如果matched为false,则返回0 |
| str() | 返回一个包含输入中匹配部分的string。如果matched为false,则返回空string |
| s = ssub | 将ssub_match对象ssub转化为string对象s。等价于s=ssub.str() 。转化运算符不是explicit的 |
这些操作适用于ssub_match, csub_match, wssub_match, wcsub_match。
valid函数注意点:要么区号前后都有括号(左右括号配对),要么都没有。
bool valid(const smatch& m)
{
// 如果区号前有一个左括号
if (m[1].matched)
// 则区号后必须有一个右括号,之后紧跟剩余号码或一个空格
return m[3].matched && (m[4].matched == 0 || m[4].str() == " ");
else
// 否则,区号后不能有右括号
// 另两个组成部分间的分隔符必须匹配
return !m[3].matched && m[4].str() == m[6].str();
}
17.3.4 使用regex_replace
当想查找序列并替换为另一个序列的时候,可以使用regex_replace。例如,将美国电话号码转化为"ddd.ddd.dddd"形式。
使用$ + 数字,代表一个子表达式,数字指定子表达式是整个表达式的第几个。比如$2,代表第二个子表达式。
// 定义替换字符串
string fmt = "$2.$5.$7"; // 将7个子表达式的号码格式改为ddd.ddd.dddd
// 使用正则表达式模式和替换字符串
regex r(phone); // 寻址模式的regex对象
string number = "(908) 555-1800";
cout << regex_replace(number, r, fmt) << endl; // 将打印 908.555.1800
- 用来控制匹配和格式的标志
标准库定义了用来在替换过程中控制匹配或格式的标志。这些标志可以传递给regex_search,regex_match,或smatch类的format成员。标志格式为match_flag_type类型,值定义在regex_constants命名空间中。
如果要使用的话,需要用using声明或具体类型的using指示:
// using指示
using std::regex_constants::format_no_copy;
// 或者使用 using声明
using namespace std::regex_constants;
匹配标志(定义于regex_constants::match_flag_type)有:
| 匹配标志 | 描述 |
|---|---|
| match_default | 等价于format_default |
| match_not_bol | 不将首字符作为行首处理 |
| match_not_eol | 不将尾字符作为行尾处理 |
| match_not_bow | 不将首字符作为单词首处理 |
| match_not_eow | 不将首字符作为单词尾处理 |
| match_any | 如果存在多余一个匹配,则可返回任意一个匹配 |
| match_not_null | 不匹配任何空序列 |
| match_continuous | 匹配必须从输入的首字符开始 |
| match_prev_avail | 输入序列包含第一个匹配之前的内容 |
| format_default | 用ECMAScript规则替换字符串 |
| format_end | 用POSIX sed规则替换字符串 |
| format_no_copy | 不输出输入序列中未匹配的部分 |
| format_first_only | 只替换子表达式的第一次出现 |

浙公网安备 33010602011771号