正则表达式(regular expression)
一种描述字符序列的方法,是一种极其强大的计算工具。我们重点介绍如何使用c++正则表达式库(RE库)。它是新标准库的一部分。定义在头文件regex中,它包含多个组件。
正则表达式库组件
regex 表示有一个正则表达式的类
regex_match 将一个字符序列与一个正则表达式匹配
regex_search 寻找第一个与正则表达式匹配的子序列
regex_replace 使用给定格式替换一个正则表达式
sregex_iterator 迭代器适配器,调用regex_search来遍历一个string中所有匹配的子串
smatch 容器类,保存在string中搜索的结果
ssub_match string中匹配的子表达式的结果
如果你还不熟悉正则表达式的使用,那么请看下去以获得正则表达式可以做什么的一些概念。
regex类表示一个正则表达式。除了初始化和赋值以外,regex还支持其它一些操作。函数regex_match和regex_search确定一个给定的字符序列是否和一个给定regex匹配。如果整个输入序列与表达式匹配,则regex_match返回true;如果输入序列中一个子串与表达式匹配,则regex_search返回true。
1.简单使用正则表达式库
我们从一个简单的例子开始–查找违反众所周知的拼写规则“i除非在c之后,否则必须在e之前”的单词。
//注释便于理解
除非A,否则B 关系:-B推出A
除非A,否则不B 关系:-(-B)推出A 即 B推出A
#include<string>
#include<iostream>
#include<regex>
int main(){
std::string pattern("[^c]ei");
pattern = "[ [:alpha:] ]*" + pattern + "[[:alpha:]]*";
std::regex r(pattern.begin(),pattern.end()); //构造一个用于查找模式的regex
std::smatch results; //构造一个对象保存搜索结果
std::string test_str = "receipt freind theif receive"; //保存与模式匹配和不匹配的文本
if (std::regex_search(test_str, results, r)) //如果有匹配子串
std::cout << results.str() << std::endl; //打印匹配的单词
system("pause");
return 0;
}
我们首先定义了一个string来保存我么希望查找的正则表达式。正则表达式[ ^c]表示我们希望匹配的是任意不是‘c’的字符,而"[ ^c]ei"指出我们想要匹配这种字符后接ei的字符串。此模式描述的字符串正好包括三个字符。
[ [ :alpha: ] ]:表示匹配任意字母,符号+和符号*分别表示“一个或多个”或“零个或多个”匹配。因此,[ [ :alpha: ] ] *将匹配零个或多个字母。
将正则表达式存入pattern后,我们用它来初始化一个名为r的regex对象。接下来用一个string类型的test_str来进行测试。我们将test_str初始化为与模式匹配的“freind”和“theif”和不匹配的单词“recepit”和“receive”。还定义了一个名为resuls的smatch对象,传递给了regex_search。如果找到匹配子串,results会保存匹配位置的细节信息。接下来我们调用regex_search,如果找到匹配子串,就返回true。我们用results的str()成员来打印匹配的部分。函数regex_search在输入序列中只要找到一个匹配子串就会停止查找。因此程序的输出是:
freind
后边我们会介绍如何打印出全部的符合条件的子串。
2.指定regex对象的选项
定义regex时指定的标志:
这里我们只介绍其中三个标志,剩余6个标志指出编写正则表达式所用的语言。
icase : 在匹配过程中忽略大小写
nosubs:不保存匹配的子表达式
optimize : 执行速度优先于构造速度
举个例子:我们可以用icase标志查找具有特定扩展名的文件名—可以将一个c++程序保存在.cc结尾的文件中。也可以是.Cc,.cC,.CC结尾的文件中,效果是一样的。
//一个或多个字母或数字字符后接一个'.',再接“cpp”或“cxx”或"cc"
regex r("[[:alpha:]]* + \\.(cpp|cxx|cc)$ ".regex::icase);
smatch results;
string filename;
while(cin>>filename){
if(regex_search(filename,results,r))
cout<<results.str()<<endl;
}
这样,此正则表达式按照指定规则匹配的时候就不会考虑大小写了。
至于上述代码中的 \. 写法再做解释:类似于c++语言中有特殊字符,正则表达式中也是一样。而 ‘.’ 通常匹配任意字符。所以需要 ‘\’ 来转义。由于\也是一个特殊字符,所以才需要写成 ‘\.’。第一个\去掉反斜线的特殊含义,第二个实现 ‘.’ 的转义。
使用注意:一个正则表达式所表示的“程序”是在运行时而非编译时编译的。正则表达式的编译是一个很慢的过程,特别是当使用了比较复杂的正则表达式的时候。因此构造一个regex对象或者像一个已经存在regex赋予新的正则表达式可能十分耗时。为了最小化这种开销,应该努力避免创建不必要的regex。如果在一个循环中使用了正则表达式,应该在循环外创建它,而不是在每步迭代时都编译它。
3.正则表达式类和输入序列类型
我们使用的RE库类型必须与输入序列类型相匹配。
例如:
regex r("[[:alpha:]]* + \\.(cpp|cxx|cc)$ ".regex::icase);
smatch results;
if(regex_search("myfile.cc",results,r))
cout<<results.str()<<endl;
这段代码会编译失败。因为match的类型与输入序列的类型不匹配。如果我们希望输入一个字符数组的话,就必须使用cmatch对象
regex r("[[:alpha:]]* + \\.(cpp|cxx|cc)$ ".regex::icase);
cmatch results;
if(regex_search("myfile.cc",results,r))
cout<<results.str()<<endl;
4.匹配与regex迭代器类型
回忆上面搜索指定规范单词的例子,我们最终的打印结果只能打印出第一个符合规范的单词。我们可以使用“sregex_iterator”来获得所有匹配。regex迭代器是一种迭代器适配器,被绑定到一个输入序列和一个regex对象上。
当我们将一个sregex_iterator绑定到一个string和一个regex对象时迭代器自动定位到给定string的第一个匹配位置。当我们解引用迭代器时,会得到一个对应最近一次搜索结果的smatch对象。当我们递增迭代器的时候,它调用regex_search在输入string中查找下一个匹配。
sregex_iterator的使用
std::string pattern("[^c]ei");
pattern = "[ [:alpha:] ]*" + pattern + "[[:alpha:]]*";
std::regex r(pattern.begin(),pattern.end(),regex::icase);
//它将反复调用regex_search来寻找文件中的所有匹配
for(sregex_iterator(file.begin(),file.end(),r),end_it;
it != end_it;++it){
cout<<it->str()<<endl; //匹配的单词
}
这样就会输出所有符合规则的字符串。
5.使用匹配数据
在有些情况下,我们可能不仅仅需要打印指定的单词,可能还会需要打印出匹配单词的上下文。我们再来改进一下程序:
//循环头不变
for(sregex_iterator(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;
}
匹配类型有两个成员:prefix,suffix,分别返回表示输入序列中当前匹配之前和之后部分的ssub_match对象。一个ssub_match对象包含str()和length()成员,分别返回匹配的string和该string的大小。
6.使用子表达式
正则表达式中的模式通常包含一个或多个子表达式。一个子表达式是模式的一部分,本身也具有意义。正则表达式语法通常用括号表示子表达式。
就像之前过滤.cXX后缀文件的时候的写法,我们就是用括号来分组可能的文件扩展名。
//r 有两个子表达式:.之前表示文件名的部分和之后表示文件扩展名的部分
regex r("[[:alpha:]]* + \\.(cpp|cxx|cc)$ ".regex::icase);
现在我们的模式包含两个括号括起来的子表达式:
1.([[:alpha:]]*) :匹配一个或多个字符的序列
2. ( cpp|cxx|cc ) :匹配文件扩展名
之前的程序我们可以稍作更改使其只打印出文件名:
if(regex_search(filename,results,r))
cout<<results.str(1)<<endl;
注意:子匹配是按照位置来访问的。第一个子匹配位置为0,表示整个模式对应的匹配,随后是每个子表达式对应的匹配。
例如,如果文件名是foo.cpp,那么results.str(0)保存的就是foo.cpp,results.str(1)保存的是foo,reults.str(2)表示的是cpp。
7.子表达式用于数据验证
子表达式的一个常见用途是验证必须匹配特定格式的数据。
例如,美国的电话号码有十位数字,包含一个区号和一个七位的本地号码。区号通常放在括号里,但也不是必须的。剩余七位数字可以用一个短横线,一个点或一个空格分隔,也可以完全不用分隔符。我们希望接受任何这种格式的数据而拒绝任何其他格式的数。我们分为两步进行操作。
1.用一个正则表达式找到可能是号码的序列;
2.调用一个函数完成数据验证 ;
在编写电话号码模式之前,我们需要介绍一下ECMAScript正则表达式语言的特性:
·\d 表示单个数字,而\d{n}表示一个n个数字的序列;
·方括号中的字符集合表示匹配这些字符中的任意一个;
·后接'?'的组件是可选的;
·每一次\出现的地方前面都要再加上一个\表示我们需要一个\字符而不是特殊符号。
因此用\\ d{3}表示\d{3};
为了验证电话号码,我们需要访问模式的组成部分。例如,我们希望验证区号部分的数字如果用了左括号,那么它是否也在区号后面用了右括号。即,我们不希望出现(903.1111.3333 这样的号码。
为了获得匹配的组成部分,我们需要在定义正则表达式时使用子表达式。每个子表达式用一对括号包围:
//整个正则表达式包括七个子表达式: (ddd)分隔符 ddd 分隔符 dddd
//子表达式1,3,4和6是可选的;2,5,7保存号码
//“(\\()? (\\d{3}) (\\))? ([-. ])? (\\d{3}) ([-. ])? (\\d{4}) ”;
由于我们的模式使用了括号,而且必须去除反斜线的特殊含义,因此这个模式很难读写。理解的时候要逐个剥离子表达式。
(\\()? 表示区号部分可选的左括号
(\\d{3}) 表示区号
(\\))? 表示区号部分可选的右括号
([-. ])? 表示区号部分可选的分隔符
(\\d{3}) 表示区号的下三位数字
([-. ])? 表示可选的分隔符
(\\d{4}) 表示号码的最后四位数字
下面的代码读取一个文件,并用此模式查找与完整的号码格式匹配的数据。然后会调用一个vaild函数来检查号码格式是否合法。
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(vaild(*it))
cout<<"vaild: "<<it->str()<<endl;
else
cout<<"not vaild: "<<it->str() <<endl;
}
使用子匹配操作
我们的phone有七个子表达式。与往常一样,每个smatch对象会包含八个ssub_match元素。位置[0]的元素表示整个匹配;[1]-[7]分别表示对应的七个子表达式。
当调用vaild时,我们知道已经有了一个完整的匹配,但是不知道每个可选的子表达式是否是匹配的一部分。如果一个子表达式是完整匹配的一部分,则其对应的ssub_match对象的matched成员为true。
bool vaild(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();
}
8.使用regex_replace
正则表达式不仅用在我们希望查找一个给定序列的时候,还用在当我们想将找到的序列替换为另一个序列的时候。例如,我们可以将美国的电话号码为“ddd.ddd.dddd”形式,即区号和后面三位数字用一个点分隔。
当我们希望在输入序列中查找并替换一个正则表达式时,可以 调用regex_replace。类似搜索函数,它接受一个输入字符序列和一个regex对象,不同的是,它还接受一个描述我们想要的输出形式的字符串。我们用一个符号 $ 后跟子表达式索引号来表示一个特定的子表达式:
string fmt="$2,$5,$7"; //将号码格式改为 ddd.ddd.dddd
可以像下面这样简单的使用:
regex r(phone);
string number="(908) 555-1800";
cout<<regex_replace(number,r,fmt)<<endl;
此程序的输出为: 908.555.1800
使用替换过程中的格式标志
匹配标志
match_not_bol 不将首字符作为行首处理
match_not_eol 不将尾字符作为行尾处理
match_any 如果存在多于一个匹配,则可返回任意一个匹配
format_no_copy 不输出输入序列中未匹配的部分
... ...
简单的使用,如果给定的文本格式为:
morgan (201) 555-2222 293-111-1234
drew (923)222.2345
//只生成电话号码
string fmt="$2,$5,$7 ";
//通知regex_replace只拷贝它替换的文本
cout<< regex_replace(s,r,fmt,format_no_copy)<< endl;
输出结果:
201.555.2222 293.111.1234
923.222.2345