zoukankan      html  css  js  c++  java
  • 详解C++正则表达式

    来源:CPP编程客-cpluspluser

     

    【CPP开发者导读】:在讨论正则表达式之前,先介绍另外一话题:字符串处理能力是所有程序员的基本功,例如在自然语言处理领域,就经常会遇到字符串处理的问题,当数据在输入到机器学习模型之前和之后,要涉及到大量的预处理和后处理工作,比如要在预处理阶段过滤掉一部分字符,或者把数据进行规范化,在后处理阶段可能还要纠正模型的一些错误和不足。尤其是在工业界,这些工作占的比重还会更大。这时正则表达式的作用显得极其重要。

     

    而在过去写C++程序的时候,当需要用到正则表达式的时候,由于C++本身的标准库不支持,要么使用第三方库,要么自己编写程序实现,不但非常的麻烦而且容易出错难以维护。

     

    因此从C++11开始,引入对正则表达式的支持,使得C++具备了更多的现代语言特性。

     

    本文适合急需提高字符串处理能力的C++程序员阅读,利用从本文所学到的知识大大提高开发效率,达到事半功倍的效果。

     

     

    以下是正文


     

    若要判断一个输入的QQ号是否有效,你会如何处理呢?

    首先你得分析一下其对应规则,依次列出:

    1. 长度大于5,小于等于11;

    2. 首位不能为0;

    3. 是否为纯数字?

    规则既列,接着就该尝试实现了,那么用什么来表示字符串呢?在C++中,最容易想到的就是string了,其中提供了许多成员函数可以处理字符串,所以有了如下实现:

     1  std::string qq;
     2  std::cin >> qq;
     3  
     4  // 1. 判断位数是否合法
     5  if (qq.length() >= 5 && qq.length() <= 11)
     6  {
     7      // 2. 判断是否非'0'开头
     8      if (qq[0] != '0')
     9      {
    10          // 3. 判断是否为纯数字
    11          auto pos = std::find_if(qq.begin(), qq.end(), [](const char& ch) {
    12              return ch < '0' || ch > '9';
    13          });
    14          if (pos == qq.end())
    15              std::cout << "valid. ";
    16      }
    17  }

    这仅仅是一个对应规则较少的处理,便相当麻烦。若是要检测IP地址、身份证号,或是解析一段HTML数据等等复杂的字符串,便应该寻求别的方式。

    当然,也有许多扩展库对字符串处理提供了方便,其中比较好用的是boost中的string_algo库(已于C++17纳入了标准库,并改名为string_view),但本篇主要说C++11的regex库,其对复杂数据的处理能力非常强,比如可以用它来检测QQ号:

    1  std::regex qq_reg("[1-9]\d{4,11}");
    2  bool ret = std::regex_match(qq, qq_reg);
    3  std::cout << (ret ? "valid" : "invalid") << std::endl;

    是不是超级方便呢?那么接下来便来看看如何使用「正则表达式」。

    正则程序库(regex)

    「正则表达式」就是一套表示规则的式子,专门用来处理各种复杂的操作。

    std::regex是C++用来表示「正则表达式」(regular expression)的库,于C++11加入,它是class std::basic_regex<>针对char类型的一个特化,还有一个针对wchar_t类型的特化为std::wregex。

    正则文法(regex syntaxes)

    std::regex默认使用是ECMAScript文法,这种文法比较好用,且威力强大,常用符号的意义如下:

    符号意义
    ^ 匹配行的开头
    $ 匹配行的结尾
    . 匹配任意单个字符
    […] 匹配[]中的任意一个字符
    (…) 设定分组
    转义字符
    d 匹配数字[0-9]
    D d 取反
    w 匹配字母[a-z],数字,下划线
    W w 取反
    s 匹配空格
    S s 取反
    + 前面的元素重复1次或多次
    * 前面的元素重复任意次
    ? 前面的元素重复0次或1次
    {n} 前面的元素重复n次
    {n,} 前面的元素重复至少n次
    {n,m} 前面的元素重复至少n次,至多m次
    | 逻辑或

    上面列出的这些都是非常常用的符号,靠这些便足以解决绝大多数问题了。

    匹配(Match)

    字符串处理常用的一个操作是「匹配」,即字符串和规则恰好对应,而用于匹配的函数为std::regex_match(),它是个函数模板,我们直接来看例子:

     1  std::regex reg("<.*>.*</.*>");
     2  bool ret = std::regex_match("<html>value</html>", reg);
     3  assert(ret);
     4  
     5  ret = std::regex_match("<xml>value<xml>", reg);
     6  assert(!ret);
     7  
     8  std::regex reg1("<(.*)>.*</\1>");
     9  ret = std::regex_match("<xml>value</xml>", reg1);
    10  assert(ret);
    11  
    12  ret = std::regex_match("<header>value</header>", std::regex("<(.*)>value</\1>"));
    13  assert(ret);
    14  
    15  // 使用basic文法
    16  std::regex reg2("<\(.*\)>.*</\1>", std::regex_constants::basic);
    17  ret = std::regex_match("<title>value</title>", reg2);
    18  assert(ret);

    这个小例子使用regex_match()来匹配xml格式(或是html格式)的字符串,匹配成功则会返回true,意思非常简单,若是不懂其中意思,可参照前面的文法部分。

    对于语句中出现\,是因为需要转义,C++11以后支持原生字符,所以也可以这样使用:

    1  std::regex reg1(R"(<(.*)>.*</1>)");
    2  auto ret = std::regex_match("<xml>value</xml>", reg1);
    3  assert(ret);

    但C++03之前并不支持,所以使用时要需要留意。

    若是想得到匹配的结果,可以使用regex_match()的另一个重载形式:

     1  std::cmatch m;
     2  auto ret = std::regex_match("<xml>value</xml>", m, std::regex("<(.*)>(.*)</(\1)>"));
     3  if (ret)
     4  {
     5      std::cout << m.str() << std::endl;
     6      std::cout << m.length() << std::endl;
     7      std::cout << m.position() << std::endl;
     8  }
     9  
    10  std::cout << "----------------" << std::endl;
    11  
    12  // 遍历匹配内容
    13  for (auto i = 0; i < m.size(); ++i)
    14  {
    15      // 两种方式都可以
    16      std::cout << m[i].str() << " " << m.str(i) << std::endl;
    17  }
    18  
    19  std::cout << "----------------" << std::endl;
    20  
    21  // 使用迭代器遍历
    22  for (auto pos = m.begin(); pos != m.end(); ++pos)
    23  {
    24      std::cout << *pos << std::endl;
    25  }

    输出结果为:

     1  <xml>value</xml>
     2  16
     3  0
     4  ----------------
     5  <xml>value</xml> <xml>value</xml>
     6  xml xml
     7  value value
     8  xml xml
     9  ----------------
    10  <xml>value</xml>
    11  xml
    12  value
    13  xml

    cmatch是class template std::match_result<>针对C字符的一个特化版本,若是string,便得用针对string的特化版本smatch。同时还支持其相应的宽字符版本wcmatch和wsmatch。

    在regex_match()的第二个参数传入match_result便可获取匹配的结果,在例子中便将结果储存到了cmatch中,而cmatch又提供了许多函数可以对这些结果进行操作,大多方法都和string的方法类似,所以使用起来比较容易。

    m[0]保存着匹配结果的所有字符,若想在匹配结果中保存有子串,则得在「正则表达式」中用()标出子串,所以这里多加了几个括号:

    1    std::regex("<(.*)>(.*)</(\1)>")

    这样这些子串就会依次保存在m[0]的后面,即可通过m[1],m[2],…依次访问到各个子串。

    搜索(Search)

    「搜索」与「匹配」非常相像,其对应的函数为std::regex_search,也是个函数模板,用法和regex_match一样,不同之处在于「搜索」只要字符串中有目标出现就会返回,而非完全「匹配」。

    还是以例子来看:

     1  std::regex reg("<(.*)>(.*)</(\1)>");
     2  std::cmatch m;
     3  auto ret = std::regex_search("123<xml>value</xml>456", m, reg);
     4  if (ret)
     5  {
     6      for (auto& elem : m)
     7          std::cout << elem << std::endl;
     8  }
     9  
    10  std::cout << "prefix:" << m.prefix() << std::endl;
    11  std::cout << "suffix:" << m.suffix() << std::endl;

    输出为:

    1   <xml>value</xml>
    2   xml
    3   value
    4   xml
    5   prefix:123
    6   suffix:456

    这儿若换成regex_match匹配就会失败,因为regex_match是完全匹配的,而此处字符串前后却多加了几个字符。

    对于「搜索」,在匹配结果中可以分别通过prefix和suffix来获取前缀和后缀,前缀即是匹配内容前面的内容,后缀则是匹配内容后面的内容。

    那么若有多组符合条件的内容又如何得到其全部信息呢?这里依旧通过一个小例子来看:

     1   std::regex reg("<(.*)>(.*)</(\1)>");
     2   std::string content("123<xml>value</xml>456<widget>center</widget>hahaha<vertical>window</vertical>the end");
     3   std::smatch m;
     4   auto pos = content.cbegin();
     5   auto end = content.cend();
     6   for (; std::regex_search(pos, end, m, reg); pos = m.suffix().first)
     7   {
     8       std::cout << "----------------" << std::endl;
     9       std::cout << m.str() << std::endl;
    10       std::cout << m.str(1) << std::endl;
    11       std::cout << m.str(2) << std::endl;
    12       std::cout << m.str(3) << std::endl;
    13   }

    输出结果为:

     1   ----------------
     2   <xml>value</xml>
     3   xml
     4   value
     5   xml
     6   ----------------
     7   <widget>center</widget>
     8   widget
     9   center
    10   widget
    11   ----------------
    12   <vertical>window</vertical>
    13   vertical
    14   window
    15   vertical

    此处使用了regex_search函数的另一个重载形式(regex_match函数亦有同样的重载形式),实际上所有的子串对象都是从std::pair<>派生的,其first(即此处的prefix)即为第一个字符的位置,second(即此处的suffix)则为最末字符的下一个位置。

    一组查找完成后,便可从suffix处接着查找,这样就能获取到所有符合内容的信息了。

    分词(Tokenize)

    还有一种操作叫做「切割」,例如有一组数据保存着许多邮箱账号,并以逗号分隔,那就可以指定以逗号为分割符来切割这些内容,从而得到每个账号。

    而在C++的正则中,把这种操作称为Tokenize,用模板类regex_token_iterator<>提供分词迭代器,依旧通过例子来看:

    1   std::string mail("123@qq.vip.com,456@gmail.com,789@163.com,abcd@my.com");
    2   std::regex reg(",");
    3   std::sregex_token_iterator pos(mail.begin(), mail.end(), reg, -1);
    4   decltype(pos) end;
    5   for (; pos != end; ++pos)
    6   {
    7       std::cout << pos->str() << std::endl;
    8   }

    这样,就能通过逗号分割得到所有的邮箱:

    1   123@qq.vip.com
    2   456@gmail.com
    3   789@163.com
    4   abcd@my.com

    sregex_token_iterator是针对string类型的特化,需要注意的是最后一个参数,这个参数可以指定一系列整数值,用来表示你感兴趣的内容,此处的-1表示对于匹配的正则表达式之前的子序列感兴趣;而若指定0,则表示对于匹配的正则表达式感兴趣,这里就会得到“,";还可对正则表达式进行分组,之后便能输入任意数字对应指定的分组,大家可以动手试试。

    替换(Replace)

    最后一种操作称为「替换」,即将正则表达式内容替换为指定内容,regex库用模板函数std::regex_replace提供「替换」操作。

    现在,给定一个数据为"he…ll..o, worl..d!", 思考一下,如何去掉其中误敲的“.”?

    有思路了吗?来看看正则的解法:

    1  char data[] = "he...ll..o, worl..d!";
    2  std::regex reg("\.");
    3  // output: hello, world!
    4  std::cout << std::regex_replace(data, reg, "");

    我们还可以使用分组功能:

    1  char data[] = "001-Neo,002-Lucia";
    2  std::regex reg("(\d+)-(\w+)");
    3  // output: 001 name=Neo,002 name=Lucia
    4  std::cout << std::regex_replace(data, reg, "$1 name=$2");

    当使用分组功能后,可以通过N来得到分组内容,这个功能挺有用的。

    实例(Examples)

    1. 验证邮箱

    这个需求在注册登录时常有用到,用于检测用户输入的合法性。

    若是对匹配精确度要求不高,那么可以这么写:

    1  std::string data = "123@qq.vip.com,456@gmail.com,789@163.com,abcd@my.com";
    2  std::regex reg("\w+@\w+(\.\w+)+");
    3
    4  std::sregex_iterator pos(data.cbegin(), data.cend(), reg);
    5  decltype(pos) end;
    6  for (; pos != end; ++pos)
    7  {
    8      std::cout << pos->str() << std::endl;
    9  }

    这里使用了另外一种遍历正则查找的方法,这种方法使用regex iterator来迭代,效率要比使用match高。这里的正则是一个弱匹配,但对于一般用户的输入来说没有什么问题,关键是简单,输出为:

    1  123@qq.vip.com
    2  456@gmail.com
    3  789@163.com
    4  abcd@my.com

    但若我输入一个“Abc0_@aAa1.123.456.789”,它依旧能匹配成功,这明显是个非法邮箱,更精确的正则应该这样写:

     1   std::string data = "123@qq.vip.com, 
     2            456@gmail.com,
     3            789@163.com.cn.mail,
     4            abcd@my.com,
     5            Abc0_@aAa1.123.456.789
     6            haha@163.com.cn.com.cn";
     7   std::regex reg("[a-zA-z0-9_]+@[a-zA-z0-9]+(\.[a-zA-z]+){1,3}");
     8 
     9   std::sregex_iterator pos(data.cbegin(), data.cend(), reg);
    10   decltype(pos) end;
    11   for (; pos != end; ++pos)
    12   {
    13       std::cout << pos->str() << std::endl;
    14   }

    输出为:

    1  123@qq.vip.com
    2  456@gmail.com
    3  789@163.com.cn.mail
    4  abcd@my.com
    5  haha@163.com.cn.com

    2. 匹配IP

    有这样一串IP地址,192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30,
    要求:取出其中的IP地址,并按地址段顺序输出IP地址。

    有点晚了,便不详细解释了,这里直接给出答案,可供大家参考:

     1  std::string ip("192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30");
     2  
     3  std::cout << "原内容为: " << ip << std::endl;
     4  
     5  // 1. 位数对齐
     6  ip = std::regex_replace(ip, std::regex("(\d+)"), "00$1");
     7  
     8  std::cout << "位数对齐后为: " << ip << std::endl;
     9  
    10  // 2. 有0的去掉
    11  ip = std::regex_replace(ip, std::regex("0*(\d{3})"), "$1");
    12  
    13  std::cout << "去掉0后为: " << ip << std::endl;
    14  
    15  // 3. 取出IP
    16  std::regex reg("\s");
    17  std::sregex_token_iterator pos(ip.begin(), ip.end(), reg, -1);
    18  decltype(pos) end;
    19  
    20  std::set<std::string> ip_set;
    21  for (; pos != end; ++pos)
    22  {
    23      ip_set.insert(pos->str());
    24  }
    25  
    26  std::cout << "------ 最终结果: ";
    27  
    28  // 4. 输出排序后的数组
    29  for (auto elem : ip_set)
    30  {
    31      // 5. 去掉多余的0
    32      std::cout << std::regex_replace(elem,
    33          std::regex("0*(\d+)"), "$1") << std::endl;
    34  }

    输出结果为:

     1  原内容为:
     2  192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30
     3  位数对齐后为:
     4  00192.0068.001.00254 00102.0049.0023.00013 0010.0010.0010.0010 002.002.002.002 008.00109.0090.0030
     5  去掉0后为:
     6  192.068.001.254 102.049.023.013 010.010.010.010 002.002.002.002 008.109.090.030
     7  ------
     8  最终结果:
     9  2.2.2.2
    10  8.109.90.30
    11  10.10.10.10
    12  102.49.23.13
    13  192.68.1.254
  • 相关阅读:
    java-泛型及上界下界详解
    【CSDN】Spring+Spring MVC+Mybatis实战项目之云笔记项目
    mybatis
    spring笔记-spring mvc表单
    spring笔记-第一个spring mvc 项目
    巡风源码阅读与分析---AddPlugin()方法
    巡风源码阅读与分析---view.py
    BUGKUctf-web-writeup
    陕西省网络空间安全技术大赛部分题目writeup
    “百度杯”CTF比赛(二月场)-web-writeup
  • 原文地址:https://www.cnblogs.com/roea1/p/13839016.html
Copyright © 2011-2022 走看看