返回介绍

4.2 正则表达式

发布于 2024-01-26 22:39:51 字数 14264 浏览 0 评论 0 收藏 0

在编写处理网页文本的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。正则表达式是由普通字符(例如字符a到z)以及特殊字符(称为“元字符”)组成的文字模式。模式用于描述在搜索文本时要匹配的一个或多个字符串。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。

4.2.1 基本语法与使用

正则表达式功能非常强大,但是学好并不是很困难。一些初学者总是感觉到正则表达式很抽象,看到稍微长的表达式直接选择放弃。接下来从一个新手的角度,由浅及深,配合各种示例来讲解正则表达式的用法。

1.入门小例子

学习正则表达式最好的办法就是通过例子。在不断解决问题的过程中,就会不断理解正则表达式构造方法的灵活多变。

例如我们想找到一篇英文文献中所有的we单词,你可以使用正则表达式:we,这是最简单的正则表达式,可以精确匹配英文文献中的we单词。正则表达式工具一般可以设置为忽略大小写,那we这个正则表达式可以将文献中的We、wE、we和WE都匹配出来。如果仅仅使用we来匹配,会发现得出来的结果和预想的不一样,类似于well、welcome这样的单词也会被匹配出来,因为这些单词中也包含we。如何仅仅将we单词匹配出来呢?我们需要使用这样的正则表达式:\bwe\b。

“\b”是正则表达式规定的一个特殊代码,被称为元字符,代表着单词的开头或结尾,也就是单词的分界处,它不代表英语中空格、标点符号、换行等单词分隔符,只是用来匹配一个位置,这种理解方式很关键。

假如我们看到we单词不远处有一个work单词,想把we、work和它们之间的所有内容都匹配出来,那么我们需要了解另外两个元字符“.”和“*”,正则表达式可以写为\bwe\b.*\bwork\b。“.”这个元字符的含义是匹配除了换行符的任意字符,“*”元字符不是代表字符,而是代表数量,含义是“*”前面的内容可以连续重复任意次使得整个表达式被匹配。“.*”整体的意思就非常明显了,表示可以匹配任意数量不换行的字符,那么\bwe\b.*\bwork\b作用就是先匹配出we单词,接着再匹配任意的字符(非换行),直到匹配到work单词结束。通过上面的例子,我们看到元字符在正则表达式中非常关键,元字符的组合能构造出强大的功能。

接下来咱们开始讲解常用的元字符,在讲解之前,需要介绍一个正则表达式的测试工具Match Tracer,这个工具可以将写的正则表达式生成树状结构,描述并高亮每一部分的语法,同时可以检验正则表达式写的是否正确,如图4-23所示。

2.常用元字符

元字符主要有四种作用:有的用来匹配字符,有的用来匹配位置,有的用来匹配数量,有的用来匹配模式。在上面的例子中,我们讲到了“.”“*”这两个元字符,还有其他元字符,如表4-1所示。

图4-23 Match Tracer

表4-1 常见元字符

上面的元字符是用来匹配字符和位置的,接下来讲解其他功能时,会依次列出匹配数量和模式的元字符。下面对上面列出的元字符使用一些小例子来进行一下练习。

假如一行文本为:we are still studying and so busy,我们想匹配出所有以s开头的单词,那么正则表达式可以写为:\bs\w*\b。\bs\w*\b的匹配顺序:先是某个单词开始处(\b),然后是字母s,然后是任意数量的字母或数字(\w*),最后是单词结束处(\b)。同理,如果匹配s100这样的字符串(不是单词),需要用到“^”和“$”,一个匹配开头,一个匹配结束,可以写为^s\d*$。

3.字符转义

如果你想查找元字符本身的话,比如你查找“.”或者“*”就会出现问题,因为它们具有特定功能,没办法把它们指定为普通字符。这个时候就需要用到转义,使用“\”来取消这些字符的特殊意义。因此如果查找“.”、“\”或者“*”时,必须写成“\.”、“\\”和“\*”。例如匹配www.google.com这个网址时,可以表达式可以写为www\.google\.com。

4.重复

首先列举出匹配重复的限定符(指定数量的代码),如表4-2所示。

表4-2 常用限定符

下面是一些重复的例子:

·hello\d+:匹配hello后面跟1个或更多数字,例如可以匹配hello1、hello10等情况。

·^\d{5,12}$:匹配5到12个数字的字符串,例如QQ号符合要求。

·we\d:匹配we后面跟0个或者一个数字,例如we、we0符合情况。

5.字符集合

