怎么才能从一段文本中获取我们想要的信息呢?正则表达式就是其中一个非常有效的方法。正则表达式测试工具。我们使用python的re模块进行正则表达式的匹配,下面对re库中的常见用法进行介绍。
3.1 基本用法
对于 URL 来说,可以用下面的正则表达式匹配:
[a-zA-z]+://[^\s]*
a-z
代表匹配任意的小写字母,\s
表示匹配任意的空白字符,*
就代表匹配前面的字符任意多个,这一长串的正则表达式就是这么多匹配规则的组合。
写好正则表达式后,就可以拿它去一个长字符串里匹配查找了。不论这个字符串里面有什么,只要符合我们写的规则,统统可以找出来。对于网页来说,如果想找出网页源代码里有多少 URL,用匹配 URL 的正则表达式去匹配即可。
常用的匹配规则:
模 式 | 描 述 |
---|---|
\w | 匹配字母、数字及下划线 |
\W | 匹配不是字母、数字及下划线的字符 |
\s | 匹配任意空白字符,等价于 [\t\n\r\f] |
\S | 匹配任意非空字符 |
\d | 匹配任意数字,等价于 [0-9] |
\D | 匹配任意非数字的字符 |
\A | 匹配字符串开头 |
\Z | 匹配字符串结尾,如果存在换行,只匹配到换行前的结束字符串 |
\z | 匹配字符串结尾,如果存在换行,同时还会匹配换行符 |
\G | 匹配最后匹配完成的位置 |
\n | 匹配一个换行符 |
\t | 匹配一个制表符 |
^ | 匹配一行字符串的开头 |
$ | 匹配一行字符串的结尾 |
. | 匹配任意字符,除了换行符,当 re.DOTALL 标记被指定时,则可以匹配包括换行符的任意字符 |
[...] | 用来表示一组字符,单独列出,比如 [amk] 匹配 a 、m 或 k |
[^...] | 不在 [] 中的字符,比如 匹配除了 a 、b 、c 之外的字符 |
* | 匹配 0 个或多个表达式 |
+ | 匹配 1 个或多个表达式 |
? | 匹配 0 个或 1 个前面的正则表达式定义的片段,非贪婪方式 |
{n} | 精确匹配 n 个前面的表达式 |
{n, m} | 匹配 n 到 m 次由前面正则表达式定义的片段,贪婪方式 |
正则表达式不是 Python 独有的,它也可以用在其他编程语言中。Python 的 re 库提供了整个正则表达式的实现,利用这个库,可以在 Python 中使用正则表达式。
3.2 match
首先介绍第一个常用的匹配方法 —— match
,向它传入要匹配的字符串以及正则表达式,就可以检测这个正则表达式是否匹配字符串。
match
方法会尝试从字符串的起始位置匹配正则表达式,如果匹配,就返回匹配成功的结果;如果不匹配,就返回 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())
开头的 ^
是匹配字符串的开头,也就是以 Hello
开头;然后 \s
匹配空白字符,用来匹配目标字符串的空格;\d
匹配数字,3 个 \d
匹配 123
;然后再写 1 个 \s
匹配空格;后面还有 4567
,我们其实可以依然用 4 个 \d
来匹配,但是这么写比较烦琐,所以后面可以跟 {4}
以代表匹配前面的规则 4 次,也就是匹配 4 个数字;后面再紧接 1 个空白字符,最后的 \w{10}
匹配 10 个字母及下划线。
在 match
方法中,第一个参数传入了正则表达式,第二个参数传入了要匹配的字符串。
(1)匹配目标
如果想从字符串中提取一部分内容,可以使用括号 ()
将想提取的子字符串括起来。()
实际上标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,调用 group
方法传入分组的索引即可获取提取的结果。示例如下:
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^Hello\s(\d+)\sWorld', content)
print(result)
print(result.group())
print(result.group(1))
print(result.span())
结果如图所示:
我们成功得到了 1234567
。这里用的是 group(1)
,它与 group()
有所不同,后者会输出完整的匹配结果,而前者会输出第一个被 ()
包围的匹配结果。假如正则表达式后面还有 ()
包括的内容,那么可以依次用 group(2)
、group(3)
等来获取。
(2)通用匹配
还有一个万能匹配可以用,那就是 .*
。其中 .
可以匹配任意字符(除换行符),*
代表匹配前面的字符无限次,所以它们组合在一起就可以匹配任意字符了。有了它,我们就不用挨个字符匹配了。
content = 'Hello 123 4567 World_This is a Regex Demo'
result = re.match('^Hello.*Demo$', content)
print(result)
print(result.group())
print(result.span())
(3)贪婪与非贪婪
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*(\d+).*Demo$', content)
print(result)
print(result.group(1))
值得注意,这里最终输出的结果为 7 !
在贪婪匹配下,.*
会匹配尽可能多的字符。正则表达式中 .*
后面是 \d+
,也就是至少一个数字,并没有指定具体多少个数字,因此,.*
就尽可能匹配多的字符,这里就把 123456
匹配了,给 \d+
留下一个可满足条件的数字 7,最后得到的内容就只有数字 7 了。
这很明显会给我们带来很大的不便。有时候,匹配结果会莫名其妙少了一部分内容。其实,这里只需要使用非贪婪匹配就好了。非贪婪匹配的写法是 .*?
,多了一个 ?
。代码如下
result = re.match('^He.*?(\d+).*Demo$', content)
print(result.group(1))
但这里需要注意,如果匹配的结果在字符串结尾,.*?
就有可能匹配不到任何内容了,因为它会匹配尽可能少的字符。
(4)修饰符
正则表达式可以包含一些可选标志修饰符来控制匹配模式。修饰符被指定为一个可选的标志。
content = '''Hello 1234567 World_This
is a Regex Demo
'''
result = re.match('^He.*?(\d+).*?Demo$', content)
print(result.group(1))
该文本存在换行,会导致最终匹配失败,程序报错。可以添加一个re.S修饰符修正错误。类似的换行符如下:
修饰符 | 描 述 |
---|---|
re.I | 使匹配对大小写不敏感 |
re.L | 做本地化识别(locale-aware)匹配 |
re.M | 多行匹配,影响 ^ 和 $ |
re.S | 匹配包括换行符在内的所有字符 |
re.U | 根据 Unicode 字符集解析字符。这个标志影响 \w 、\W 、\b 和 \B |
re.X | 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解 |
(5)转义匹配
正则表达式定义了许多匹配模式,如 .
匹配除换行符以外的任意字符,但是如果目标字符串里面就包含 .
,那该怎么办?
content = '(百度) www.baidu.com'
result = re.match('\(百度\) www\.baidu\.com', content)
print(result)
当遇到用于正则匹配模式的特殊字符时,在前面加反斜线转义一下即可。例如可以用 \.
来匹配 .
3.3 search
match
方法是从字符串的开头开始匹配的,一旦开头不匹配,那么整个匹配就失败了。
content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'
result = re.match('Hello.*?(\d+).*?Demo', content)
print(result)
这里的字符串以 Extra
开头,但是正则表达式以 Hello
开头,整个正则表达式是字符串的一部分,但是这样匹配是失败的。运行结果为:None
方法 search
,它在匹配时会扫描整个字符串,然后返回第一个成功匹配的结果。也就是说,正则表达式可以是字符串的一部分,在匹配时,search
方法会依次扫描字符串,直到找到第一个符合规则的字符串,然后返回匹配内容,如果搜索完了还没有找到,就返回 None
。
result = re.search('Hello.*?(\d+).*?Demo', content)
print(result)
现有html文本如下:
html = '''
<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>
'''
如果要匹配第一个歌手名和歌名:
result = re.search('<li.*?active.*?singer="(.*?)">(.*?)</a>', html, re.S)
if result:
print(result.group(1), result.group(2))
3.4 findall
findall
方法,该方法会搜索整个字符串,然后返回匹配正则表达式的所有内容。
results = re.findall('<li.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>', html, re.S)
print(results)
print(type(results))
for result in results:
print(result)
print(result[0], result[1], result[2])
3.5 sub
除了使用正则表达式提取信息外,有时候还需要借助它来修改文本。比如,想要把一串文本中的所有数字都去掉,如果只用字符串的 replace
方法,那就太烦琐了,这时可以借助 sub
方法。
content = '54aK54yr5oiR54ix5L2g'
content = re.sub('\d+', '', content)
print(content)
这里只需要给第一个参数传入 \d+
来匹配所有的数字,第二个参数为替换成的字符串(如果去掉该参数的话,可以赋值为空),第三个参数是原字符串。
在上面的 HTML 文本中,如果想获取所有 li
节点的歌名,直接用正则表达式来提取可能比较烦琐。
results = re.findall('<li.*?>\s*?(<a.*?>)?(\w+)(</a>)?\s*?</li>', html, re.S)
for result in results:
print(result[1])
此时借助 sub
方法就比较简单了。可以先用 sub
方法将 a
节点去掉,只留下文本,然后再利用 findall
提取就好了:
html = re.sub('<a.*?>|</a>', '', html)
print(html)
results = re.findall('<li.*?>(.*?)</li>', html, re.S)
for result in results:
print(result.strip())
3.6 compile
compile
方法,这个方法可以将正则字符串编译成正则表达式对象,以便在后面的匹配中复用
content1 = '2019-12-15 12:00'
content2 = '2019-12-17 12:55'
content3 = '2019-12-22 13:21'
pattern = re.compile('\d{2}:\d{2}')
result1 = re.sub(pattern, '', content1)
result2 = re.sub(pattern, '', content2)
result3 = re.sub(pattern, '', content3)
print(result1, result2, result3)
这里有 3 个日期,我们想分别将 3 个日期中的时间去掉,这时可以借助 sub
方法。
compile
还可以传入修饰符,例如 re.S
等修饰符,这样在 search
、findall
等方法中就不需要额外传了。所以,compile
方法可以说是给正则表达式做了一层封装,以便我们更好地复用。