Skip to content

Regex | 正则表达式

约 3095 个字 256 行代码 预计阅读时间 15 分钟

正则表达式介绍

正则表达式是一个面向字符串处理的工具,利用正则表达式的相关工具,我们可以实现字符串的检索、替换和匹配验证。

这意味着我们有了一个强有力的工具来处理爬虫获得的HTML信息。

一个正则表达式长什么样?

# 这是一个匹配类似URL文本的正则表达式
[a-zA-z]+://[^\s]*

看起来毫无头绪?我们拆开来,举出里面的几个来讲!

  • a-z代表匹配任意小写字母
  • \s匹配任意的空白字符
  • *匹配前面任意多个字符

以上所有的匹配规则共同构成了正则表达式。

以下列出常用的匹配规则:

模 式
描 述
\w 匹配字母、数字及下划线
\W 匹配不是字母、数字及下划线的字符
\s 匹配任意空白字符,等价于 [\t\n\r\f]
\S 匹配任意非空字符
\d 匹配任意数字,等价于 [0-9]
\D 匹配任意非数字的字符
\A 匹配字符串开头
\Z 匹配字符串结尾,如果存在换行,只匹配到换行前的结束字符串
\z 匹配字符串结尾,如果存在换行,同时还会匹配换行符
\G 匹配最后匹配完成的位置
\n 匹配一个换行符
\t 匹配一个制表符
^ 匹配一行字符串的开头
$ 匹配一行字符串的结尾
. 匹配任意字符,除了换行符,当 re.DOTALL 标记被指定时,则可以匹配包括换行符的任意字符
[...] 用来表示一组字符,单独列出,比如 [amk] 匹配a、m或k
[^...] 不在[]中的字符,比如 [^abc] 匹配除了a、b、c之外的字符
* 匹配0个或多个表达式
+ 匹配1个或多个表达式
? 匹配0个或1个前面的正则表达式定义的片段,非贪婪方式
{n} 精确匹配n个前面的表达式
{n, m} 匹配n到m次由前面正则表达式定义的片段,贪婪方式
a\|b 匹配a或b
( ) 匹配括号内的表达式,也表示一个组

感觉太多了?不try不知道,一try吓一跳,这些匹配规则都会在之后的讲解中运用到,慢慢就熟悉了。

应该说,很多编程语言都支持正则表达式,但是Python的re库提供了完整的正则表达式的实现。

Python,我该如何赞美你!

match()

简单介绍

让我们来看看第一个常用的匹配方法match(),以下是它的定义:

def match(pattern, string, flags=0):
  • pattern用于传入正则表达式
  • string用于传入待匹配的字符串
  • flags用于指定match()的模式,比如忽视大小写等

matchu()函数的作用是将传入的正则表达式从待匹配的字符串的开头开始进行比较,如果发现匹配正则表达式的部分,则返回匹配成功的成果,如果没有则返回None

下举一实例说明:

import re

content = 'Hello 123 4567 World_This is a Regex Demo'
print(len(content))
result = re.match('^Hello\s\d\d\d\s\d{4}\s\w{10}', content)
print(result)
print(result.group())
print(result.span())

# 输出为
'''
41
<re.Match object; span=(0, 25), match='Hello 123 4567 World_This'>
Hello 123 4567 World_This
(0, 25)
'''

让我们来详细解释一下上面代码的意思!

首先我们声明了一个字符串'Hello 123 4567 World_This is a Regex Demo',其中包含了英文字母,空白字符和数字等。

接下来我们设定了一个正则表达式:

^Hello\s\d\d\d\s\d{4}\s\w{10}

这个正则表达式是什么意思呢,让我们结合前面的表格来看看:

  • ^Hello匹配以"Hello"开头的字符串
  • \s匹配空格
  • \d匹配数字,如果采用\d{4}的写法,则表明匹配连续的4个数字
  • \w匹配字母,数字和下划线,\w{10}代表匹配连续10个的字母,数字和下划线

正则表达式本质上是一个从前往后顺序执行的过程,依次按照规则进行匹配,如果一个进行匹配的字符串满足所有条件,那么这就是一个符合我们设定正则规则的字符串,match()会把它返回给我们。

从打印结果来看,match()返回的是一个Match对象,其内包含两个方法:span()group(),前者可以输出匹配的范围,本例中是(0, 25),后者可以输出匹配到的内容,本例中是Hello 123 4567 World_This

