正则表达式的使用

正则表达式是一个特殊的字符序列,它能帮助你方便的检查一个字符串是否与某种模式匹配。这里先分析正则表达式的模式字符串,然后分别给出了python和c++类的用法

正则语法

模式字符串

字母和数字表示他们自身。一个正则表达式模式中的字母和数字匹配同样的字符串。

多数字母和数字前加一个反斜杠时会拥有不同的含义。

标点符号只有被转义时才匹配自身,否则它们表示特殊的含义。

反斜杠本身需要使用反斜杠转义。

由于正则表达式通常都包含反斜杠,所以你最好使用原始字符串来表示它们。模式元素(如 r',等价于 '\t')匹配相应的特殊字符。

下表列出了正则表达式模式语法中的特殊元素。如果你使用模式的同时提供了可选的标志参数,某些模式元素的含义会改变。

模式 描述
^ 匹配字符串的开头
$ 匹配字符串的末尾。
. 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。
[...] 用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k'
[^...] 不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。
re* 匹配0个或多个的表达式。
re+ 匹配1个或多个的表达式。
re? 匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
re{ n} 精确匹配 n 个前面表达式。例如, o{2} 不能匹配 "Bob" 中的 "o",但是能匹配 "food" 中的两个 o。
re{ n,} 匹配 n 个前面表达式。例如, o{2,} 不能匹配"Bob"中的"o",但能匹配 "foooood"中的所有 o。"o{1,}" 等价于 "o+"。"o{0,}" 则等价于 "o*"。
re{ n, m} 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
a| b 匹配a或b
(re) 对正则表达式分组并记住匹配的文本
(?imx) 正则表达式包含三种可选标志:i, m, 或 x 。只影响括号中的区域。
(?-imx) 正则表达式关闭 i, m, 或 x 可选标志。只影响括号中的区域。
(?: re) 类似 (...), 但是不表示一个组
(?imx: re) 在括号中使用i, m, 或 x 可选标志
(?-imx: re) 在括号中不使用i, m, 或 x 可选标志
(?#...) 注释.
(?= re) 前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。
(?! re) 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功
(?> re) 匹配的独立模式,省去回溯。
匹配字母数字及下划线
匹配非字母数字及下划线
匹配任意空白字符,等价于 [.
匹配任意非空字符
匹配任意数字,等价于 [0-9].
匹配任意非数字
匹配字符串开始
匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。
匹配字符串结束
匹配最后匹配完成的位置。
匹配一个单词边界,也就是指单词和空格间的位置。例如, 'er 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。
匹配非单词边界。'er' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。
, 等. 匹配一个换行符。匹配一个制表符。等
\1...\9 匹配第n个分组的内容。
\10 匹配第n个分组的内容,如果它经匹配。否则指的是八进制字符码的表达式。

修饰符

标记也称为修饰符,正则表达式的标记用于指定额外的匹配策略。 标记不写在正则表达式里,标记位于表达式之外,格式如/pattern/flags

修饰符 含义 描述
i ignore - 不区分大小写 将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别。
g global - 全局匹配 查找所有的匹配项。
m multi line - 多行匹配 使边界字符 ^ 和 $ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。
s 特殊字符圆点 . 中包含换行符 默认情况下的圆点 . 是匹配除换行符 之外的任何字符,加上 s 修饰符之后, . 中包含换行符 。

正则表达式实例

实例 描述
[Pp]ython 匹配 "Python" 或 "python"
rub[ye] 匹配 "ruby" 或 "rube"
[aeiou] 匹配中括号内的任意一个字母
[0-9] 匹配任何数字。类似于 [0123456789]
[a-z] 匹配任何小写字母
[A-Z] 匹配任何大写字母
[a-zA-Z0-9] 匹配任何字母及数字
[^aeiou] 除了aeiou字母以外的所有字符
[^0-9] 匹配除了数字外的字符

python中的正则表达式

Python 自1.5版本起增加了re 模块,它提供 Perl 风格的正则表达式模式。 re 模块使 Python 语言拥有全部的正则表达式功能。

re对象

re.compile() 返回 RegexObject 对象。

RegexObject.group() 返回被 RE 匹配的字符串。

  • start() 返回匹配开始的位置
  • end() 返回匹配结束的位置
  • span() 返回一个元组包含匹配 (开始,结束) 的位置

re.match函数

re.match 尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话,match()就返回none。

函数语法

re.match(pattern, string, flags=0)

函数参数说明:

参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。

匹配成功re.match方法返回一个匹配的对象,否则返回None。

我们可以使用group(num) 或 groups() 匹配对象函数来获取匹配表达式。

匹配对象方法 描述
group(num=0) 匹配的整个表达式的字符串,group() 可以一次输入多个组号,在这种情况下它将返回一个包含那些组所对应值的元组。
groups() 返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。
#!/usr/bin/python 
# -*- coding: UTF-8 -*-
import re
print(re.match('www', 'www.runoob.com').span()) # 在起始位置匹配
print(re.match('com', 'www.runoob.com')) # 不在起始位置匹配

以上实例运行输出结果为:

(0, 3)
None

re.search方法

re.search 扫描整个字符串并返回第一个成功的匹配。

函数语法:

re.search(pattern, string, flags=0)

函数参数说明:

参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。

匹配成功re.search方法返回一个匹配的对象,否则返回None。

我们可以使用group(num) 或 groups() 匹配对象函数来获取匹配表达式。

匹配对象方法 描述
group(num=0) 匹配的整个表达式的字符串,group() 可以一次输入多个组号,在这种情况下它将返回一个包含那些组所对应值的元组。
groups() 返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。
#!/usr/bin/python 
# -*- coding: UTF-8 -*-
import re
print(re.search('www', 'www.runoob.com').span()) # 在起始位置匹配
print(re.search('com', 'www.runoob.com').span()) # 不在起始位置匹配

以上实例运行输出结果为:

(0, 3)
(11, 14)

re.sub方法

Python 的 re 模块提供了re.sub用于替换字符串中的匹配项。

语法:

re.sub(pattern, repl, string, count=0, flags=0)

参数:

  • pattern : 正则中的模式字符串。
  • repl : 替换的字符串,也可为一个函数。
  • string : 要被查找替换的原始字符串。
  • count : 模式匹配后替换的最大次数,默认 0 表示替换所有的匹配。
#!/usr/bin/python 
# -*- coding: UTF-8 -*-
import re
phone = "2004-959-559 # 这是一个国外电话号码"
# 删除字符串中的 Python注释
num = re.sub(r'#.*$', "", phone)
print "电话号码是: ", num
# 删除非数字(-)的字符串
num = re.sub(r'\D', "", phone)
print "电话号码是 : ", num

以上实例执行结果如下:

电话号码是:  2004-959-559 
电话号码是 : 2004959559

repl 参数是一个函数。以下实例中将字符串中的匹配的数字乘以 2:

#!/usr/bin/python 
# -*- coding: UTF-8 -*-
import re
# 将匹配的数字乘以 2
def double(matched):
value = int(matched.group('value'))
return str(value * 2)
s = 'A23G4HFD567'
print(re.sub('(?P<value>\d+)', double, s))

执行输出结果为:

A46G8HFD1134

re.compile 函数

compile 函数用于编译正则表达式,生成一个正则表达式( Pattern )对象,供 match() 和 search() 这两个函数使用。

语法格式为:

re.compile(pattern[, flags])

参数:

  • pattern : 一个字符串形式的正则表达式
  • flags : 可选,表示匹配模式,比如忽略大小写,多行模式等,具体参数为:
    • re.I 忽略大小写
    • re.L 表示特殊字符集 , , , , 依赖于当前环境
    • re.M 多行模式
    • re.S 即为 . 并且包括换行符在内的任意字符(. 不包括换行符)
    • re.U 表示特殊字符集 , , , , , 依赖于 Unicode 字符属性数据库
    • re.X 为了增加可读性,忽略空格和 # 后面的注释
>>>import re
>>> pattern = re.compile(r'\d+') # 用于匹配至少一个数字
>>> m = pattern.match('one12twothree34four') # 查找头部,没有匹配
>>> print m
None
>>> m = pattern.match('one12twothree34four', 2, 10) # 从'e'的位置开始匹配,没有匹配
>>> print m
None
>>> m = pattern.match('one12twothree34four', 3, 10) # 从'1'的位置开始匹配,正好匹配
>>> print m # 返回一个 Match 对象
<_sre.SRE_Match object at 0x10a42aac0>
>>> m.group(0) # 可省略 0
'12'
>>> m.start(0) # 可省略 0
3
>>> m.end(0) # 可省略 0
5
>>> m.span(0) # 可省略 0
(3, 5)

在上面,当匹配成功时返回一个 Match 对象,其中:

  • group([group1, …]) 方法用于获得一个或多个分组匹配的字符串,当要获得整个匹配的子串时,可直接使用 group()group(0)
  • start([group]) 方法用于获取分组匹配的子串在整个字符串中的起始位置(子串第一个字符的索引),参数默认值为 0;
  • end([group]) 方法用于获取分组匹配的子串在整个字符串中的结束位置(子串最后一个字符的索引+1),参数默认值为 0;
  • span([group]) 方法返回 (start(group), end(group))

re.findall方法

在字符串中找到正则表达式所匹配的所有子串,并返回一个列表,如果没有找到匹配的,则返回空列表。

注意: match 和 search 是匹配一次 findall 匹配所有。

语法格式为:

findall(string[, pos[, endpos]])

参数:

  • string : 待匹配的字符串。
  • pos : 可选参数,指定字符串的起始位置,默认为 0。
  • endpos : 可选参数,指定字符串的结束位置,默认为字符串的长度。

查找字符串中的所有数字:

# -*- coding:UTF8 -*-

import re

pattern = re.compile(r'\d+') # 查找数字
result1 = pattern.findall('runoob 123 google 456')
result2 = pattern.findall('run88oob123google456', 0, 10)

print(result1)
print(result2)

输出结果:

['123', '456']
['88', '12']

re.split方法

split 方法按照能够匹配的子串将字符串分割后返回列表,它的使用形式如下:

re.split(pattern, string[, maxsplit=0, flags=0])

参数:

参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串。
maxsplit 分隔次数,maxsplit=1 分隔一次,默认为 0,不限制次数。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。参见:正则表达式修饰符 - 可选标志
>>>import re
>>> re.split('\W+', 'runoob, runoob, runoob.')
['runoob', 'runoob', 'runoob', '']
>>> re.split('(\W+)', ' runoob, runoob, runoob.')
['', ' ', 'runoob', ', ', 'runoob', ', ', 'runoob', '.', '']
>>> re.split('\W+', ' runoob, runoob, runoob.', 1)
['', 'runoob, runoob, runoob.']

>>> re.split('a*', 'hello world') # 对于一个找不到匹配的字符串而言,split 不会对其作出分割
['hello world']

C++中的正则表达式

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

匹配regex_match

std::regex reg("<.*>.*</.*>");
bool ret = std::regex_match("<html>value</html>", reg);
assert(ret);

ret = std::regex_match("<xml>value<xml>", reg);
assert(!ret);

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

ret = std::regex_match("<header>value</header>", std::regex("<(.*)>value</\\1>"));
assert(ret);

// 使用basic文法
std::regex reg2("<\\(.*\\)>.*</\\1>", std::regex_constants::basic);
ret = std::regex_match("<title>value</title>", reg2);
assert(ret);

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

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

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

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

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

std::cmatch m;
auto ret = std::regex_match("<xml>value</xml>", m, std::regex("<(.*)>(.*)</(\\1)>"));
if (ret)
{
std::cout << m.str() << std::endl;
std::cout << m.length() << std::endl;
std::cout << m.position() << std::endl;
}

std::cout << "----------------" << std::endl;

// 遍历匹配内容
for (auto i = 0; i < m.size(); ++i)
{
// 两种方式都可以
std::cout << m[i].str() << " " << m.str(i) << std::endl;
}

std::cout << "----------------" << std::endl;

// 使用迭代器遍历
for (auto pos = m.begin(); pos != m.end(); ++pos)
{
std::cout << *pos << std::endl;
}

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

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

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

搜索(Search)

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

还是以例子来看:

std::regex reg("<(.*)>(.*)</(\\1)>");
std::cmatch m;
auto ret = std::regex_search("123<xml>value</xml>456", m, reg);
if (ret)
{
for (auto& elem : m)
std::cout << elem << std::endl;
}

std::cout << "prefix:" << m.prefix() << std::endl;
std::cout << "suffix:" << m.suffix() << std::endl;

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

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

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

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

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

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

分词(Tokenize)

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

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

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

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

替换(Replace)

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

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

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

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

实例(Examples)

验证邮箱

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

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

std::string data = "123@qq.vip.com,456@gmail.com,789@163.com,abcd@my.com";
std::regex reg("\\w+@\\w+(\\.\\w+)+");

std::sregex_iterator pos(data.cbegin(), data.cend(), reg);
decltype(pos) end;
for (; pos != end; ++pos)
{
std::cout << pos->str() << std::endl;
}

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

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

std::string data = "123@qq.vip.com, \
456@gmail.com, \
789@163.com.cn.mail, \
abcd@my.com, \
Abc0_@aAa1.123.456.789 \
haha@163.com.cn.com.cn";
std::regex reg("[a-zA-z0-9_]+@[a-zA-z0-9]+(\\.[a-zA-z]+){1,3}");

std::sregex_iterator pos(data.cbegin(), data.cend(), reg);
decltype(pos) end;
for (; pos != end; ++pos)
{
std::cout << pos->str() << std::endl;
}

匹配IP

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

std::string ip("192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30");
std::cout << "原内容为:\n" << ip << std::endl;

// 1. 位数对齐
ip = std::regex_replace(ip, std::regex("(\\d+)"), "00$1");
std::cout << "位数对齐后为:\n" << ip << std::endl;

// 2. 有0的去掉
ip = std::regex_replace(ip, std::regex("0*(\\d{3})"), "$1");
std::cout << "去掉0后为:\n" << ip << std::endl;

// 3. 取出IP
std::regex reg("\\s");
std::sregex_token_iterator pos(ip.begin(), ip.end(), reg, -1);
decltype(pos) end;

std::set<std::string> ip_set;
for (; pos != end; ++pos)
{
ip_set.insert(pos->str());
}

std::cout << "------\n最终结果:\n";

// 4. 输出排序后的数组
for (auto elem : ip_set)
{
// 5. 去掉多余的0
std::cout << std::regex_replace(elem,
std::regex("0*(\\d+)"), "$1") << std::endl;
}

输出结果为:

原内容为:
192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30
位数对齐后为:
00192.0068.001.00254 00102.0049.0023.00013 0010.0010.0010.0010 002.002.002.002 008.00109.0090.0030
去掉0后为:
192.068.001.254 102.049.023.013 010.010.010.010 002.002.002.002 008.109.090.030
------
最终结果:
2.2.2.2
8.109.90.30
10.10.10.10
102.49.23.13
192.68.1.254