C++标准库(九):正则表达式

本文为《C++ Primer》的读书笔记

目录

#include <regex>	// regular expression

regex

  • regex 类表示一个正则表达式
    • 默认情况下,regex 使用的正则表达式语言是 ECMAScript
  • 标准库还定义了一个 wregex 类保存类型 wachar_t, 其操作与 regex 完全相同。两者唯一的差别是 wregex 的初始值必须使用 wchar_t 而不是 char

regex 支持的操作

初始化

regex r(re)
  • re 表示一个正则表达式, 它可以是一个 string、一个表示字符范围的迭代器对、一个指向空字符结尾的字符数组的指针、一个字符指针和一个计数器或是一个花括号包围的字符列表
  • 我们使用的 RE 库类型必须与输入序列类型匹配
    C++标准库(九):正则表达式

赋值

r1 = re
r1.assign(re)	// 与 = 效果相同
  • r1中的正则表达式替换为rere表示一个正则表达式, 它可以是另一个regex对象、一个string、一个指向空字符结尾的字符数组的指针或是一个花括号包围的字符列表

设置标志

  • 当我们定义一个regex或是对一个regex调用assign为其赋予新值时,可以指定一些标志来影响regex如何操作。这些标志控制regex对象的处理过程
regex r(re, f)
r1.assign(re, f)
  • f是指出对象如何处理的标志,如下表所示,正则表达式语法一共有 6 个,我们必须且只能设置其中一个标志。默认为 ECMAScript
    C++标准库(九):正则表达式

r.mark_count()

  • r中子表达式的数目

r.flags()

  • 返回r的标志集

regex_error

  • 正则表达式本身可以看作用一种简单程序设计语言编写的“程序” 。这种语言不是由C++编译器解释的。正则表达式是在“程序”运行时,当一个regex对象被初始化或被赋予一个新模式时,才被“编译”的;因此,如果我们编写的正则表达式存在错误,则在 “程序”运行时 标准库会抛出一个类型为 regex_error 的异常
    • 避免创建不必要的正则表达式
      • 如我们所见, 一个正则表达式所表示的“程序”是在运行时而非编译时编译的。正则表达式的编译是一个非常慢的操作,特别是在你使用了扩展的正则表达式语法或是复杂的正则表达式时。因此, 构造一个regex 对象以及向一个已存在的regex 赋予一个新的正则表达式可能是非常耗时的。为了最小化这种开销,你应该努力避免创建很多不必要的regex
      • 特别是, 如果你在一个循环中使用正则表达式, 应该在循环外创建它,而不是在每步迭代时都编译它
  • 类似标准异常类型,regex_error有一个what操作来描述发生了什么错误
  • regex_error还有一个名为code的成员,用来返回某个错误类型对应的数值编码。code返回的值是由具体实现定义的。RE 库能抛出的标准错误如下表所示,code 成员, 返回对应错误类型的编号 (编号从0开始)
    C++标准库(九):正则表达式
try {
 	// 错误: alnum漏掉了右括号,构造函数会抛出异常
	regex r("[[:alnum:]+\\.(cpp|cxx|cc)$", regex::icase);
} catch (regex_error e)
	{ 	cout << e.what() << "\ncode: "<< e.code() << endl; }

output:

regex error(error_brack):
The expression contained mismatched [ and ].
code: 4

ECMAScript

参考:C++正则表达式1C++正则表达式2

