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是否匹配。
如果整个输入序列与表达式匹配,则regex_match函数返回true;
如果输入序列中一个子串与表达式匹配,则regex_search函数返回true;
如果想要将找到的序列提问为另一个序列时,使用regex_replace。

下表列出regex的函数参数,都返回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的值,会影响匹配过程。

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:]]匹配任意字母,符号+和*分别代表我们表示希望“一个或多个”或“零个或多个”匹配。因此,[[:alpha:]]*将匹配零个或多个字母。

最后运行结果:

freind
  • 指定regex对象的选项

当我们定义一个regex或对一个regex调用assign为其赋予新值时,可以指定一些标志来影响regex如何操作。共有6个标志,可用于指出编写正则表达式所用的语言,而且必须设置其中之一。默认设置ECMAScript标志。

regex和wregex选项:

regex r(re)
regex r(re, f)
re表示一个正则表达式,可以说一个string、一个表示字符范围的迭代器对、
一个指向空字符结尾的字符数组的指针、一个字符指针和一个计数器或是一个花括号包围
的字符列表。f是指出对象如何处理的标志。f通过下面列出的值来设置。如果未指定f,
其默认值为ECMAScript
r1 = re 将r1中的正则表达式替换为re。re表示一个正则表达式,它可以是另一个regex对象、
一个string、一个指向空字符结尾的字符数组的指针或是一个花括号报文的字符列表
r1.assign(re, f) 与使用赋值运算符(=)效果相同;可选的标志f也与regex的构造函数中对应的参数含义相同
r.mark_count() r中子表达式的数目
r.flag() 返回r的标志集

注:构造函数和赋值操作可能抛出类型为regex_error的异常。

定义regex时指定的标志,位于regex和regex_constants::syntax_option_type。

icase 在匹配过程中忽略大小写
nosubs 不保存匹配的子表达式
optimize 执行速度优先于构造速度
ECMAScript 使用ECMA-262指定的语法
basic 使用POSIX基本的正则表达式语法
extended 使用POSIX扩展的正则表达式语法
awk 使用POSIX版本的awk语言的语法
grep 使用POSIX版本的grep的语法
egrep 使用POSIX版本的egrep的语法

前面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库能抛出的标准错误如下表(正则表达式错误类型):

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

建议:避免创建不必要的正则表达式。因为正则表达式是在运行时编译的,编译非常慢,特别是在使用了扩展的正则表达式语法或者复杂的正则表达式时。耗时的步骤在于:构造一个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})";

子表达式:
1 (\()? 表示区号可选的左括号,其中第1个左括号跟最后一个右括号配对
2 (\d{3}) 表示3个数字的区号
3 (\))? 表示区号部分可选的右括号,其中第1个左括号跟第一个右括号配对
4 ([-. ])? 表示区号部分可选的分隔符(短横线、点、空格)
5 (\d{3}) 表示号码的下三位数字
6 ([-. ]?) 表示可选的分隔符
7 (\d{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 只替换子表达式的第一次出现
posted @ 2022-12-09 23:31  明明1109  阅读(493)  评论(0编辑  收藏  举报