匹配目标

如果我们想要单独获得匹配内容的一小部分该怎么办呢?单独写另一个match()

天哪这太麻烦了!不是Python的风格!

match()中,我们可以在传入的正则表达式中添加适当的括号,来标记我们想要提取的部分,这个在括号里的部分会被当做一个子表达式,之后我们可以用group()方法进行提取,实例如下:

import re

content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^Hello\s(\d+)\s(\w{6})', content)
print(result)
print(result.group())
print(result.group(1))
print(result.group(2))

# 输出为
'''
<re.Match object; span=(0, 20), match='Hello 1234567 World_'>
Hello 1234567 World_
1234567
World_
'''

通过向group()传入索引参数,我们可以分别提取出已经分好组的匹配内容。

通用匹配

刚才我们写的正则表达式其实比较复杂,基本上是“一个萝卜一个坑”的思路,写起来又费劲又容易出纰漏,这显然太不优雅了!

幸好,在正则表达式中提供了万能匹配,分别是.*(点星),其中.(点)可以匹配除了换行符以外的所有字符,*(星)代表匹配前面的字符无限次,两个组合在一起就可以匹配任意字符了。

字符串:我不知道啊!它们喊着什么友情羁绊之类听不懂的话就冲上来了。

下举一实例说明:

import re

content = 'Hello 123 4567 World_This is a Regex Demo'
result = re.match('^Hello.*Demo$', content)
print(result)
print(result.span())
print(result.group())

# 输出为
'''
<re.Match object; span=(0, 41), match='Hello 123 4567 World_This is a Regex Demo'>
(0, 41)
Hello 123 4567 World_This is a Regex Demo
'''

无所谓,强大的.*会出手。我们直接轻松匹配了整个字符串,所需要做的只有^Hello匹配开头,Demo$匹配结尾。

贪婪与非贪婪

使用上面的通用匹配.*有时候会出意外,让我们看下面的例子:

import re

content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*(\d+).*Demo$', content)
print(result)
print(result.group(1))

# 输出为
'''
<re.Match object; span=(0, 40), match='Hello 1234567 World_This is a Regex Demo'>
7
'''

在我们的设想中,用^He匹配开头,.*匹配前面的内容,(\d+)提取我们需要的数字内容,.*匹配后面的内容,Demo$匹配结尾。

但是最后只剩了一个……7?

这里涉及到了一个贪婪/非贪婪匹配的问题,表达式中.*在贪婪匹配下会尽可能匹配更多的字符,也就是说尽管后面有一个\d+,但是+可没说是一个还是几个,于是.*会毫不留情地尽可能多地匹配,最终只剩下一个7给\b+

假如我们稍作修改:

result = re.match('^He.*?(\d+).*Demo$', content)

.*?为非贪婪匹配,也就是他会尽可能少地匹配,匹配到Hello后的空格后发现后面是一个数字1,就会把后面的匹配交给\d+来处理。

Tips:
在匹配时候,字符串中间应该尽可能使用非贪婪匹配,以免造成匹配结果缺失的情况。

但是注意的是,如果提取的结果在末尾,又在这里使用.*?,就有可能匹配不到任何内容,因为对于.*?来说,它尽可能匹配更少的字符,举一实例如下:

import re

content = 'http://weibo.com/comment/kEraCN'
result_1 = re.match('http.*?comment/(.*?)', content)
result_2 = re.match('http.*?comment/(.*)', content)
print('result_1=', result_1.group(1))
print('result_2=', result_2.group(1))

# 输出为
'''
result_1= 
result_2= kEraCN
'''

在这个实例中,位于最后的.*?背后没有其他匹配表达式了,它也不知道要读到哪里,干脆就不读了,所以返回是空的。

修饰符

正则表达式可以包含一些可选标志修饰符来控制匹配的模式,下举一个实例:

import re

content = '''Hello 1234567 World_This
is a Regex Demo'''
result = re.match('^He.*?(\d+).*?Demo$', content)
print(result)
print(result.group(1))

# 输出为
'''
None
Traceback (most recent call last):
  File "/home/yangshu233/python projects/crawler/temp/temp_regex/temp_match.py", line 48, in <module>
    print(result.group(1))
          ^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'group'
'''

可见原先可以的匹配方法在这里失效了,match()返回了一个None,此时我们再调用group()方法,会抛出AttributeError

如果我们稍作修改:

result = re.match('^He.*?(\d+).*?Demo$', content, flags=re.S)

