一、正则表达式是什么?
正则表达式是一种描述字符串的一种手段。正则表达式像是一系列字符串特征的描述,比如我们需要在一系列文本中找出[email protected]
的字符串,我们就可以通过描述正则表达式这个特征找出所有的email地址。正则表达式在很多地方都有应用,如文本合法性表达,网络爬虫,Web网表单验证等等。
二、正则表达式库简介
C++11开始支持正则表达式,大大提升了字符串处理能力。头文件定义在<regex>
中,详细的参考手册见这里,下面是对手册的一些简单整理。
2.1 Main classes
类 | 含义 |
---|---|
basic_regex | 正则表达式对象 |
sub_match | 代表由子表达式匹配的字符序列 |
match_results | 代表一个正则表达式匹配,包括所有子表达式匹配 |
我们知道,正则表达式是对一个字符串的描述,假如我们需要匹配多个信息时,显然是一个描述是不够的,需要额外的子表达式。如:zhangsan 10086
我们的目的是要找出两个信息,一个是名字,另一个是电话。
- 正则对象basic_regex
std::basic_regex是构造正则表达式的模板,其定义如下:
template <
class CharT,
class Traits = std::regex_traits<CharT>
> class basic_regex;
可以看出构造一个正则对象只需要提供一个字符类型charT
即可,一些通用的字符类型已经被定义方便使用:
Type | Definition |
---|---|
regex | basic_regex<char> |
wregex | basic_regex<wchar_t> |
那么模板实例化成类后,该如何构造?
basic_regex();
explicit basic_regex( const CharT* s, flag_type f = std::regex_constants::ECMAScript );
basic_regex( const CharT* s, std::size_t count,flag_type f = std::regex_constants::ECMAScript );
basic_regex( const basic_regex& other );
basic_regex( basic_regex&& other );
template< class ST, class SA >
explicit basic_regex( const std::basic_string<CharT,ST,SA>& str,
flag_type f = std::regex_constants::ECMAScript );
template< class ForwardIt >
basic_regex( ForwardIt first, ForwardIt last,
flag_type f = std::regex_constants::ECMAScript );
basic_regex( std::initializer_list<CharT> init,
flag_type f = std::regex_constants::ECMAScript );
默认情况下,构造出来的正则表达式语法是ECMAScript
,构造函数有:
- 默认构造函数;
- C风格字符串;
- 指定长度的C风格字符串;
- 拷贝构造和移动构造函数;
- std::string;
- 范围迭代器构造(满足前向迭代器);
- 初始化列表
表达式属性可以是:
- 正则表达式语法
ECMAScript egrep awk
等 - 表达式行为控制 大小写敏感 是否加速(牺牲构造时间) 是否子匹配 默认范围是全闭
其他方法 | 含义 |
---|---|
r1=re | 替换r1中的正则表达式 |
r1.assign(re,f) | 重新给定正则表达式及选项 |
r.mark_count() | 子表达式数量 |
r.flags() | 返回r的标志集 |
注意:构造函数和赋值操作都会抛出regex_error异常(见表3)。
- std::match_result 用于存储匹配或者搜索结果的容器。容器的元素是子匹配,一般而言,他只能被算法
regex_search
regex_match
或者iterator
所改变,如果成功则返回true且第一个子匹配为整个序列;反之则返回false。
template<
class BidirIt,
class Alloc = std::allocator<std::sub_match<BidirIt>>
> class match_results;
namespace pmr {
template <class BidirIt>
using match_results = std::match_results<BidirIt,
std::pmr::polymorphic_allocator<
std::sub_match<BidirIt>>>;
}
同样有一些方便使用点的类实例:
Type | Definition |
---|---|
std::cmatch | std::match_results<const char*> |
std::wcmatch | std::match_results<const wchar_t*> |
std::smatch | std::match_results<std::string::const_iterator> |
std::wsmatch | std::match_results<std::wstring::const_iterator> |
std::pmr::cmatch (C++17) | std::pmr::match_results<const char*> |
std::pmr::wcmatch (C++17) | std::pmr::match_results<const wchar_t*> |
std::pmr::smatch(C++17) | std::pmr::match_results<std::string::const_iterator> |
std::pmr::wsmatch (C++17) | std::pmr::match_results<std::wstring::const_iterator> |
具体地,smatch的操作及含义如下:
smatch操作 | 含义 |
---|---|
m.ready() | 是否通过regex_match或regex_search设置m,如果没设置,m操作未定义 |
m.size() | 匹配失败返回0,否则返回最近一次匹配的正则表达式中子表达式的数目 |
m.empty() | m.size()=0返回true;反之false |
m.prefix() | 一个ssub_match对象表示匹配之前的序列 |
m.suffix() | 一个ssub_match对象匹配之后的序列 |
m.format(…) | 正则表达式替换操作 |
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元素范围迭代器 |
注:在接受一个引索操作中,n的默认值位0且必须小用户m.size()。第一个子匹配(引索为0)表示整个匹配。特别提一下,suffix是匹配到的元素之后的序列,一般用在对表达式重复搜寻上!
while(std::regex_search(s,sm,re2))
{
for(auto x:sm)
{
std::cout<<x<<std::endl;
}
s=sm.suffix().str();
}
- sub_match结果中的子匹配存储类
std::sub_match 子匹配(搜寻)结果存
template<class BidirIt>
class sub_match;
常见的实例类如下:
Type | Definition |
---|---|
csub_match | sub_match<const char*> |
wcsub_match | sub_match<const wchar_t*> |
ssub_match | sub_match<std::string::const_iterator> |
wssub_match | sub_match<std::wstring::const_iterator> |
它是std::pair<BidirIt,BidirIt>
的派生类,第一个frist成员代表匹配序列的起点,第二个成员second代表匹配的尾后迭代器。你可以使用中括号或者str()来将其转换成字符串。
2.2 Algorithm
有了正则表达式和表示结果的容器后,我们就可以使用正则表达式算法完成字符串截取、替换等操作。
- regex_match 在整个字符序列上引用正则匹配
regex_match的参数比较多样,本质上是对不同字符串风格输入的进行兼容。参数可以表示为:字符串范围+匹配结果容器+正则表达式+额外选项,具体的:
参数 | 含义 |
---|---|
(seq,m,r,mft) (seq,r,mft) | seq是字符串表达,string、字符数组、指针,迭代器字符串范围 m是一个match对象,用来保存匹配结果相关细节,输入序列seq和m (match对象)必须匹配 mft是一个可选的regex_constants::match_flag_type值(见表2) |
-
regex_search 在字符序列的每个部分上应用正则匹配
需要注意与match之间区别,match是要整个序列都与正则表达式对应才返回true;而search则是在序列中找一个能匹配上的就会返回true。 -
regex_replace 在字符序列中替换指定满足正则的字符串
参数上和match和search的区别在于,前面传入一个输出迭代器,正则对象参数后面多了一个取代的字符串。
2.3 Iterator
正则迭代器用于遍历序列中找到的所有正则表达式匹配集合。
- std::regex_iterator 遍历字符序列中的所有正则表达式匹配
template<
class BidirIt,
class CharT = typename std::iterator_traits<BidirIt>::value_type,
class Traits = std::regex_traits<CharT>
> class regex_iterator
大多数情况下我们只需要使用别名就行了:
Type | Definition |
---|---|
cregex_iterator | regex_iterator<const char*> |
wcregex_iterator | regex_iterator<const wchar_t*> |
sregex_iterator | regex_iterator<std::string::const_iterator> |
wsregex_iterator | regex_iterator<std::wstring::const_iterator> |
std::regex_iterator是一个只读迭代器,在以下两情况将会调用std::regex_search:
- 构造时
- 自增时
迭代器默认构造是一个尾后迭代器,当匹配接近终点时,迭代器将会指向尾后迭代器。
对regex_iterator解引用的结果是得到当前最近的std::match结果。
如果你想知道全部匹配数量可以使用以下语句得到:
std::distance(words_begin, words_end);
- std::regex_token_iterator 遍历给定字符串中所有正则表达式匹配项中的指定子表达式,或遍历不匹配子字符串中的指定子表达式
感觉这个只是在std::regex上的封装,token意味着可能适合对符号的分割。
template<
class BidirIt,
class CharT = typename std::iterator_traits<BidirIt>::value_type,
class Traits = std::regex_traits<CharT>
> class regex_token_iterator
同样的,为了方便使用有:
Type | Definition |
---|---|
cregex_token_iterator | regex_token_iterator<const char*> |
wcregex_token_iterator | regex_token_iterator<const wchar_t*> |
sregex_token_iterator | regex_token_iterator<std::string::const_iterator> |
wsregex_token_iterator | regex_token_iterator<std::wstring::const_iterator> |
实例化的类的构造参数为:
a - 首迭代器
b - 尾迭代器
re - 正则对象
submatch - 应该被返回的子匹配。0代表整个匹配,-1代表没有匹配到的序列(如,匹配之间的东西)
submatches - 每次迭代器结果都会存储于此
m - 管理re行为标志
2.5 ECMAScript特殊模式字符
在正则表达式表达过程中,特殊模式字符可以表达某些有特殊含义序列或者难以表达的字符而出现。对于一个序列,只要一个元素匹配就是匹配。
构造正则表达式可能需要特殊模式字符。他可以帮助我们匹配一类有特殊含义的字符集合,如数字、非数字,
characters | description | matches |
---|---|---|
. | not newline | any character except line terminators (LF, CR, LS, PS). |
\t | tab (HT) | a horizontal tab character (same as \u0009 ). |
\n | newline (LF) | a newline (line feed) character (same as \u000A ). |
\v | vertical tab (VT) | a vertical tab character (same as \u000B ). |
\f | form feed (FF) | a form feed character (same as \u000C ). |
\r | carriage return (CR) | a carriage return character (same as \u000D). |
\cletter | control code | a control code character whose code unit value is the same as the remainder of dividing the code unit value of letter by 32.For example: \ca is the same as \u0001 , \cb the same as \u0002 , and so on… |
\xhh | ASCII character | a character whose code unit value has an hex value equivalent to the two hex digits hh.For example: \x4c is the same as L, or\x23 the same as #. |
\uhhhh | unicode character | a character whose code unit value has an hex value equivalent to the four hex digits hhhh. |
\0 | null | a null character (same as \u0000 ). |
\int | backreference | the result of the submatch whose opening parenthesis is the int-th (int shall begin by a digit other than 0). See groups below for more info. |
\d | digit | a decimal digit character (same as [[:digit:]] ). |
\D | not digit | any character that is not a decimal digit character (same as [^[:digit:]] ). |
\s | whitespace | a whitespace character (same as [[:space:]] ). |
\S | not whitespace | any character that is not a whitespace character (same as[^[:space:]]) . |
\w | word | an alphanumeric or underscore character (same as [_[:alnum:]] ). |
\W | not word | any character that is not an alphanumeric or underscore character (same as[^_[:alnum:]] ). |
\character | character | the character character as it is, without interpreting its special meaning within a regex expression. Any character can be escaped except those which form any of the special character sequences above.Needed for: ^ $ \ . * + ? ( ) [ ] { } |
[class] | 字符类 | the target character is part of the class,class,见引用2 |
[^class] | 非字符类 | the target character is not part of the class |
在C++里边我们不能直接输出一个反斜杠:
std::cout<<"\"<<std::endl;//error,反斜杠在C++表示后面的字符需要转义,而不是反斜杠字符本身
为了输出反斜杠,我们使用对/
使用转义,还原本身:
std::cout<<"\\"<<std::endl;
注:ECMAScript同样也是用反斜杠进行转义,结合上面所说的,如果你要在模式中提供一个反斜杠,你需要额外提供一个反斜杠,确保在C++语境中确实给出了一个反斜杠。如,小括号是有特殊含义的(子表达式),当我们真正想表达一个小括号时可以借助反斜杠\(
,可是反斜杠已经是C++的特殊字符(\n)用于转义,所以在正则表达式中,模式中出现\
的地方,我们都需要用一个额外的反斜杠来告知C++我们需要一个反斜杠字符,而不是一个特殊字符。举个例子,\\{d}
表达数字。
斜杠为啥是
/
,反斜杠为什么叫做\
。凭什么/
是斜杠,\
叫反斜杠,而不是反过来,肯定是规定啊!不过中文叫法很难联想到其形状,英文上可以在名字上体现forward slash
,/
向前的杠,其实你可以想象成一个人在走在一条从左往右的路上,那个人往前面倒,他的侧面看上去就是/
,所以他叫做forward slash
;如果他是往后倒的,那在侧面就是这个\
样子的。
上面形状就是反斜杠\
。为什么Windows路径名要用反斜杠\
分割路径名,而不是和Linux一样用/
斜杠?历史原因,Windows Dos斜杠/
用来表示命令行参数,已经被占用,因此找了个最近的反斜杠\
。C++的转义字符、注释也是反斜杠\
,这是编程上的事情了。
三、正则表达式实践!
3.1 检查邮箱地址是否正确
[email protected]
122355.com
[email protected]
std::string email("[email protected]");
std::regex re("[[:alnum:]]+@[[:alnum:]]+\\.com");
std::cout << std::boolalpha << std::regex_match(email, re) << std::endl;
3.2 找到第一个浮点数
12.3,44.5,1.2,4.55,90.0,0.1
std::string points("12.3,44.5,1.2,4.55,90.0,0.1");
std::regex re("[[:digit:]]+\\.[[:digit:]]+");
std::smatch sm;
if (regex_search(points, sm, re))
{
std::cout << sm.str() << std::endl;
}
3.3 分隔所有逗号分隔的浮点数
std::string points("0.12,44.5,1.2,4.55,90.0,0.1");
string str("([[:digit:]]+\\.*[[:digit:]]+)");
string pattern = str + ',' + str + ',' + str + ',' + str + ',' + str + ',' + str;
std::regex re(pattern);
std::smatch sm;
if (regex_search(points, sm, re))
{
for (auto it = sm.begin() + 1; it != sm.end(); it++)
{
std::cout << *it << std::endl;
}
}
else{ }
3.4 将所有的逗号替换成加号
std::string str{"3.3,4.0,0.5,0.01,90.0"};
std::regex re(",");
std::regex_replace(std::ostream_iterator<char>(std::cout),str.begin(),str.end(),re ,"+");
3.5 迭代器逗号分割
// Tokenization (non-matched fragments)
// Note that regex is matched only two times; when the third value is obtained
// the iterator is a suffix iterator.
const std::string text = "1.2,4.2,4,5,6,8";
const std::regex ws_re(","); // whitespace
std::copy( std::sregex_token_iterator(text.begin(), text.end(), ws_re, -1),
std::sregex_token_iterator(),
std::ostream_iterator<std::string>(std::cout, "\n"));
std::cout << '\n';
// // Iterating the first submatches
const std::string html = R"(<p><a href="http://google.com">google</a> )"
R"(< a HREF ="http://cppreference.com">cppreference</a>\n</p>)";
const std::regex url_re(R"!!(<\s*A\s+[^>]*href\s*=\s*"([^"]*)")!!", std::regex::icase);
std::copy( std::sregex_token_iterator(html.begin(), html.end(), url_re, 1),
std::sregex_token_iterator(),
std::ostream_iterator<std::string>(std::cout, "\n"));
这个例子是来自cppref的,里面用到了std::copy+输出迭代器打印,除此,为了避免重复输入转义字符,使用了R"(字符串)"去掉所有的转义保留原始字符串。
表一:
regex定义时指定的标志 | 含义 |
---|---|
icase | 匹配过程忽略大小写 |
nosubs | 不匹配子表达式 |
optimize | 执行速度优于构造速度 |
ECMAScript | 使用ECMA-262指定语法 |
basic | 使用POSIX基本的正则表达式语法 |
extended | 使用POSIX扩展正则表达式语法 |
awk | 使用POSIX版本的awk语法 |
grep | 使用POSIX的grep语法 |
egrep | 使用POSIX的egrep语法 |
这些标志定义在regex或regex_constants::syntax_option_type,这是在构造regex的选项。
表二:
regex_match、regex_search标志 | 含义 |
---|---|
match_default | 等价于format_default |
match_not_bol | 不将首字符作为行首处理 |
match_not_eol | 不将尾字符作为行尾处理 |
match_not_bow | 不将首字符作为单词首处理 |
match_not_eow | 不将尾字符作为单词尾处理 |
match_any | 如果有多于一个匹配,则任意返回一个匹配 |
match_not_null | 不匹配任何空序列 |
match_continueous | 匹配必须从输入的首字符开始 |
match_pre_avail | 输入序列包含第一个匹配前的内容 |
format_default | 使用ECMAScript规则替换字符串 |
format_sed | 使用POSIX sed规则替换字符串 |
format_not_copy | 不输出序列中未匹配部分 |
format_first_only | 只替换子表达式第一次出现 |
这些标志定义在regex_constants::match_flag_type。using namespace std::regex_constants
表三:
错误 | 含义 |
---|---|
error_collate | 无效的元素校对请求 |
error_ctype | 无效的字符类 |
error_escape | 无效的转义字符或无效的尾置转义 |
error_backref | 无效的向后引用 |
error_brack | 不匹配的方括号 |
error_paren | 不匹配的小括号 |
error_brace | 不匹配的花括号 |
error_range | 无效的字符范围[z-a] |
error_space | 内存不足,无法处理此正则表达式 |
error_badrepeat | 重复字符(* ? +或{)之前没有有效的正则表达式 |
error_complexity | 要求匹配过于复杂 |
error_stack | 栈空间不足,无法处理匹配 |
这些错误定义在regex和regex_constants::error_type
[1] https://www.ecma-international.org/publications-and-standards/standards/ecma-262/
[2] https://www.cplusplus.com/reference/regex/ECMAScript/#character_classes
[3] https://www.cplusplus.com/reference/regex/ECMAScript/