购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.9 使用正则表达式验证字符串的格式

正则表达式是一种用于在文本中执行模式匹配和替换的语言,C++11通过头文件<regex>中提供的一组类、算法和迭代器来支持标准库中的正则表达式。在本节中,我们将学习如何使用正则表达式来验证字符串是否匹配某个模式(例如验证电子邮件或IP地址格式)。

2.9.1 准备工作

在本节中,我们将在必要时解释所使用的正则表达式的细节。然而,为了使用C++标准库来处理正则表达式,你至少应该了解正则表达式的一些基础知识。正则表达式语法和标准的详细描述超出了本书的范围,如果你不熟悉正则表达式,那么建议你先了解更多有关正则表达式的内容。同样,你可以在https://regexr.com和https://regex101.com上找到学习、构建和调试正则表达式的优质在线资源。

2.9.2 使用方式

为了验证字符串是否匹配正则表达式,请执行以下操作:

❍ 包含头文件<regex>和<string>以及命名空间std::string_literals(C++14标准字符串可以使用的用户自定义字面量):

❍ 使用原始字符串字面量来指定正则表达式,以避免转义反斜杠(因为这可能时常发生)。以下正则表达式可验证大多数电子邮件格式:

❍ 创建std::regex或std::wregex对象(具体取决于所使用的字符集)来封装正则表达式:

❍ 若要忽略大小写或指定其他解析选项,请使用具有用于正则表达式标志的额外参数的重载构造函数:

❍ 使用std::regex_match()将正则表达式与整个字符串匹配:

2.9.3 工作原理

考虑到验证电子邮件地址格式的问题,尽管这可能看起来是一个微不足道的问题,但在实践中,很难找到一个简单的正则表达式来覆盖所有可能的有效电子邮件格式。在本节中,我们不会尝试找到最优正则表达式,而是要找一个适用于大多数情况的正则表达式。为此,我们将使用以下正则表达式:

表2.2解释了正则表达式的结构。

表2.2

需要记住的是,在实践中,域名是由主机名和DNS标签的点分隔列表组成的,例如localhost、gmail.com和yahoo.co.uk。因此,我们使用的正则表达式不匹配没有DNS标签的域,比如localhost(比如root@localhost就是有效的电子邮件地址)。同时,域名也可以是括号中指定的IP地址,如[192.168.100.11](如在john.doe @[192.168.100.11]中)。包含这些域名的电子邮件地址将无法匹配前面定义的正则表达式。尽管这些相当罕见的格式不会被匹配,但正则表达式可以覆盖大多数电子邮件格式。

本章示例中的正则表达式仅用于教学目的,不建议像在生产代码中那样使用。如前所述,此示例并未涵盖所有可能的电子邮件格式。

我们首先要包含必要的头文件,也就是<regex>(用于正则表达式)和<string>(用于字符串)。如下面的代码(其中基本上包含了2.9.2节中的例子)所示,is_valid_email()函数接受一个表示电子邮件地址的字符串,并返回一个布尔值,从而判断电子邮件是否具有有效的格式。

首先我们需要创建std::regex对象来封装用原始字符串字面量表示的正则表达式。使用原始字符串字面量很有帮助,因为反斜杠在正则表达式中也用于转义字符,而它巧妙地避免了对反斜杠的转义。然后,让该函数调用std::regex_match(),给它传入文本和正则表达式参数:

std::regex_match()方法尝试将正则表达式与整个字符串匹配,如果匹配成功,则返回true;否则,返回false:

在这个简单的测试中,唯一不匹配正则表达式的电子邮件地址只有ROOT@LOCALHOST和john.doe@domain.com。其中第一个没有包含以点为前缀的DNS标签的域名,这种情况不在正则表达式的覆盖范围内;第二个只包含小写字母,并且在正则表达式中,本地部分和域名的有效字符集都是大写字母(A~Z)。

我们可以指定匹配时忽略这种情况,而不是使用额外的有效字符(例如[A-Za-z0-9._%+-])使正则表达式复杂化。这可以通过在std::basic_regex类的构造函数中添加一个附加参数来实现。用于此目的的可用常量在regex_constants命名空间中定义。以下对is_valid_email_format()的细微更改将使其忽略大小写,并允许同时包含小写字母和大写字母的电子邮件地址正确匹配正则表达式:。

这个is_valid_email_format()函数非常简单,如果正则表达式作为参数提供,并且提供了要匹配的文本,那么它可以用于匹配任何内容。但是,如果能够用一个函数同时处理多字节字符串(std::string)和宽字符串(std::wstring),那就太好了。这可以通过创建一个函数模板来实现,其中字符类型作为模板参数:

