dy2903

为什么要用正则表达式

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

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

所以比如我们要判断一串字符是否为合法的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分钟入门教程

分类:

技术点:

相关文章: