测试环境:Windows+VC2022
总结:
只需掌握3个命令就足以使用绝大部分正则表达式的功能了,分别是:
regex //正则表达式的模式语法
regex_search //匹配搜索函数
match //结果存储对象
如果要使用替换功能则还有
regex_replace //替换函数
一.字符串
除了使用传统的双引号界定的字符串,C++11引入了新的文本写法(Raw String):
R"(字符串,字符串,字符串)"
完整语法如下:
R"delim(.....)delim"
这里的delim最多可以16个字符,基本符合c++的变量名规则。
示列:
R"abc(c:\windows\system32)abc"
R"_abc(c:\windows\syst)em32)_abc"
R"_(c:\windows\)_"
使用长定界符的好处是可以让字符串里可以包含 )" ,如下:
s = R"_x(C:\Program Files (x86)\Microsoft V R"_x(isu)_y"al Studio 14.0\)"ht\v3.0)_x";
如果此时的字符串中需要包含)_x"怎么办?那就换一个定界符,如:R"_z(...)_z"。因为这里本就是码农人工输入而不是运行时生成,所以足以应对各种情况。
严格来说这不属于正则表达式的内容,但它在正则表达式中应该是比较实用的,否则双层转义能把人搞晕。
二.regex
1.regex用于定义正则表达式的模式(Pattern)
2.std::regex是模板类class std::basic_regex<>的特化,char对应regex,wchar_t对应wregex
using regex = basic_regex<char>;
using wregex = basic_regex<wchar_t>;
3.示例:
a) regex reg1("<.*>.*</.*>");
b) regex reg2("<(.*)>.*</\\1>"); //使用子匹配,由圆括号界定。
\1表示要求匹配与1号子匹配一样的字符串,如果指定一个表达式中不存在的子匹配如\2则在构造regex时(而不是regex_match()时)会抛出一个错误regex_error,需要使用try...catch捕捉异常,如下:
try
{
bool found = regex_match("<tag>value</tag>", regex("<(.*)>.*</\\2>"));
}
catch (regex_error& re)
{
int e = re.code();
const char* info = re.what();
}
c) regex reg3("<\\(.*\\)>.*</\\1>", regex_constants::grep); //指定模式的文法。
默认文法是ECMAScript(欧洲计算机制造商协会,ECMA- 262脚本语言的规范:规范化脚本语言,叫ECMAScript),另外还有:basic、extended、awk、grep、egrep
ECMAScript是目前威力最强大的文法。它少有不支持的性质是,像grep和egrep那样使用newline字符搭配"or"分隔多个pattern,以及像awk那样指明八进制转义序列
d) regex reg4(R_"(<((.*)(.*))>(.*)(</\1>))_") //子匹配嵌套。
虽然文法里看到是嵌套,但是匹配结果的存储并不是层次结构,存储大致是以(外-->内,左-->右)顺序排列成数组
三.regex_match()与regex_search()以及match
regex_match() 检验是否整个字符序列匹配某个正则表达式
regex_search() 检验是否部分字符序列匹配某个正则表达式
除此之外别无不同。因此
regex_search(data,regex(pattern))
等价于
regex_match(data,regex("(.|\n)*"+pattern+"(.|\n)*"))
如果没有match对象参与,则上面两个函数只能相当于判断函数,因为它们不能告诉你到底匹配的字符串出现在哪个位置以及匹配到多少。所以需要如下方法:
cmatch m;
const char* data = "XML tag: <tag-name>the value</tag-name>.";
bool found = regex_search(data, m, regex(R"(<((.*)(.*))>(.*)(</\1>))"));
忽略大小写:
cmatch m;
regex reg(R"(<(.*)>.*</(\1)>)", regex_constants::ECMAScript | regex_constants::icase); //实际测试发现ECMAScript不写也可以,但猜测其它文法应该不能省略
bool found = regex_match("<tAg>value</tag>", m, reg);
match对象是一个模板类match_results<>,定义在头文件<regex>里,如下:
using cmatch = match_results<const char*>;
using wcmatch = match_results<const wchar_t*>;
using smatch = match_results<string::const_iterator>;
using wsmatch = match_results<wstring::const_iterator>;
成功匹配之后如何从match对象中取出结果:
void Test()
{
const char* data = "XML tag: <tag-name>the value</tag-name>.";
cmatch m; // for returned details of the match
bool found = regex_search(data, m, regex(R"(<((.*)(.*))>(.*)(</\1>))"));
Print("m.empty(): %d\n", m.empty());
Print("m.size(): %d\n", m.size());
if (found)
{
Print("m.str(): %s\n", m.str().c_str());
Print("m.length(): %d\n", m.length());
Print("m.position(): %d\n", m.position());
Print("m.prefix().str(): %s\n", m.prefix().str().c_str()); //前缀(匹配的前面)
Print("m.suffix().str(): %s\n", m.suffix().str().c_str()); //后缀(匹配的后面)
Print("\n");
//迭代所有匹配(使用匹配索引)
Print("迭代所有匹配(使用匹配索引):\n");
for (int i = 0; i < m.size(); ++i)
{
Print("%d:\n", i);
//Print("m[%d].position(); %d\n", i, m[i].position()); //错误
Print("m.position(%d); %d\n", i, m.position(i));
Print("m[%d].length(); %d\n", i, m[i].length());
Print("m.length(%d); %d\n", i, m.length(i));
Print("m[%d].str(); %s\n", i, m[i].str().c_str());
Print("m.str(%d); %s\n", i, m.str(i).c_str());
//非标准的访问方法:
char buf[100];
strncpy_s(buf, m[i].first, m[i].second - m[i].first); //m[i].first、m[i].second分别是子匹配的开始与结束指针,全部指向data的内部
CS.Print("buf: %s\n", buf);
}
Print("\n");
Print("迭代所有匹配(使用迭代器):\n");
//迭代所有匹配项(使用迭代器)
for (auto pos = m.begin(); pos != m.end(); ++pos)
{
Print("%s ", (*pos).str().c_str());
Print("(length: %d)\n", pos->length());
}
Print("===============================\n\n");
}
}
上面的Print()函数是自定义函数,用途与参数都兼容printf()
对于单个匹配以上代码差不多已经够了,但是很多时候我们要求匹配文本中的所有符合要求的子文本,可以使用如下方式:
void Test()
{
const char* data =
"<person>\n"
" <first>Nico</first>\n"
" <last>Josuttis</last>\n"
"</person>\n";
regex reg("<(.*)>(.*)</(\\1)>");
auto pos = data; //对于std::string文本,这里使用: pos = data.cbegin();
auto end = data + ::strlen(data); //对于std::string文本,这里使用: pos = data.cend();
cmatch m;
for (; regex_search(pos, end, m, reg); pos = m.suffix().first)
{
Print("match: %s\n", m.str().c_str());
Print("tag: %s\n", m.str(1).c_str());
Print("value: %s\n", m.str(2).c_str());
}
}
也可以使用如下方式(利用regex_iterator):
void Test()
{
const char* data =
"<person>\n"
" <first>Nico</first>\n"
" <last>Josuttis</last>\n"
"</person>\n";
regex reg("<(.*)>(.*)</(\\1)>");
cregex_iterator pos(data, data + ::strlen(data), reg);
cregex_iterator bg = pos;
cregex_iterator end;
for (; pos != end; pos++) //没有pos--和--pos
{
Print("match: %s\n", pos->str().c_str());
Print("tag: %s\n", pos->str(1).c_str());
Print("value: %s\n", pos->str(2).c_str());
Print("tag: %s\n", pos->str(3).c_str());
}
cregex_iterator beg(data, data + ::strlen(data), reg);
for_each(beg, end, [](const cmatch& m)
{
Print("match: %s\n", m.str().c_str());
Print("tag: %s\n", m.str(1).c_str());
Print("value: %s\n", m.str(2).c_str());
Print("tag: %s\n", m.str(3).c_str());
});
}
个人更喜欢第1种方式,如果要自己封装一个Regexp类最好选择第1种方式,但如果直接用于写代码似乎第2中方式更简洁明了
四.regex_iterator、regex_token_iterator
regex_iterator的定义:
using cregex_iterator = regex_iterator<const char*>;
using wcregex_iterator = regex_iterator<const wchar_t*>;
using sregex_iterator = regex_iterator<string::const_iterator>;
using wsregex_iterator = regex_iterator<wstring::const_iterator>;
regex_iterator使用方法就是(三)里的例子,这里主要关于regex_token_iterator。
regex_token_iterator的定义:
using cregex_token_iterator = regex_token_iterator<const char*>;
using wcregex_token_iterator = regex_token_iterator<const wchar_t*>;
using sregex_token_iterator = regex_token_iterator<string::const_iterator>;
using wsregex_token_iterator = regex_token_iterator<wstring::const_iterator>;
regex_token_iterator的用途:
有时候我们只对匹配中的某个或几个子匹配感兴趣,甚至不对任何匹配感兴趣而只想要匹配之外的文本(就是将匹配当成文本分隔符),此时最好使用regex_token_iterator来实现,而不是自己写大量代码来甄别匹配内容。
示例:
void Test()
{
const char* data =
"<person>\n"
" <first>Nico</first>\n"
" <last>Josuttis</last>\n"
"</person>\n";
regex reg("<(.*)>(.*)</(\\1)>");
auto xx = { 0, 2 }; //xx类型为std::initializer_list<int>
cregex_token_iterator pos(data, data + strlen(data), reg, { 0,2 }); //只保留整个匹配和它的第2个子匹配。所有匹配与子匹配将会全部混在一个(等价于)一维数组里,不区分层次,但是某个匹配还是会与它的子匹配紧挨着,通过pos++可以遍历它们。
cregex_token_iterator end;
char buf[100]; strncpy_s(buf, pos->first, pos->second - pos->first); //这种方式与使用pos->str()得到的结果一模一样,书上用的是pos->str()
Print("buf: %s\n", buf);
for (; pos != end; ++pos)
{
Print("match: %s\n", pos->str().c_str());
}
Print("\n\n");
//下面演示将匹配作为分割符的用法
const char* names = "nico ,\n jim\n , helmut\n,paul,tim,john paul.rita";
regex sep(R"_([ \t\n]*[,;.][ \t\n]*)_");
cregex_token_iterator p(names, names + strlen(names), sep, -1); //最后这个参数-1也可以使用前面的那种写法:{-1}
cregex_token_iterator e;
for (; p != e; ++p)
{
Print("name: %s\n", p->str().c_str());
}
}
代码输出如下:
buf: <first>Nico</first>
match: <first>Nico</first>
match: Nico
match: <last>Josuttis</last>
match: Josuttis
name: nico
name: jim
name: helmut
name: paul
name: tim
name: john paul
name: rita
五.替换regex_replace
示例:
void Test() //测试wchar_t字符串
{
const wchar_t* data =
L"<person>\n"
L" <first>Nico</first>\n"
L" <last>Josuttis</last>\n"
L"</person>\n";
wregex reg(L"<(.*)>(.*)</(\\1)>");
auto uu = regex_replace(data, reg, L""); //返回值类型是std::wstring,它是新创建的字符串,所以这个函数不会影响原来的字符串
auto vv = regex_replace(data, reg, L"<$1 value=\"$2\"/>");
auto ww = regex_replace(data, reg, LR"_s(<\1 value="\2"/>)_s", regex_constants::format_sed);
wstring res2;
auto xx = regex_replace(back_inserter(res2),
data, data + wcslen(data),
reg, L"<$1 value=\"$2\"/>",
regex_constants::format_no_copy | regex_constants::format_first_only
); //返回值类型是back_insert_iterator<std::wstring>,而真正的替换结果存储在res2中
CS.Print(L"%s-----\n", uu.c_str());
CS.Print(L"%s-----\n", vv.c_str());
CS.Print(L"%s-----\n", ww.c_str());
CS.Print(L"%s\n", res2.c_str());
}
代码输出结果为:
<person>
</person>
-----
<person>
<first value="Nico"/>
<last value="Josuttis"/>
</person>
-----
<person>
<first value="Nico"/>
<last value="Josuttis"/>
</person>
-----
<first value="Nico"/>
sed(中文意为流编辑器,源自“stream editor”的缩写)是 Linux/Unix 下的常见命令行程序。 sed 用来把文档或字符串中的文字经过一系列编辑命令转换为另一种格式输出。 sed 常用来匹配一个或多个正则表达式的文本。 分号(;)可用于分隔命令的指示符
字符$表示“匹配之次表达式”(matched subexpression)(见下表第1列)。这里我们使用$1和$2代表替换过程中找到的tag和value:
"<$1 value=\"$2\"/>"
如果给regex_replace()传入一个常量regex_constants::format_sed,就可以改用UNIX command sed的语法(见下表第2列)
表:(Regex用于替换的符号)
默认的pattern | Unix Sed pattern | 意义 |
$& | & | Matched pattern |
$n | \n | 第n个matched capture group |
$` | Matched pattern 的前缀 | |
$’ | Matched pattern 的后缀 | |
$$ | 字符$ |
以上对《C++标准库第2版》这本书关于正则表达式部分的初步学习笔记,测试代码主体来自于它,因为测试环境不是控制台程序所以输出函数采用了自定义函数Print()