数据处理 | 你不得不会的「正则表达式」

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

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

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

  2. 首位不能为0;

  3. 是否为纯数字?

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

 1std::string qq;
 2std::cin >> qq;
 3
 4// 1. 判断位数是否合法
 5if (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.\n";
16    }
17}

虽然写出来了,但是有没有感到异常繁琐?这还仅仅是一个对应规则较少的处理,便如此麻烦,若是要检测IP地址、身份证号,或是解析一段HTML数据,或是其它更复杂的字串,那岂非更令人叫苦不迭?

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

1std::regex qq_reg("[1-9]\\d{4,11}");
2bool ret = std::regex_match(qq, qq_reg);
3std::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(),它是个函数模板,我们直接来看例子:

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

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

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

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

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

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

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

输出结果为:

 1<xml>value</xml>
 216
 30
 4----------------
 5<xml>value</xml> <xml>value</xml>
 6xml xml
 7value value
 8xml xml
 9----------------
10<xml>value</xml>
11xml
12value
13xml

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

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

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

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

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

搜索(Search)

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

还是以例子来看:

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

输出为:

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

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

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

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

 1std::regex reg("<(.*)>(.*)</(\\1)>");
 2std::string content("123<xml>value</xml>456<widget>center</widget>hahaha<vertical>window</vertical>the end");
 3std::smatch m;
 4auto pos = content.cbegin();
 5auto end = content.cend();
 6for (; 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>
 3xml
 4value
 5xml
 6----------------
 7<widget>center</widget>
 8widget
 9center
10widget
11----------------
12<vertical>window</vertical>
13vertical
14window
15vertical

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

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

分词(Tokenize)

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

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

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

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

1123@qq.vip.com
2456@gmail.com
3789@163.com
4abcd@my.com

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

替换(Replace)

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

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

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

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

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

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

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

实例(Examples)

1. 验证邮箱

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

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

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

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

1123@qq.vip.com
2456@gmail.com
3789@163.com
4abcd@my.com

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

 1std::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";
 7std::regex reg("[a-zA-z0-9_]+@[a-zA-z0-9]+(\\.[a-zA-z]+){1,3}");
 8
 9std::sregex_iterator pos(data.cbegin(), data.cend(), reg);
10decltype(pos) end;
11for (; pos != end; ++pos)
12{
13    std::cout << pos->str() << std::endl;
14}

输出为:

1123@qq.vip.com
2456@gmail.com
3789@163.com.cn.mail
4abcd@my.com
5haha@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地址。

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

 1std::string ip("192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30");
 2
 3std::cout << "原内容为:\n" << ip << std::endl;
 4
 5// 1. 位数对齐
 6ip = std::regex_replace(ip, std::regex("(\\d+)"), "00$1");
 7
 8std::cout << "位数对齐后为:\n" << ip << std::endl;
 9
10// 2. 有0的去掉
11ip = std::regex_replace(ip, std::regex("0*(\\d{3})"), "$1");
12
13std::cout << "去掉0后为:\n" << ip << std::endl;
14
15// 3. 取出IP
16std::regex reg("\\s");
17std::sregex_token_iterator pos(ip.begin(), ip.end(), reg, -1);
18decltype(pos) end;
19
20std::set<std::string> ip_set;
21for (; pos != end; ++pos)
22{
23    ip_set.insert(pos->str());
24}
25
26std::cout << "------\n最终结果:\n";
27
28// 4. 输出排序后的数组
29for (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原内容为:
 2192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30
 3位数对齐后为:
 400192.0068.001.00254 00102.0049.0023.00013 0010.0010.0010.0010 002.002.002.002 008.00109.0090.0030
 5去掉0后为:
 6192.068.001.254 102.049.023.013 010.010.010.010 002.002.002.002 008.109.090.030
 7------
 8最终结果:
 92.2.2.2
108.109.90.30
1110.10.10.10
12102.49.23.13
13192.68.1.254

THE END

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Requests 库是 Python 中一个常用的 HTTP 请求库,可以方便地发送 HTTP/1.1 请求。在使用 Requests 库时,我们可以使用正则表达式来选取页面中的数据,这对于数据爬取和处理非常有帮助。 以下是一些使用 Requests 库和正则表达式选取数据的规则: 1. 发送 HTTP 请求 首先,使用 Requests 库发送 HTTP 请求,可以使用 get() 或 post() 方法: ``` import requests url = "http://www.example.com" response = requests.get(url) ``` 2. 使用正则表达式选取数据 使用 re 模块中的 findall() 方法来选取数据。例如,要选取页面中的所有链接,可以使用正则表达式 `href="(.*?)"`: ``` import re pattern = 'href="(.*?)"' links = re.findall(pattern, response.text) print(links) ``` 3. 正则表达式语法 在使用正则表达式时,常用的语法包括: - `.`:匹配任意字符 - `*`:匹配前面的字符 0 次或多次 - `+`:匹配前面的字符 1 次或多次 - `?`:匹配前面的字符 0 次或 1 次 - `()`:匹配括号内的表达式,并捕获匹配的结果 - `[]`:匹配方括号内的任意一个字符 - `|`:匹配两个或多个表达式中的任意一个 - `\`:用于转义特殊字符,例如 `\.` 表示匹配句号字符 4. 匹配多个结果 使用 re.findall() 方法可以匹配多个结果,返回一个列表。例如,匹配页面中所有的图片链接: ``` pattern = '<img src="(.*?)"' images = re.findall(pattern, response.text) print(images) ``` 5. 匹配单个结果 使用 re.search() 方法可以匹配单个结果,返回一个 Match 对象。例如,匹配页面中第一个图片链接: ``` pattern = '<img src="(.*?)"' match = re.search(pattern, response.text) if match: print(match.group(1)) ``` 以上是使用 Requests 库和正则表达式选取数据的一些规则,需要根据实际情况进行修改和完善。同时要注意,进行数据爬取时需要遵守相关法律法规和网站协议,不得进行恶意攻击、侵犯他人权益等行为。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值