C++: 浅谈正则表达式

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/tonglin12138/article/details/91591286

正则表达式(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

猜你喜欢

转载自blog.csdn.net/tonglin12138/article/details/91591286