通过上面介绍的元字符,可以看到查找数字、字母或数字、空格是很简单的,因为已经有了对应这些字符的集合,但是如果想匹配没有预定义元字符的字符集合,例如匹配a、b、c、d和e中任意一个字符,这时候就需要自定义字符集合。正则表达式是通过[]来实现自定义字符集合,[abcde]就是匹配abcde中的任意一个字符,[.!]匹配标点符号(“.”、“”或“!”)。

除了将需要自定义的字符都写入[]中,还可以指定一个字符范围。[0-9]代表的含义与“\d”是完全一致的,代表一位数字;[a-z0-9A-Z_]也完全等同于“\w”(只考虑英文),代表着26个字母中的大小写、0~9的数字和下划线中的任一个字符。

6.分支条件

正则表达式里的分支条件指的是有几种匹配规则,如果满足其中任意一种规则都应该当成匹配,具体方法是用“|”把不同的规则分隔开。例如匹配电话号码,电话号码中一种是3位区号,8位本地号,形如010-11223344,另一种是4位区号,7位本地号,形如0321-1234567。如果想把电话号码匹配出来,就需要用到分支条件:0\d{2}-\d{8}|0\d{3}-\d{7}。在分支条件中有一点需要注意,匹配分支条件时,将会从左到右地测试每个条件,如果满足了某个分支的话,就不会去再管其他条件了,条件之间是一种或的关系,例如从1234567890匹配出连续的4个数字或者连续8个数字,如果写成\d{4}|\d{8},其实\d{8}是失效的,既然能匹配出来8位数字,肯定就能匹配出4位数字。

7.分组

先以简单的IP地址匹配为例子,想匹配类似192.168.1.1这样的IP地址,可以这样写正则表达式((\d{1,3})\.){3}\d{1,3}。下面分析一下这个正则表达式:\d{1,3}代表着1~3位的数字,((\d{1,3})\.){3}代表着将1~3位数字加上一个“.”重复3次,匹配出类似192.168.1.这部分,之后再加上\d{1,3},表示1~3位的数字。但是上述的正则表达式会匹配出类似333.444.555.666这些不可能存在的IP地址,因为IP地址中每个数字都不能大于255,所以要写出一个完整的IP地址匹配表达式,还需要关注一下细节,下面给出一个使用分组的完整IP表达式:((25[0-5]|2[0-4]\d|[0-1]\d{2}|[1-9]\d)\.){3}((25[0-5]|2[0-4]\d|[0-1]\d{2}|[1-9]\d))。其中的关键是(25[0-5]|2[0-4]\d|[0-1]\d{2}|[1-9]\d)部分,大家应该有能力分析出来。

8.反义

有时需要查找除某一类字符集合之外的字符。比如想查找除了数字以外,包含其他任意字符的情况,这时就需要用到反义,如表4-3所示。

表4-3 常用的反义

例如“\D+”匹配非数字的一个或者多个字符。

9.后向引用

前面我们讲到了分组,使用小括号指定一个表达式就可以看做是一个分组。默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。还是以简单的IP匹配表达式((\d{1,3})\.){3}\d{1,3}为例,这里面有两个分组1和2,使用Match Tracer这个工具可以很明显地看出来,如图4-24所示。

图4-24 捕获组

所以上面的表达式可以改写成((\d{1,3})\.){3}\2。

你也可以自己指定子表达式的组名。要指定一个子表达式的组名,使用这样的语法:(<Digit>\d+)或者(‘Digit’\d+)),这样就把“\d+”的组名指定为Digit了。要反向引用这个分组捕获的内容,你可以使用\k<Digit>,所以上面的IP匹配表达式写成((<Digit>\d{1,3})\.){3}\k<Digit>。使用小括号的地方很多,主要是用来分组,表4-4中列出了一些常用的形式。

表4-4 常用分组形式

在捕获这个表项里,我们讲解了前两种用法,还有(?:exp)没有进行讲解。(?:exp)不会改变正则表达式的处理方式,只是这样的组所匹配的内容不会像前两种那样被捕获到某个组里面,也不会拥有组号,这样做有什么意义?一般来说是为了节省资源,提高效率。比如说验证输入是否为整数,可以这样写^([1-9][0-9]*|0)$。这时候我们需要用到“()”来限制“|”表示“或”关系的范围,但我们只是要判断规则,没必要把exp匹配的内容保存到组里,这时就可以用非捕获组了^(:[1-9][0-9]*|0)$。

10.零宽断言