此时我们给match()flags参数传入了一个re.S(其等价于re.DOTALL),采用.可匹配换行符的模式。这下表达式就可以匹配这个字符串并按需要提取了:

# 输出为
'''
<re.Match object; span=(0, 40), match='Hello 1234567 World_This\nis a Regex Demo'>
1234567
'''

还有其他的修饰符,列表如下:

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^$
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据 Unicode 字符集解析字符。这个标志影响 \w\W\b\B
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解

在网页匹配中,较为常用的有re.Sre.I

转义匹配

如果要匹配的目标字符串包含了诸如.等关键字该怎么办?

听起来似乎很容易想到,用转义字符\

举一实例如下:

import re

content = '(百度)www.baidu.com'
result = re.match('\(百度\)www\.baidu\.com', content)
print(result)

# 输出为
'''
<re.Match object; span=(0, 17), match='(百度)www.baidu.com'>
'''

方法介绍

对于mathc()来说,给它匹配的字符串如果开头就不符合规定的正则表达式会直接匹配失败,返回None

因此采用match()来提取字符串不是很方便,它更适合检查某一个字符串是否符合某种规则。

re库提供了另外一种方法:search(),它会扫描整个字符串,直到找到第一个匹配的部分并返回,如果扫描完整个字符串都没找到匹配的部分,则返回None

search()的定义如下:

def search(pattern, string, flags=0)

match()如出一辙,同样的,对于search()返回的结果,同样有span()group()两种方法。下举一实例说明:

import re

content = 'Extra strings Hello world 1234567 World_This is a Regex Demo Extra strings'
result = re.search('Hello.*?(\d+).*?Demo', content)
print(result)
print(result.span())
print(result.group())
print(result.group(1))

# 输出为
'''
<re.Match object; span=(14, 60), match='Hello world 1234567 World_This is a Regex Demo'>
(14, 60)
Hello world 1234567 World_This is a Regex Demo
1234567
'''

在上面的实例中,如果使用match()方法,会返回None

实际使用

下面我们通过几具体的例子来看看search()的使用方法:

# 待提取的文本
'''
<div id="songs-list">
<h2 class="title">经典老歌</h2>
<p class="introduction">
经典老歌列表
</p>
<ul id="list" class="list-group">
<li data-view="2">一路上有你</li>
<li data-view="7">
<a href="/2.mp3" singer="任贤齐">沧海一声笑</a>
</li>
<li data-view="4" class="active">
<a href="/3.mp3" singer="齐秦">往事随风</a>
</li>
<li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
<li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
<li data-view="5">
<a href="/6.mp3" singer="邓丽君">但愿人长久</a>
</li>
</ul>
</div>
'''

要用正则表达式提取字符串中的信息,首先要分析文本的结构:

  • 文本的核心内容都在<ul>标签内
  • <ul>内含有多个<li>标签
  • 每个<li>的内容包含有歌曲名字
  • <li>的属性不尽相同,但可以分成data-viewclasssinger几类
  • 有些<li>标签内含有<a>标签,其属性href包含超链接内容

第一个任务,我们先尝试筛选出带有class="active"属性的<li>标签,提取其中的歌手名与歌名:

import re

content = '''
<div id="songs-list">
<h2 class="title">经典老歌...
'''

# 匹配相应的部分,并且将我们要提取的部分用()圈起来
pattern = '<li.*?class="active".*?singer="(.*?)">(.*?)</a>'

# 由于文本有换行,需要设置flags=re.S
result = re.search(pattern=pattern, string=content, flags=re.S)
print(result.group(1), result.group(2))

# 输出为
'''
齐秦 往事随风
'''

如果我们稍作修改:

pattern = '<li.*?singer="(.*?)">(.*?)</a>'

这里我们删除了正则表达式中class="active"的匹配条件,程序的输出为:

# 输出为
'''
任贤齐 沧海一声笑
'''

应该注意,search()会返回第一个符合匹配的字符串部分。

我们这次再做修改:

pattern = '<li.*?singer="(.*?)">(.*?)</a>'
result = re.search(pattern=pattern, string=content)

这里我们不仅去除了正则表达式中的class="active",还删除了search()中传入的flags=re.S,使得.*?无法匹配换行,则输出为:

beyond 光辉岁月

原文本中仅有第四项<li><a>未换行,故search()返回了此项<li>中的指定内容。