我们首先为std::basic_string创建一个模板别名以便简化它的用法。新的is_valid_format()函数是一个函数模板,它非常类似我们实现的is_valid_email()。但是,我们现在使用std::basic_regex<CharT>,而不是使用typedef std::regex(它是std::basic_regex<char>),并且将pattern作为第一个参数。现在,我们依赖这个函数模板实现了名为is_valid_email_format_w()的函数,这个函数适用于宽字符串。但是,函数模板可以在实现其他验证(例如车牌号是否有特定的格式)时重用:

正如我们所料,在这里显示的所有示例中,唯一不匹配的是ROOT@LOCALHOST。

实际上,std::regex_match()方法有几个重载版本,其中一些重载版本有一个参数,该参数是对std::match_results对象(用于存储匹配的结果)的引用。如果没有匹配项,则std::match_results为空,其大小为0;相反,如果存在匹配项,则std::match_results对象不为空,其大小为1加上所匹配的子表达式的数量。

该函数的以下版本使用了前面提到的重载版本,并在std::smatch对象中返回匹配的子表达式。注意,下面这个正则表达式与之前的不一样,它由3子表达式构成:一个用于本地部分,一个用于域的主机名部分,一个用于DNS标签。如果匹配成功,则std::smatch对象将包含4个子匹配对象:第一个匹配整个字符串,第二个用于匹配第一个子表达式(本地部分),第三个用于匹配第二个子表达式(主机名),第四个用于匹配第三个子表达式(DNS标签)。结果以元组的形式返回,其中元组的第一项实际上表示匹配成功或失败:

按照前面的代码,我们使用C++17的结构化绑定将元组的内容解包到命名变量中:

程序的输出如图2.3所示。

图2.3 程序的输出

2.9.4 更多

正则表达式有多个版本,C++标准库支持其中的6个版本:ECMAScript、基本POSIX、扩展POSIX、awk、grep和egrep(带有选项-E的grep)。使用的默认语法是ECMAScript,要使用另一种语法,必须在定义正则表达式时显式指定语法。除了指定语法外,还可以指定解析选项,例如忽略大小写进行匹配。

标准库提供了更多的类和算法,库中可用的主要类(这些类都属于类模板,为了方便起见,我们为不同的字符类型提供了typedef)如下:

❍ 类模板std::basic_regex定义了正则表达式对象:

❍ 类模板std::sub_match表示与子表达式匹配的字符序列,这个类实际上是从std::pair派生来的,它的first和second成员分别表示指向匹配序列中第一个字符和匹配序列字符后一个字符的迭代器。如果没有匹配序列,则两个迭代器相等:

❍ 类模板std::match_results是匹配项的集合,第一个元素在目标中始终是完全匹配的,其他元素是子表达式的匹配项:

正则表达式标准库中可用的算法如下:

❍ std::regex_match():它尝试将正则表达式(由std::basic_regex实例表示)与整个字符串匹配。

❍ std:: regex_search():它尝试将正则表达式(由std::basic_regex实例表示)与字符串的一部分(包括整个字符串)匹配。

❍ std:: regex_replace():这将根据指定的格式替换正则表达式中的匹配项。

正则表达式标准库中可用的迭代器如下:

❍ std:: regex_interator:一种常量前向迭代器,用于遍历字符串中模式的出现次数。这种迭代器有一个指向std::basic_regex的指针,除非迭代器被销毁,否则该指针一直存在。迭代器在创建和自增时,将调用std::regex_search()并存储算法返回的std::match_results对象的副本。

❍ std:: regex_token_iterator:一个常量前向迭代器,用于遍历字符串中正则表达式的每个匹配的子匹配项。本质上,它使用std::regex_iterator逐级遍历子匹配项。因为它存储了一个指向std::basic_regex实例的指针,所以正则表达式对象必须一直存在,直到迭代器被销毁。

值得一提的是,与其他实现(如Boost.Regex)相比,标准regex库的性能较差,而且不支持Unicode。此外,也有人认为API本身使用起来就很麻烦。

2.9.5 延伸阅读

❍ 阅读2.10节,以了解如何对文本中的一个模式进行多项匹配。

❍ 阅读2.11节,以了解如何利用正则表达式实现文本替换。

❍ 阅读1.13节,以了解如何将变量绑定到初始化表达式中的子对象或元素。 fYn6iE23+lQ2WeaXJMNlybC2NvCH2j8VTf9TIPSKg0vsLYO3R48sf9RjX2Q91nhO

点击中间区域
呼出菜单
上一章
目录
下一章
×