在表4-4中,零宽断言总共有四种形式。前两种是正向零宽断言,后两种是负向零宽断言。什么是零宽断言呢?我们知道元字符“\b”、“^”匹配的是一个位置,而且这个位置需要满足一定的条件,我们把这个条件称为断言或零宽度断言。断言用来声明一个应该为真的事实,正则表达式中只有当断言为真时才会继续进行匹配。可能大家感到有些抽象,下面通过一些例子进行讲解。

首先说一下正向零宽断言的两种形式:

·(?=exp)叫零宽度正预测先行断言,它断言此位置的后面能匹配表达式exp。比如[a-z]*(?=exp)匹配以ing结尾的单词的前面部分(除了ing以外的部分),查找I love cooking and singing时会匹配出中的cook与sing。先行断言的执行步骤应该是从要匹配字符的最右端找到第一个“ing”,再匹配前面的表达式,如无法匹配则查找第二个“ing”。

·(?=exp)叫零宽度正回顾后发断言,它断言此位置的前面能匹配表达式exp。比如(?<=abc).*匹配以abc开头的字符串的后面部分,可以匹配abcdefgabc中的defgabc而不是abcdefg。通过比较很容易看出后发断言和先行断言正好相反:它先从要匹配的字符串的最左端开始查找断言表达式,之后再匹配后面的字符串,如果无法匹配则继续查找第二个断言表达式,如此反复。

再说一下负向零宽断言的两种形式:

·(?!exp)叫零宽度负预测先行断言,断言此位置的后面不能匹配表达式exp。比如\b((?!abc)\w)+\b匹配不包含连续字符串abc的单词,查找“abc123,ade123”这个字符串,可以匹配出ade123,可以使用Match Tracer进行查看分析。

·(?!exp)叫零宽度负回顾后发断言,断言此位置的前面不能匹配表达式exp。比如(?<![a-z])\d{7}匹配前面不是小写字母的七位数字。还有一个复杂的例子:(?<=<(\w+)>).*(?=<\/\1>),用于匹配不包含属性的简单HTML标记内的内容。该表达式可以从

python爬虫

中提取出“python爬虫”,这在Python爬虫开发中常用到。大家可以思考一下是如何提取出包含属性的HTML标记内的内容。

11.注释

