首页 > 技术文章 > 【python语法】正则表达式

dy2903 2018-01-27 11:46 原文

为什么要用正则表达式

对字符串进行操作几乎是每种编程语言中最重要的功能之一。很简单就可以理解,因为人类进行信息传播主要靠的是文字,也就是字符串,但是这么多信息并不完全是我们所要的,所以我们会通过编程来提取或者验证字符串的部分。

正则表达式就是用来匹配字符串的工具,其实它定义了一套语法,用若干描述字符就可以匹配出某段字符串的特征来。凡是符合种描述规则的,我们就认为它匹配

所以比如我们要判断一串字符是否为合法的Email地址的方法就是:

  • 创建一个符合Email特征的正则表达式
  • 然后使用该正则表达式去匹配输入的字符串,以判断是否合法。

正则表达式

元字符

用\d可以匹配一个数字,\w可以匹配一个字母或数字

元字符 匹配
. 任意字符(但是不包括换行符\n\r等
\w 字母 or 数字 or 下划线
\s 空白符(包括Tab等)
\d 数字

举个例子 'py.'可以匹配'pyc''pyo''py!'等等。因为.表示的是任意字符,所以可以匹配正常的字母,也可以匹配!

注意一个元字符只代表一个字符,比如\w只代表一个字母或者数字。

可以用[]表示范围,比如[0-9]表示匹配0~9之间的任意一个数字

  • [0-9a-zA-Z\_]可以匹配一个数字、字母或者下划线,可以等价于\w

有时需要查找不属于某个能简单定义的字符类的字符,这就是反义

代码/语法 匹配
[^x] 除了x以外的任意字符
[^aeiou] 除了aeiou这几个字母以外的任意字符

匹配变长的

如果好匹配变长的字符,用*表示0个或者以上的字符,用+表示1个或者以上的字符,用?表示0个或者1个字符。

还可以用大括号来表示,用{n}表示n个字符,用{n,m}表示n-m个字符。

代码/语法 说明
* 重复0次以上,等价于{0,}
+ 重复1次以上,等价于{1,}
? 重复0次或者1次,等价于{0,1}
{n} 重复n次
{n,} 重复n次以上
{n,m} 重复n到m次

所以比如\d{3}\s+\d{3,8}可以匹配哪些类型的字符串呢?
从左到右读一下:

  • \d{3}表示匹配3个数字,例如'010';
  • \s可以匹配一个空格(也包括Tab等空白符),所以\s+表示至少有一个空格,例如匹配' ',' '等;
  • \d{3,8}表示3-8个数字,例如'1234567'。

如果要匹配'010-12345'这样的号码呢?由于'-'是特殊字符,在正则表达式中,要用''转义,所以,上面的正则是\d{3}-\d{3,8}。

  • [0-9a-zA-Z\_]+可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100''0_Z''Py3000'等等;

  • [a-zA-Z\_][0-9a-zA-Z\_]*可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量;

  • [a-zA-Z\_][0-9a-zA-Z\_]{0, 19}更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。

注意与通配符区分,linux的bash命令行中可以使用通配符,用*来代理任意个的字符。对于正则表达式而言,必须使用.*来表示任意个字符

那么对之前电话号码的那个例子,我们可以用更复杂的表达式来匹配\(?0\d{2}[) -]?\d{8}。\(?0\d{2}[) -]?\d{8}。,可以匹配(010)88886666,或022-22334455,或02912345678等。

  • 首先是一个转义字符(,它能出现0次或1次(?),
  • 然后是一个0,后面跟着2个数字(\d{2}),
  • 然后是)或-或空格中的一个,它出现1次或不出现(?),
  • 最后是8个数字(\d{8}

但是这个表达式也能匹配010)12345678或(022-87654321这样的“不正确”的格式。后面会说怎么样修改就可以解决这个问题。

边界限定符

边界限定 匹配
^ 字符串的开始
$ 字符串的结束

比如^\d{5,12}$表示以数字开头,以数字结尾,整行匹配,同时长度在5~12位一串数字。

分支条件

所谓分支条件就类似逻辑中的“或”,满足任意一个条件即匹配。具体方法是用|把不同的规则分隔开

比如之前讲过的匹配电话号码的例子。

  • 0\d{2}-\d{8}|0\d{3}-\d{7}这个表达式能匹配
    • 三位区号,8位本地号(如010-12345678),
    • 4位区号,7位本地号(0376-2233445)。
  • \(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8}:这个表达式被|分为两个条件
    • 左边的表达式:\(0\d{2}\)可以匹配(010),[- ]?表示之间的连接符可以为-,也可以用空格间隔,也可以没有。
    • 右边的表达式0\d{2}[- ]?\d{8}:表示区号不用小括号括起来。

注意:匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。

分组

之前提到的是怎么重复单个字符(直接在字符后面加上限定符就行了);
但如果想要重复多个字符又该怎么办?可以用小括号来指定子表达式(也叫做分组),然后你就可以指定这个子表达式的重复次数了

比如(\d{1,3}\.){3}\d{1,3}可以按顺序进行分析,

  • \d{1,3}匹配1到3位的数字,
  • (\d{1,3}.){3}匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复3次,
  • 最后再加上一个一到三位的数字(\d{1,3})。

总结

相信突然一下出现这么的符号大家一定是懵逼的。下面我们来总结一下{}[]()这几种符号的用途。

  • {2,3}:需要与它前面的字符结合,比如a{2,3}表示a出现2~3次
  • []:有3层含义
    • [a-z]:表示一个范围,也就是a~z之间的一个字符
    • [.*]:只要放入了[]里面的.*都不表示之前的含义,只是单纯作为一个普通的符号而已。比如这里面就表示要么为点号要么为星号的符号。
    • [^a]:表示非a的所有字符。主要不要和^a混淆,^a表示以a开头的一行。

贪婪匹配与懒惰匹配

a.*b来说 ,它将匹配最长的以a开始,以b结束的字符串,比如用它来搜索aabab的时候,会匹配整个字符串aabab,这就是贪婪匹配,也就是尽可能多的匹配

那么懒惰匹配指的就是尽可能少的匹配字符。在.*后面加上一个?以后,可以转换为懒惰匹配模式,那么.*?意味着使匹配成功的前提下使用最少的重复。比如把它应用于aabab,会匹配aab和ab

为什么第一个匹配是aab而不是ab?因为正则表达式有一条规则:最先开始的匹配拥有最高的优先权

| 代码/语法 | 说明 |
|-|
| *? | 重复任意次,但尽可能少重复 |
| +? | 重复1次或更多次,但尽可能少重复 |
| ?? | 重复0次或1次,但尽可能少重复 |
| {n,m}? | 重复n到m次,但尽可能少重复 |
| {n,}? | 重复n次以上,但尽可能少重复 |

匹配汉字

匹配汉字的表达式为[\u4E00-\u9FA5],这是汉字的UTF-8编码的范围。

python调用正则表达式

Python提供re模块,包含所有正则表达式的功能。由于Python的字符串本身也用\转义,所以要特别注意:
比如python字符串s = 'ABC\\-001' 对应的正则表达式变成'ABC\-001'
所以最好把python字符串上加上r前缀,就不用考虑转义的问题,比如s = r'ABC\-001' # Python的字符串

如何判断正则表达式是否匹配:

  • 引入re模块: import re
  • 使用match方法,如果匹配成功,返回一个Match对象,否则返回None
    test = '用户输入的字符串'
if re.match(r'正则表达式', test):
    print('ok')
else:
    print('failed')

切分字符串

使用正则表达式后,切分字符变得更灵活。

如下使用split 的正常切分代码,可以看出无法识别连续的空格

>>> 'a b   c'.split(' ')
['a', 'b', '', '', 'c']

使用正则表达式可以实现更复杂的切分:

>>> re.split(r'[\s\,\;]+', 'a,b;; c  d')
['a', 'b', 'c', 'd']

分组

除了判断是否匹配之外,正则表达式可以提取子串的强大功能。用()表示的就是要提取的分组(Group)。
比如

m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')

这个正则表达式定义了两个分组,可以匹配-前后的两个表达式。

  • m.group(0):获得的是'010-12345'
  • m.group(1):获得是“010”
  • m.group(2):获得是'12345'

group(0)永远是原始字符串,group(1)、group(2)……表示第1、2、……个子串。

贪婪匹配

正则表达式默认就是贪婪匹配的。比如

>>> re.match(r'^(\d+)(0*)$', '102300').groups()
#结果是('102300', ''),\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了

必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配:

>>> re.match(r'^(\d+?)(0*)$', '102300').groups()
('1023', '00')

再比如

import re 
line = "boooooobby123";
reg_str = ".*(b.*b).*";
match_obj = re.match (reg_str , line);
if match_obj:
    print (match_obj.group(1));

因为.*是贪婪匹配的,所以它会一直匹配到booooooboooooo,那么小括号里面实际只匹配了bb

正则

如果使用非贪婪模式,也就是在.*后面加一个?

import re 
line = "boooooobby123";
reg_str = ".*?(b.*?b).*";
match_obj = re.match (reg_str , line);
if match_obj:
    print (match_obj.group(1)); 

image.png

例子:提取日期

下面我们希望能自动化的把一段文字中的生日给提取出来,但是如果之前没有规定格式的话,大家会随心所欲的写日期,比如

  • 出生于2018年1月23日
  • 出生于2018/1/23
  • 出生于2018-1-23
  • 出生于2018-01-23
  • 出生于2018-01
  • 出生于2018年01月

下面我们需要给一个正则表达式,要求他能匹配上面所有的日期格式。

  • 首先匹配日期中的年的部分,从上面的文本可以看出,只有2018年2018-
    2018/这几种形式。也就是可以先用\d{4}表示数字,再用[年-\]来表示符号。凑起来就是
regex = r"出生于(\d{4}[年/-])"
  • 再来看月份的数字部分只可能有011两种形式:\d{1,2}
  • 月份后面的部分就相对比较复杂了。同样的,我们可以进行分类列举,然后使用分支条件即可统一表达。
    • 匹配2018年1月23日2018-01-23以及2018/1/23后面的部分:[月/-]\d{1,2}日?
    • 匹配2018年01月这种的后面的部分:[月/-]$
    • 匹配2018-01后面的部分,当然是直接用结尾符:$
    • 最后用()括起来,使用|进行分类讨论。
([月/-]\d{1,2}日?|[月/-]$|$)

最后把所有的部分合并起来。

import re 
lines = [
 "出生于2018年1月23日",
 "出生于2018/1/23",
 "出生于2018-1-23",
 "出生于2018-01-23",
 "出生于2018-01",
 "出生于2018年01月"]
regex = r"出生于(\d{4}[年/-]\d{1,2}([月/-]\d{1,2}日?|[月/-]$|$))"
for line in lines :
    m = re.match(regex  , line )
    if m :
        print(m.group(1));

image.png

编译

使用正则表达式时,re模块内部会干两件事情:

  • 编译正则表达式,此时会进行语法分析,如果表达式本身不合法,会报错;
  • 用编译后的正则表达式去匹配字符串。
    那么如果一个正则表达式要使用非常多次,可以预编译该正则表达式
# 编译:
>>> re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')
# 使用:
>>> re_telephone.match('010-12345').groups()
('010', '12345')

参考

廖雪峰-正则表达式
正则表达式30分钟入门教程

推荐阅读