表达式

  • 一般的字符,可匹配目标序列中相同的字符
    • 例如:a 只能匹配 a; abc只能匹配abc
  • 通配符 .:表示一个除了\n以外的任意一个字符
    • \.表示一个真正的.符号
  • ():分组,大正则中包含小正则。可以改变默认的优先级。在模式中可以使用 \1 来表示第一组捕获到的内容
  • |字符串1|字符串2 表示一个字符串,该字符串是字符串1、字符串2中的一个
    • |在正则中的优先级比较混乱,所以建议加上足够多的括号来分组
  • [expr]:可以匹配目标序列中包含在表达式expr定义集内的字符或者排序规则表达式;中括号中出现的所有字符都是代表本身意思的字符(没有特殊含义),如[.]只能匹配.符号,而不能匹配任意符号;表达式expr可以包含下列的任意组合:
    • 单个字符。例如:
      • [A] 匹配 A
      • [abc] 匹配 a, b, c 中的任意一个字符
    • ch1-ch2形式的字符域,例如:
      • [A-F] 匹配任意大写字母
      • [a-z] 匹配任意小写字母
    • [:name:] 形式的字符类,例如:
      • [[:alpha:]] 匹配任意字母 (不区分大小写)
      • [[:alnum:]] / [[:w:]] 匹配任意字母 (不区分大小写)或数字
      • [[:digit:]] / [[:d:]] 匹配任意数字
      • [[:lower:]]匹配小写字母
      • [[:upper:]]匹配大写字母
      • [[:punct:]]匹配英文标点
      • [[:sapce:]] / [[:s:]]匹配空格
      • [[:xdigit:]]匹配十六进制的字符(数字,a,b,c,d,e,f,A,B,C,D,E,F)
      • [[:blank:]]匹配空格或制表符
      • [[:print:]]匹配字母(不区分大小写)、数字、英文标点和空格
      • [[:graph:]]匹配字母(不区分大小写)、数字和英文标点
      • [[:cntrl:]]匹配文件格式转义字符
  • [^expr]:可匹配目标序列中未包含在表达式expr定义集内的字符或排序规则表达式,例如:
    • [^a-z]表示一个字符,该字符不是a、b、c……z中的任何一个
  • 定位点
    • ^:指示字符串的头,且要求字符串以字符开头,不占位
      • \^表示一个真正的^符号
    • $:指示字符串的尾,且要求字符串以字符结尾,不占位
      • \$表示一个真正的$符号
  • 转义序列
    • 文件格式转义
      • \\\f\n\r\t\v,他们分别匹配目标序列中的反斜杠、换页符、回车符、水平制表符合垂直制表符
    • DSW 字符转义
      • \w:等价于[a-zA-Z0-9] / [[:w:]] / [[:alnum:]]
      • \W:等价于[^a-zA-Z0-9] / [^[:w:]] / [^[:alnum:]]
      • \d:等价于[0-9] / [[:d:]] / [[:digit:]]
      • \D:等价于[^0-9] / [^[:d:]] / [^[:digit:]]
      • \s:等价于[[:s:]] / [[:space:]]
      • \S:等价于[^[:s:]] / [^[:space:]]
    • 8 进制数转义
      • \ooo,”ooo”表示三位的八进制数,例如:\101 匹配字符 A
    • 16进制数转义
      • \xhh,”hh”表示两位的十六进制数,例如:\x41 匹配字符 A
    • Unicode 转义
      • \uhhhh,”hhhh”表示四位的十六进制数,例如:\u0041 匹配字符 A

量词

  • E{n,m}:至少匹配 n n n 次,至多匹配 m m m 次E
    • E{n,}:至少匹配 n n n 次E
    • E{,m}:至多匹配 m m m 次E
    • E{n}:匹配 n n n 次 E,等价于 E{n,n}
    • 例如:\{d} 表示单个数字而 \{d}{n} 则表示一个 n n n 个数字的序列
  • E*:匹配 0 次或者多次 E (表达式) ,等价于 E{0,}
  • E+:至少匹配一次 E,等价于 E{1,}
  • E?:匹配 0 次或者 1 次 E,等价于 E{0,1}

断言

  • \b:指示单词的边界
    • 例如:\bmail\b 匹配的就是一个单词 mail
  • \B:一个非单词的边界
  • (?=E):表达式后面紧跟着 E E E 才匹配成功,但不会改名目标序列中匹配的位置
    • This is a T(?=est)表示如果T后面有est就匹配,这主要用于替换
  • (?!E):表达式后没有紧跟着 E E E 才匹配成功,但不会改名目标序列中匹配的位置
    • This is a T(?!est)表示如果 T 后面没有 est 就匹配,这主要用于替换
  • (?:E):表达式后面紧跟着 E E E 才匹配成功

注意:反斜杠的转义

  • 注意,在字符串中写正则表达式时,由于反斜线也是 C++ 中的一个特殊字符,我们在字符串字面常量中必须连续使用两个反斜线来告诉 C++ 我们想要一个普通反斜线字符。
    • 例如:匹配 11 位电话号码
regex r("\\d{1, 11}");		// 即 正则表达式 \d{1,11}

regex_match, regex_search

  • regex_match 将一个字符序列与一个正则表达式匹配,如果整个输入序列与表达式匹配, 则 regex_match 函数返回 true
  • regex_search 寻找第一个与正则表达式匹配的子序列,如果输入序列中一个子串与表达式匹配, 则 regex_search 函数返回 true
    C++标准库(九):正则表达式

m a t c h match match 类型

  • m a t c h match match 类型 (匹配类型):容器类, 保存在输入序列中搜索的结果;类型必须与输入序列类型匹配
    • smatch 表示 string 类型的输入序列
    • cmatch 表示字符数组序列
    • wsmatch 表示宽字符串 wstring 输入
    • wcmatch 表示宽字符数组

下面以 smatch 为例,其它类型的操作都一样,只不过是输入类型不一样


  • smatch 匹配类型有两个名为prefixsuffix的成员, 分别返回表示输入序列中当前匹配之前和之后部分的ssub_match对象
  • 一个ssub_match对象有两个名为strlength 的成员, 分别返回匹配的string和该string的大小
    C++标准库(九):正则表达式

// 查找不在字符 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; //打印匹配的单词
// 识别扩展名 (扩展名一般不考虑大小写)
// 一个或多个字母或数字字符后接一个'.'再接"cpp "或"cxx "或"cc"
regex r("[[:alnum:]]+\\.(cpp|cxx|cc)$", regex::icase);
smatch results;
string filename;
while (cin >> filename)
	if (regex_search(filename, results, r))
		cout << results.str() << endl; //打印匹配结果

匹配和格式化标志

  • 匹配和格式化标志的类型为 match_flag_type。这些值都定义在名为 regex_constants 的命名空间中,而 regex_constants 定义在命名空间 std 中:
