Bootstrap

标准库(三)正则表达式

一、正则表达式是什么?

正则表达式是一种描述字符串的一种手段。正则表达式像是一系列字符串特征的描述,比如我们需要在一系列文本中找出[email protected]的字符串,我们就可以通过描述正则表达式这个特征找出所有的email地址。正则表达式在很多地方都有应用,如文本合法性表达,网络爬虫,Web网表单验证等等。

二、正则表达式库简介

C++11开始支持正则表达式,大大提升了字符串处理能力。头文件定义在<regex>中,详细的参考手册见这里,下面是对手册的一些简单整理。

正则表达式组件
Main classes
basic_regex
sub_match
match_results
Algorithm
regex_match
regex_search
regex_replace
iterators
regex_iterator
regex_token_iterator
Exceptions
regex_error
Traits
regex_traits
Constants
synatax_option_type
match_flag_type
error_type
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即可,一些通用的字符类型已经被定义方便使用:

TypeDefinition
regexbasic_regex<char>
wregexbasic_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>>>;
}

同样有一些方便使用点的类实例:

TypeDefinition
std::cmatchstd::match_results<const char*>
std::wcmatchstd::match_results<const wchar_t*>
std::smatchstd::match_results<std::string::const_iterator>
std::wsmatchstd::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;

常见的实例类如下:

TypeDefinition
csub_matchsub_match<const char*>
wcsub_matchsub_match<const wchar_t*>
ssub_matchsub_match<std::string::const_iterator>
wssub_matchsub_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

大多数情况下我们只需要使用别名就行了:

TypeDefinition
cregex_iteratorregex_iterator<const char*>
wcregex_iteratorregex_iterator<const wchar_t*>
sregex_iteratorregex_iterator<std::string::const_iterator>
wsregex_iteratorregex_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

同样的,为了方便使用有:

TypeDefinition
cregex_token_iteratorregex_token_iterator<const char*>
wcregex_token_iteratorregex_token_iterator<const wchar_t*>
sregex_token_iteratorregex_token_iterator<std::string::const_iterator>
wsregex_token_iteratorregex_token_iterator<std::wstring::const_iterator>

实例化的类的构造参数为:

a - 首迭代器
b - 尾迭代器
re - 正则对象
submatch - 应该被返回的子匹配。0代表整个匹配,-1代表没有匹配到的序列(如,匹配之间的东西)
submatches - 每次迭代器结果都会存储于此
m - 管理re行为标志

2.5 ECMAScript特殊模式字符

在正则表达式表达过程中,特殊模式字符可以表达某些有特殊含义序列或者难以表达的字符而出现。对于一个序列,只要一个元素匹配就是匹配。

构造正则表达式可能需要特殊模式字符。他可以帮助我们匹配一类有特殊含义的字符集合,如数字、非数字,

charactersdescriptionmatches
.not newlineany character except line terminators (LF, CR, LS, PS).
\ttab (HT)a horizontal tab character (same as \u0009).
\nnewline (LF)a newline (line feed) character (same as \u000A).
\vvertical tab (VT)a vertical tab character (same as \u000B).
\fform feed (FF)a form feed character (same as \u000C).
\rcarriage return (CR)a carriage return character (same as \u000D).
\clettercontrol codea 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…
\xhhASCII charactera 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 #.
\uhhhhunicode charactera character whose code unit value has an hex value equivalent to the four hex digits hhhh.
\0nulla null character (same as \u0000).
\intbackreferencethe 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.
\ddigita decimal digit character (same as [[:digit:]]).
\Dnot digitany character that is not a decimal digit character (same as [^[:digit:]]).
\swhitespacea whitespace character (same as [[:space:]]).
\Snot whitespaceany character that is not a whitespace character (same as[^[:space:]]).
\wwordan alphanumeric or underscore character (same as [_[:alnum:]]).
\Wnot wordany character that is not an alphanumeric or underscore character (same as[^_[:alnum:]]).
\charactercharacterthe 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;如果他是往后倒的,那在侧面就是这个\样子的。
1
上面形状就是反斜杠\。为什么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/

;