正则表达式可以包含注释进行解释说明,通过语法(#comment)来实现,例如\b\w+(#字符串)\b。要包含注释的话,最好是启用“忽略模式里的空白符”选项,这样在编写表达式时能任意地添加空格、Tab、换行,而实际使用时这些都将被忽略。

12.贪婪与懒惰

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符,这就是贪婪模式。以表达式a\w+b为例,如果搜索a12b34b,会尽可能匹配更多的个数,最后就会匹配整个a12b34b,而不是a12b。但是如果想匹配出a12b怎么办呢?这时候就需要使用懒惰模式,尽可能匹配个数较少的情况,因此需要将上面的a\w+b表达式改为a\w+b,使用“”来启用懒惰模式。表4-5列举了懒惰限定符的使用方式。

表4-5 懒惰限定符的使用方式

13.处理选项

一般正则表达式的实现库都提供了用来改变正则表达式处理选项的方式,表4-6提供了常用的处理选项。

表4-6 常用的处理选项

正则表达式中还有平衡组/递归匹配的概念,对于初学者来说,一般用不了这么复杂,此处不进行讲解。

4.2.2 Python与正则

上一节讲解了正则表达式的语法和应用,对于不同的编程语言来说,对正则表达式的语法绝大部分语言都是支持的,但是还是略有不同,每种编程语言都有一些独特的匹配规则,Python也不例外。下面通过表4-7列出一些Python的匹配规则。

表4-7 Python的匹配规则

在讲Python对正则表达式的实现之前,首先让说一下反斜杠问题。正则表达式里使用“\”作为转义字符,这就可能造成反斜杠困扰。假如你需要匹配文本中的字符“\”,那么使用编程语言表示的正则表达式里将需要4个反斜杠“\\\\”:前两个和后两个分别用于在编程语言里转义成反斜杠,转换成两个反斜杠后再在正则表达式里转义成一个反斜杠。但是Python提供了对原生字符串的支持,从而解决了这个问题。匹配一个‘\’的正则表达式可以写为r‘\\’,同样,匹配一个数字的‘\\d’可以写成r‘\d’,

Python通过re模块提供对正则表达式的支持。使用re的一般步骤是先将正则表达式的字符串形式编译为Pattern实例,然后使用Pattern实例处理文本并获得匹配结果,最后使用Match实例获得信息,进行其他操作。主要用到的方法列举如下:

·re.compile(string[,flag])

·re.match(pattern,string[,flags])

·re.search(pattern,string[,flags])

·re.split(pattern,string[,maxsplit])

·re.findall(pattern,string[,flags])

·re.finditer(pattern,string[,flags])

·re.sub(pattern,repl,string[,count])

·re.subn(pattern,repl,string[,count])

首先说一下re中compile函数,它将一个正则表达式的字符串转化为Pattern匹配对象。示例如下:

  pattern = re.compile(r'\d+')

这会生成一个匹配数字的pattern对象,用来给接下来的函数作为参数,进行进一步的搜索操作。

大家发现其他几个函数中,还有一个flag参数。参数flag是匹配模式,取值可以使用按位或运算符“|”表示同时生效,比如re.I|re.M。flag的可选值如下:

·re.I:忽略大小写。

·re.M:多行模式,改变“^”和“$”的行为。

·re.S:点任意匹配模式,改变“.”的行为。

·re.L:使预定字符类\w\W\b\B\s\S取决于当前区域设定。

·re.U:使预定字符类\w\W\b\B\s\S\d\D取决于unicode定义的字符属性。

·re.X:详细模式。这个模式下正则表达式可以是多行,忽略空白字符,并可以加入注释。

1.re.match(pattern,string[,flags])

这个函数是从输入参数string(匹配的字符串)的开头开始,尝试匹配pattern,一直向后匹配,如果遇到无法匹配的字符或者已经到达string的末尾,立即返回None,反之获取匹配的结果。示例如下:

  # coding:utf-8
  import re
  # 将正则表达式编译成pattern对象
  pattern = re.compile(r'\d+')
  # 使用re.match匹配文本,获得匹配结果,无法匹配时将返回None
  result1 = re.match(pattern,'192abc')
  if result1:
     print result1.group()
  else:
     print '匹配失败1'
  result2 = re.match(pattern,'abc192')
  if result2:
     print result2.group()
  else:
     print '匹配失败2'

运行结果如下:

  192
  匹配失败2

匹配192abc字符串时,match函数是从字符串开头进行匹配,匹配到192立即返回值,通过group()可以获取捕获的值。同样,匹配abc192字符串时,字符串开头不符合正则表达式,立即返回None。

2.re.search(pattern,string[,flags])

search方法与match方法极其类似,区别在于match()函数只从string的开始位置匹配,search()会扫描整个string查找匹配,match()只有在string起始位置匹配成功的时候才有返回,如果不是开始位置匹配成功的话,match()就返回None。search方法的返回对象和match()返回对象在方法和属性上是一致的。示例如下:

  import re
  # 将正则表达式编译成pattern对象
  pattern = re.compile(r'\d+')
  # 使用re.match匹配文本获得匹配结果;无法匹配时将返回None
  result1 = re.search(pattern,'abc192edf')
  if result1:
     print result1.group()
  else:
     print '匹配失败1'

输出结果为:

  192

3. re.split(pattern, string[, maxsplit])

按照能够匹配的子串将string分割后返回列表。maxsplit用于指定最大分割次数,不指定,则将全部分割。示例如下:

  import re
  pattern = re.compile(r'\d+')
  print re.split(pattern,'A1B2C3D4')

输出结果为:

  ['A', 'B', 'C', 'D', '']

4.re.findall(pattern,string[,flags])

搜索整个string,以列表形式返回能匹配的全部子串。示例如下:

  import re
  pattern = re.compile(r'\d+')
  print re.findall(pattern,'A1B2C3D4')

输出结果为:

  ['1', '2', '3', '4']

5.re.finditer(pattern,string[,flags])

搜索整个string,以迭代器形式返回能匹配的全部Match对象。示例如下:

  import re
  pattern = re.compile(r'\d+')
  matchiter = re.finditer(pattern,'A1B2C3D4')
  for match in matchiter:
     print match.group()

输出结果为:

  1
  2
  3
  4

6.re.sub(pattern,repl,string[,count])

使用repl替换string中每一个匹配的子串后返回替换后的字符串。当repl是一个字符串时,可以使用\id或\g<id>、\g<name>引用分组,但不能使用编号0。当repl是一个方法时,这个方法应当只接受一个参数(Match对象),并返回一个字符串用于替换(返回的字符串中不能再引用分组)。count用于指定最多替换次数,不指定时全部替换。示例如下:

  import re
  p = re.compile(r'(P<word1>\w+) (P<word2>\w+)')# 使用名称引用
  s = 'i say, hello world!'
  print p.sub(r'\g<word2> \g<word1>', s)
  p = re.compile(r'(\w+) (\w+)')# 使用编号
  print p.sub(r'\2 \1', s)
  def func(m):
     return m.group(1).title() + ' ' + m.group(2).title()
  print p.sub(func, s)

输出结果为:

  say i, world hello!
  say i, world hello!
  I Say, Hello World!

7.re.subn(pattern,repl,string[,count])

返回(sub(repl,string[,count]),替换次数)。示例如下:

  import re
  s = 'i say, hello world!'
  p = re.compile(r'(\w+) (\w+)')
  print p.subn(r'\2 \1', s)
  def func(m):
     return m.group(1).title() + ' ' + m.group(2).title()
  print p.subn(func, s)

输出结果为:

  ('say i, world hello!', 2)
  ('I Say, Hello World!', 2)

以上7个函数在re模块中进行搜索匹配,如何将捕获到的值提取出来呢?这就需要用到Match对象,之前已经使用了Match中的groups方法,现在介绍一下Match对象的属性和方法。

Match对象的属性:

·string:匹配时使用的文本。

·re:匹配时使用的Pattern对象。

·pos:文本中正则表达式开始搜索的索引。值与Pattern.match()和Pattern.search()方法的同名参数相同。

·endpos:文本中正则表达式结束搜索的索引。值与Pattern.match()和Pattern.search()方法的同名参数相同。

·lastindex:最后一个被捕获的分组在文本中的索引。如果没有被捕获的分组,将为None。

·lastgroup:最后一个被捕获的分组的别名。如果这个分组没有别名或者没有被捕获的分组,将为None。

·Match对象的方法:

·group([group1,…]):获得一个或多个分组截获的字符串,指定多个参数时将以元组形式返回。group1可以使用编号也可以使用别名,编号0代表整个匹配的子串,不填写参数时,返回group(0)。没有截获字符串的组返回None,截获了多次的组返回最后一次截获的子串。

·groups([default]):以元组形式返回全部分组截获的字符串。相当于调用group(1,2,…last)。default表示没有截获字符串的组以这个值替代,默认为None。

·groupdict([default]):返回以有别名的组的别名为键、以该组截获的子串为值的字典,没有别名的组不包含在内。default含义同上。

·start([group]):返回指定的组截获的子串在string中的起始索引(子串第一个字符的索引)。group默认值为0。

·end([group]):返回指定的组截获的子串在string中的结束索引(子串最后一个字符的索引+1)。group默认值为0。

·span([group]):返回(start(group),end(group))。

·expand(template):将匹配到的分组代入template中然后返回。template中可以使用\id或\g<id>、\g<name>引用分组,但不能使用编号0。\id与\g<id>是等价的,但\10将被认为是第10个分组,如果你想表达\1之后是字符‘0’,只能使用\g<1>0。

示例如下:

  import re
  pattern = re.compile(r'(\w+) (\w+) (P<word>.*)')
  match = pattern.match( 'I love you!')
  
  print "match.string:", match.string
  print "match.re:", match.re
  print "match.pos:", match.pos
  print "match.endpos:", match.endpos
  print "match.lastindex:", match.lastindex
  print "match.lastgroup:", match.lastgroup
  
  print "match.group(1,2):", match.group(1, 2)
  print "match.groups():", match.groups()
  print "match.groupdict():", match.groupdict()
  print "match.start(2):", match.start(2)
  print "match.end(2):", match.end(2)
  print "match.span(2):", match.span(2)
  print r"match.expand(r'\2 \1 \3'):", match.expand(r'\2 \1 \3')

输出结果:

  match.string: I love you!
  match.re: <_sre.SRE_Pattern object at 0x003F47A0>
  match.pos: 0
  match.endpos: 11
  match.lastindex: 3
  match.lastgroup: word
  match.group(1,2): ('I', 'love')
  match.groups(): ('I', 'love', 'you!')
  match.groupdict(): {'word': 'you!'}
  match.start(2): 2
  match.end(2): 6
  match.span(2): (2, 6)
  match.expand(r'\2 \1 \3'): love I you!

前文介绍的7种方法的调用方式大都是re.match、re.search之类,其实还可以使用由re.compile方法产生的Pattern对象直接调用这些函数,类似pattern.match,pattern.search,只不过不用将Pattern作为第一个参数传入。函数对比如表4-8所示。

表4-8 函数调用方式

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文