这里我们必须再次强调,由于可读性考虑,大多数HTML文本都包含有换行符,所以在使用search()都尽量加上re.S,以免出现差错。

findall()

前文中search()已经很方便了,但是还不够!这个方法仅能返回获取到的第一个符合匹配的内容,如果我们想要获取所有符合匹配的内容呢?

去吧,findall()

函数如其名,findall()会检索整个文本,然后以列表形式返回所有符合匹配的文本,如果没有符合匹配的内容,则返回空列表[]

下举一实例:

import re

content = '''
<div id="songs-list">
<h2 class="title">经典老歌...
'''
results = re.findall('<li.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>', content, flags=re.S)
print(results)
print(type(results))
for result in results:
    print(result)
    print(result[0], result[1], result[2])

# 输出为
'''
[('/2.mp3', '任贤齐', '沧海一声笑'), ('/3.mp3', '齐秦', '往事随风'), ('/4.mp3', 'beyond', '光辉岁月'), ('/5.mp3', '陈慧琳', '记事本'), ('/6.mp3', '邓丽君', '但愿人长久')]
<class 'list'>
('/2.mp3', '任贤齐', '沧海一声笑')
/2.mp3 任贤齐 沧海一声笑
('/3.mp3', '齐秦', '往事随风')
/3.mp3 齐秦 往事随风
('/4.mp3', 'beyond', '光辉岁月')
/4.mp3 beyond 光辉岁月
('/5.mp3', '陈慧琳', '记事本')
/5.mp3 陈慧琳 记事本
('/6.mp3', '邓丽君', '但愿人长久')
/6.mp3 邓丽君 但愿人长久
'''

返回的列表中的各个元素都是元组类型,我们可以按照索引依次提取相应的内容。

sub()

使用正则表达式,不仅可以实现提取与检查字符串,还可以实现对文本中字符串的替换。

我们都知道,python中有replace()方法,支持对一个字符串内容的修改替换,但是如果在一个长段文本中使用replace()未免太傻了!

幸运的是,re库中提供了sub()方法,让我们可以使用正则表达式对文本内容进行匹配与修改,sub()的定义如下:

def sub(pattern, repl, string, count=0, flags=0)
  • pattern正则表达式
  • repl替换内容
  • string待替换内容
  • count替换次数,默认为0,即无限次
  • flages正则匹配模式

下举一实例:

import re

content = '54aKS4yrsoiRS4ixSL2g'
content = re.sub('\d+', '', content)
print(type(content), content)

# 输出为
'''
<class 'str'> aKSyrsoiRSixSLg
'''

sub()具体有什么用?它可以极大方便我们利用正则表达式分析文本,下举一实例说明:

import re

content = '''
<div id="songs-list">
<h2 class="title">经典老歌...
'''
pattern_1 = '<a.*?>|</a>|\n'
pattern_2 = '<li.*?>(.*?)</li>'

# 去除所有的<a>标签和换行符
content = re.sub(pattern=pattern_1, repl='', string=content, flags=re.S)
results = re.findall(pattern=pattern_2, string=content)

print(content)
for item in results:
    print(item)

# 输出为
'''
<div id="songs-list"><h2 class="title">经典老歌</h2><p class="introduction">经典老歌列表</p><ul id="list" class="list-group"><li data-view="2">一路上有你</li><li data-view="7">沧海一声笑</li><li data-view="4" class="active">往事随风</li><li data-view="6">光辉岁月</li><li data-view="5">记事本</li><li data-view="5">但愿人长久</li></ul></div>
一路上有你
沧海一声笑
往事随风
光辉岁月
记事本
但愿人长久
'''

compile()

compile()方法与前面所介绍的其他方法有所不同,它操作的对象是正则表达式,compile可以将正则字符串编译成正则表达式对象,以便在后续的匹配中复用

下举一实例:

import re

content_1 = '2016-12-15 12:00'
content_2 = '2016-12-17 12:55'
content_3 = '2016-12-22 13:21'
pattern = re.compile('\d{2}:\d{2}')
result_1 = re.sub(pattern, '', content_1)
result_2 = re.sub(pattern, '', content_2)
result_3 = re.sub(pattern, '', content_3)
print(result_1, result_2, result_3)

# 输出为
'''
2016-12-15  2016-12-17  2016-12-22 
'''

同样的,compile()还可以传入如re.S等修饰符,等同于为正则表达式做了一层封装,方便重复使用。