using std::regex_constants::xxx;
using namespace std::regex_constants;

C++标准库(九):正则表达式

使用子表达式

  • 正则表达式中的模式通常包含一个或多个子表达式 (subexpression)。一个子表达式是模式的一部分。正则表达式语法通常用括号表示子表达式
// r 有两个子表达式:第一个是点之前表示文件名的部分, 笫二个表示文件扩展名
regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$", regex::icase);
if (regex_search(filename, results, r))
	cout << results.str(1) << endl; // 打印第一个子表达式

子匹配操作

  • 子表达式的一个常见用途是验证必须匹配特定格式的数据
    C++标准库(九):正则表达式

  • 例如, 美国的电话号码有十位数字, 包含一个区号和一个七位的本地号码。区号通常放在括号里, 但这并不是必需的。剩余七位数字可以用一个短横线、一个点或是一个空格分隔, 但也可以完全不用分隔符。我们可能希望接受任何这种格式的数据而拒绝任何其他格式的数。我们将分两步来实现这一目标: 首先,我们将用一个正则表达式找到可能是电话号码的序列, 然后再调用一个函数来完成数据验证
  • 为了验证电话号码, 我们需要访问模式的组成部分。例如, 我们希望验证区号部分的数字如果用了左括号, 那么它是否也在区号后面用了右括号。即, 我们不希望出现 ( 908.555.1800 (908.555.1800 (908.555.1800 这样的号码。为了获得匹配的组成部分, 我们需要在定义正则表达式时使用子表达式
// 整个正则表达式包含 7 个子表达式
// 子表达式 1, 3, 4, 6 是可选的;2, 5, 7 保存号码
"(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})";
  • 理解此模式的最简单的方法是逐个剥离(括号包围的)7 个子表达式:
    • (\\()? 表示区号部分可选的左括号
    • (\\d{3}) 表示区号
    • (\\))? 表示区号部分可选的右括号
    • ([-. ])? 表示区号部分可选的分隔符
    • (\\d{3}) 表示号码的下三位数字
    • ([-. ])? 表示可选的分隔符
    • (\\d{4}) 表示号码的最后四位数字
// 每个 smatch 对象会包含八个 ssub_match 元素
// 位置 [0] 的元素表示整个匹配; 元素 [1] ... [7] 表示每个对应的子表达式
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();	// 如果 ssub_match 未匹配,则 str() 返回空 string
}
string phone =
	"(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})";
regex r(phone); 
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;
}

匹配与 Regex 迭代器类型

  • regex 迭代器是一种迭代器适配器,被绑定到一个输入序列和一个regex 对象上。 每种不同输入序列类型都有对应的特殊regex迭代器类型
    C++标准库(九):正则表达式
  • 当我们将一个sregex_iterator绑定到一个string和一个regex对象时,迭代器自动定位到给定string 中第一个匹配位置。即, sregex_iterator 构造函数对给定stringregex调用regex_search
  • 当我们解引用迭代器时, 会得到一个对应最近一次搜索结果的smatch对象。当我们递增迭代器时,它调用regex_search在输入string中查找下一个匹配
  • sregex_iterator 起到尾后迭代器的作用
// 查找前一个字符不是c的字符串ei
string pattern("[^c]ei");
// 我们想要包含pattern的单词的全部内容
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
regex r(pattern, regex::icase); //在进行匹配时将忽略大小写
// 它将反复调用regex_search来寻找文件中的所有匹配,并输出匹配单词的前、后最多40个字符
// 假定名为file的string保存了我们要搜索的输入文件的全部内容
for (sregex_iterator it(file.begin(), file.end(), r), end_it;
		it != end_it; ++it) 
{
	auto pos = it->prefix().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;
}

regex_replace

  • 当我们希望在输入序列中查找并替换一个正则表达式时, 可以调用 regex_replace
  • 类似搜索函数, 它接受一个输入字符序列和一个 regex 对象, 不同的是, 它还接受一个描述我们想要的输出形式的字符串
    C++标准库(九):正则表达式

string re = "E(?!xpression)";
regex rule(re);
string str = "Regular E";
cout << regex_replace(str,rule,"Expression") << endl; //Regular Expression
string phone = "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})";
// 将号码格式改为 ddd.ddd.dddd
string fmt = "$2.$5.$7 "; 		// 用 $ 后跟子表达式的索引号来表示一个特定的子表达式;这里在替换字符串中使用第 2,5,7 个子表达式
regex r(phone); //用来寻找模式的regex对象

string number = " (908) 555-1800";
cout << regex_replace(number, r, fmt) << endl;	// 908.555.1800

string s;
// 从输入文件中读取每条记录,如果未匹配则原样输出,如果匹配成功则进行替换后输出
while (getline(cin, s))
	cout << regex_replace(s, r, fmt) << endl;

// 从输入文件中读取每条记录,如果未匹配则不输出,如果匹配成功则进行替换后输出
while (getline(cin, s))
	cout << regex_replace(s, r, fmt, format_no_copy) << endl;
上一篇:Java基础--正则表达式的规则


下一篇:C# 判断邮件格式