跳至主要內容

06-正则表达式详解

AI悦创原创2022年12月21日Python 网络爬虫专栏Crawler大约 37 分钟...约 11215 字

1568962558573.png
1568962558573.png

我们接下来看一下什么是正则表达式,什么时候用到正则表达式。

那这里我们打开一个在线正则表达式的一个网站也就是在线正则表达式测试,那在这里我写了一些字符串,(网址、QQ号、邮箱、邮编、字符串、电话号)那我们可以在下面写一些正则表达式就可以匹配了。

那在右边呢,就是一些常用的正则表达式,我们可以直接点击然后匹配我们的字符串。

常用正则表达式的方法
  • re.compile(编译)
  • pattern.match(从头匹配)
  • pattern.search(匹配一个,扫描所有)
  • pattern.findall(匹配所有)
  • pattern.sub(替换)

1.常见匹配模式

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

2. 详解

2.1 re.match

re.match 尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话,match() 就返回 none

re.match(pattern, string, flags=0)

2.1.1 最常规的匹配

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}.*Demo$', content)
print(result)# 输出匹配结果
print(result.group())# 获取匹配的内容(匹配的结果)
print(result.span())# 获取匹配的长度

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

上面的匹配是最常规的匹配,写了很长的正则表达式。这样的正则表达式通用性也不强,这时候刚刚燃气对正则表达式的好感又有点小慌,那咋办?别急,下面继续看。

Ps: 上面的正则表达式还可以这么写:

import re

content = 'Hello 123 4567 World_This is a Regex Demo'
# pattern = '^H\w+\s\d{3}\s\d{4}\s\w+\s\w+\s\w\s\w+\sDemo$'
# pattern = '^Hello\s\d\d\d\s\d{4}\s\w{10}.*Demo$'
pattern = '^Hello\s\d\d\d\s\d{4}\s\w{10}.+Demo$'

res = re.match(pattern=pattern, string=content)
print(res)

这个时候有同学会问,那我是用 .* 好呢,还是用 .+ 好呢?

我个人觉得使用 .* 较为稳妥,因为,. 是匹配任意字符,这两个最主要的区别在于 *+ 前者是匹配零个或 1 个,后者是匹配 1 个或多个,那有时候为了代码更加强壮,显然是前者—— .* (这样说法其实也不是非常严谨,看具体使用)

不过有这几点你得注意:

[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)  匹配有效数字的
^(?:\s*(<[\w\W]+>)[^>]*$ 匹配 html 标签的
^(?:\s*(<[\w\W]+>)[^>]*$ 匹配 html 标签的

2.1.2 泛匹配

上面,我给大家简单的演示了一下最常规的匹配,我们观察上面写的正则表达式会发现,我们原先的正则表达式显得繁琐且通用性也不强,那么有个非常好的方法,就是用刚才我们所用的的 .* 来匹配所有的字符串。

上面我们使用到了“.*" 匹配任意字符,零次或多次。接下来,我就使用这个方法来匹配。

'^Hello.*Demo$' 那么我们把中间所有的结果匹配出来了。

import re

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

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

那我们接下来,如何获取匹配目标呢?(也就是指定匹配目标)

比如,我就要字符串 content 中的 123 该怎么匹配呢?

2.1.3 匹配目标

一个思路就是,你要知道一个位置,如果两边已经指定知道,那这个位置的数据不就知道了?

为了方便理解,我画一草图:

1568965254422.png
1568965254422.png
import re

content = 'Hello 123 4567 World_This is a Regex Demo'
result = re.match('^Hello\s(\d+)\s4567.*Demo$', content)
# 指定左右空格 \s
print(result)
print(result.group(1))
# 如果有两个括号指定,那就是result.group(n)  >>> n 你说的数据
print(result.span())

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

2.1.4 贪婪匹配

按原来的想法,应该输出的结果是:**1234567 ,**不过实际却是:7,同学们是不是这么想的。

但是呢,看到这个运行结果,它只是把这个 7 匹配出来了,也就是说:它前面的 123456 被这个 .* 完全的匹配掉了,那其实也就是说:这个 .* 会依次的进行匹配而它会匹配尽可能多的字符。

我们可以看到,它前面有点星就会直接匹配到 123456,后面有 \d+ 也就是说至少要有一个数字。所它就会把 \d 匹配成一个 7 。(Point:.* 匹配尽可能地多,直到匹配不到为止!)

这也就是我们所说的贪婪匹配

那小伙伴有没有注意到,如果把上面贪婪匹配中,括号内的**(\d+)** 修改成 (\d*) 输出结果会这样?

import re

content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^Hello.*(\d*).*Demo$', content)
# 我们直接使用 .* 来匹配字符串,然后中间使用(\d*)来匹配我们的目标字符串,最后再使用 .* 匹配剩余的字符,(Dome然后$结束符)
print(result)
print(result.group(1))

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

那改成就一个**(\d)** 呢?

import re

content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^Hello.*(\d).*Demo$', content)
# 我们直接使用 .* 来匹配字符串,然后中间使用(\d)来匹配我们的目标字符串,最后再使用 .* 匹配剩余的字符,(Dome然后$结束符)
print(result)
print(result.group(1))

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

具体感受还是要靠你自己敲代码感受感受,我说出来,并不能然你灵活运用和理解。我能做的就是引导。

我们接下来来看一下非贪婪模式的匹配,非贪婪匹配我们可以在这个 .* 后面加一个 ?

2.1.5 非贪婪匹配

import re
# .*? 尽可能少的匹配
# 以下代码自己修改多运行几次就知道了
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*?(\d+).*Demo$', content)
print(result)
print(result.group(1))

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

我们运行一下,那这里我们可以看到运行的结果就是 1234567 了,那这是为什么呢?

那这个问号就会指定这个模式为非贪婪匹配,也就是说匹配尽可能少的字符。那你看这个问号之后有这个 \d+ 也就是说它开始匹配数字了。那当他开始匹配的时候如果看到它后面是一个数字,它就把前面的直接停止匹配。那这样的话 .*? 匹配 llo ,然后这个 \d+ 就会匹配到 1234567 ,那这个 .*? 指定为非贪婪,匹配尽可能少的字符。这样我们就能看到匹配效果,也就是我们想要的一种匹配效果,也就是匹配 1234567,那这样的话,我们可以使用非贪婪模式。把前面无关的字符用 .*? 来匹配,后面加上我们的匹配目标,这样我们就可以非常方便的把我们的匹配目标获取出来了。那这种效果就是我们想要的。

那把加号去掉呢?

提示: 按这样来看,可以使用 .*? 来指定一个格式,只要非贪婪匹配后面跟着某个特定规则(例如(\d)),一碰到满足的,.*? 就直接停止匹配。

解析:

.* :尽可能多的匹配(贪)

.(点) : 匹配任意字符,除了换行符

*(星号):匹配0次或者多次,这里的多次就是很多次(不限匹配次数)

.* : 连起来就是,匹配表达式:.(点) 的次数(也就是0次或多次)

.*? :尽可能少的匹配(懒)

上面已经解析我就不重复解析:

? :匹配次数只有 0 次或者 1 次(这就非常好理解了)

2.1.6 匹配模式

1568969690656.png
1568969690656.png
# 由上面的图片我们可以知道,返回的是 None。为什么呢?因为 . (点).	匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。
# 接下来指定一下匹配模式即可
import re

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

那接下来我们顺便把上面的 * 改成 + 观察一下以下代码:

总结:

点代表匹配任意字符除换行符,星号是匹配前面的表达式的零次或多次(也就是说:最少可以一次都不匹配,当然在条件允许的情况下,可以匹配多次),而加号就是表达:匹配一次或者多次(也就是说:至少要匹配一个数字,当然在条件允许的情况下,可以匹配多次)

( 'a', 'i', 'L', 'm', 's', 'u', 'x' 中的一个或多个) 这个组合匹配一个空字符串;

  1. 这些字符对正则表达式设置以下标记
  2. re.A (只匹配ASCII字符),
  3. re.I (忽略大小写),
  4. re.L (语言依赖),
  5. re.M (多行模式),
  6. re.S (点(dot)匹配全部字符( (点匹配所有字符)),
  7. re.U (Unicode匹配), and re.X (冗长模式)。
  8. (这些标记在 模块内容 中描述) 如果你想将这些标记包含在正则表达式中,这个方法就很有用,免去了在 re.compile() 中传递 flag 参数。标记应该在表达式字符串首位表示。

匹配模式名称匹配含义
re.A只匹配 ASCII 字符
re.I忽略大小写
re.L语言依赖
re.M多行模式
re.S匹配所有字符
re.U(Unicode匹配), and re.X (冗长模式)。

2.1.7 转义

import re

content = 'price is $5.00'
result = re.match('price is $5.00', content)
print(result)
None
import re

content = 'price is $5.00'
result = re.match('price is \$5\.00', content)
print(result)
<_sre.SRE_Match object; span=(0, 14), match='price is $5.00'>

那到这里,我们把正则表达式里面最常见的用法介绍完了,那么总结一下:

总结:尽量使用泛匹配、使用括号得到匹配目标、尽量使用非贪婪模式、有换行符就用 re.S(如果,在匹配的时候遇见特别长的字符,我们就可以使用 .* 来匹配,但是贪婪模式会导致我们少一些字符)

re.match:的缺点,就是他是从头开始匹配的,如果第一个不匹配的话,就无法执行了 这个是比较不方便的。

那 re 模块还有 re.search() ,这个方法它会直接搜索整个字符串,那如果有你写的正则表达式的话,它就会直接返回。

re.search 扫描整个字符串并返回第一个成功的匹配。

search

import re

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

# 输出
<_sre.SRE_Match object; span=(13, 53), match='Hello 1234567 World_This is a Regex Demo'>
1234567

总结:为匹配方便,能用 search 就不用 match

**在 search 匹配中慎重使用 ^$ **

你看下面的代码示例,看正则表达式有没有问题

import re

content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'

result = re.search('^Hello.*?(\d+).*?Demo$', content)
print(result)
# 你看上面的正则式,输出结果会是怎样的呢?不要着急看结果,自己试一试才能理解的更快。


# 输出
None

这时候,你会不会发现,哎?

我写对了鸭!为啥会是输出 None

会不会是 Demo 问题? 那把 Demo后面的 $ 去掉试一试!

import re

content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'

result = re.search('^Hello.*?(\d+).*?Demo', content)
print(result)

# 输出
None

What?

那咱们把 ^ 去掉呢?

import re

content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'

result = re.search('Hello.*?(\d+).*?Demo', content)
print(result)

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

可以了,不过,你有可能会有疑问,在 search 不能加 ^ $

咱们把代码修改一下:

import re

content = 'Hello 1234567 World_This is a Regex Demo'

result = re.search('^Hello.*?(\d+).*?Demo$', content)
print(result)

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

貌似可以鸭,其他的自行修改感受。

2.2.1 匹配演练

1.提取:齐秦 往事随风

齐秦 往事随风
任贤齐 沧海一声笑

上面的 search() 方法是查询一个结果,而接下来的 findall() 是查询符合条件的所有结果的。

2.3 re.findall

搜索字符串,以列表形式返回全部能匹配的子串。

[('/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 邓丽君 但愿人长久

从上面的代码,看起来没有问题,正常的匹配。但是,细心的你会发现,我们匹配的是从第二个开始的。那我该如何匹配全部呢?(Ps:因为第一个里面没有 href 标签)

(* 表示可能有 0 个或多个空白字符,?代表有还是没有(也就是 0 或 1 ))

[('', '一路上有你', ''), ('<a href="/2.mp3" singer="任贤齐">', '沧海一声笑', '</a>'), ('<a href="/3.mp3" singer="齐秦">', '往事随风', '</a>'), ('<a href="/4.mp3" singer="beyond">', '光辉岁月', '</a>'), ('<a href="/5.mp3" singer="陈慧琳">', '记事本', '</a>'), ('<a href="/6.mp3" singer="邓丽君">', '但愿人长久', '</a>')]
一路上有你
沧海一声笑
往事随风
光辉岁月
记事本
但愿人长久

这时候有同学想到:为什么不能用 \n 来匹配换行呢?

results = re.findall('<li.*?>\n?(<a.*?>)?(\w+)(</a>)?\n?</li>', html)
# 这样是不行的,
# 1. \n不能匹配空格
# 2. li后面除了换行距离下一个a标签还有一段空格的
# 3. \s是匹配空白字符,所以\n也会被匹配
# 4. 上面那条正则才没问题
print(results)

匹配不捕获 ?:

2.4 选择与反向引用

2.4.1 选择

用圆括号将所有选择项括起来,相邻的选择项之间用|分隔。但用圆括号会有一个副作用,使相关的匹配会被缓存,此时可用?:放在第一个选项前来消除这种副作用。

其中 ?: 是非捕获元之一,还有两个非捕获元是?=?!,这两个还有更多的含义,前者为正向预查,在任何开始匹配圆括号内的正则表达式模式的位置来匹配搜索字符串,后者为负向预查,在任何开始不匹配该正则表达式模式的位置来匹配搜索字符串。

2.4.2 反向引用

对一个正则表达式模式或部分模式两边添加圆括号将导致相关匹配存储到一个临时缓冲区中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 \n 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。

可以使用非捕获元字符 ?:?=?! 来重写捕获,忽略对相关匹配的保存。

反向引用的最简单的、最有用的应用之一,是提供查找文本中两个相同的相邻单词的匹配项的能力。以下面的句子为例:

Is is the cost of of gasoline going up up?

上面的句子很显然有多个重复的单词。如果能设计一种方法定位该句子,而不必查找每个单词的重复出现,那该有多好。下面的正则表达式使用单个子表达式来实现这一点:

2.4 re.sub

替换字符串中每一个匹配的子串后返回替换后的字符串。

re.sub(正则表达式, 你要替换的成的字符串,修改对象:字符串)

那,现在问题来了。

如果我们要替换的字符串目标是原字符串本身或者说包含原字符串。该怎么办呢?(就是替换了,原来的字符串还在。添加数字之类的......)

那这里我们可以把匹配的正则表达式加一个括号,表示一个整体并且我们还可以通过 Group() 来获取。(所以可以用 反斜杠 \1 来拿到这个字符,来进行字符串之后的操作,又因为这个反斜杠 1 是转义字符,所以需要加个 r 就是禁止转义更加(保持)原生。 )

import re

content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'
content = re.sub('(\d+)', r'\1 8910', content)
print(content)

# 输出
Extra stings Hello 1234567 8910 World_This is a Regex Demo Extra stings

在前面的的例子中,也就是下面的例子:

为了方便学习,我也把他写在下面了。

原先我们的正则表达式,写了 \s*? 是否有空白字符或换行的操作,是否有超链接的判断

那我们有了这个 re.sub() 方法,我们就先用一个正则表达式替换。然后把刚才的 a 标签完全替换掉(也就可以把 a 标签完全替换掉)之后就可以直接使用 findall() 方法来找到 li 标签的内容了。具体操作,继续往下看。

首先,我们使用 re.sub() 方法替换掉 a 标签,替换成空字符:>>> re.sub('<a.*?>|\</a>', '', html)

这里,你有可能就会有疑问,为什么要使用中间的这个符号:|。在正则表达式中,这个含义不是运算符中的并集。是或的含义,最上面开头已经讲过了,希望你在看到这里的时候已经全部记忆下来了。

为了让你或者说,可能是懒惰的你,知道原因,我把操作代码和结果都给你提供,这是其他教程不可能有的,其他教程目的不是让你懂,或者付费教程,而是赚钱都是这样,没有大圣人。唯独的区别就是:我也是想赚钱,但我每一篇都会尽可能的详细,绝不忽悠你任何技术上的问题。

那接下来使用 re.findall() 方法就特别方便了li 标签中的歌名提取出来。

但是还有一个匹配方式:

2.5 re.compile

将正则字符串编译成正则表达式对象

将一个正则表达式串编译成正则对象,以便于复用该匹配模式
import re

content = '''Hello 1234567 World_This
is a Regex Demo'''
pattern = re.compile('Hello.*Demo', re.S)
# re.compile('正则表达式', 匹配模式)
result = re.match(pattern, content)
#result = re.match('Hello.*Demo', content, re.S)
print(result)
<_sre.SRE_Match object; span=(0, 40), match='Hello 1234567 World_This\nis a Regex Demo'>

3. 实战练习

讲了那么多内容,现在就进入实战演练一下,是骡子是马,拉出来溜溜。

目标:豆瓣

目标网址:https://book.douban.com/

不过在实战之前,我再补充一条:()可以提取我们想要的文字

代码示例:

带括号:

"C:\Program Files\Python37\python.exe" D:/daima/pycharm_daima/爬虫大师班/Python_Web_Spider/re/re_test.py
['杜尚', '哀伤纪', '中央帝国的军事密码', '破碎海岸', '日本人为何选择了战争', '下町火箭', '闽国', '沉睡者', '品味', '庸人自扰', '敌人与邻居', '爱的救赎', '人生模式', '野兔', '筋膜拉伸', '可能和你有关', '南渡君臣', '黑暗中飘香的谎言', '羊之歌', '82年生的金智英', '沿着季风的方向', '行乞家族', '从一到无穷大', '战争', '英国下层阶级的愤怒', '尸人庄谜案', '元老', '奇迹的孩子', '北方以北', '大茂那', '如何用手机拍一部电影', '离婚', '1789年大恐慌', '奥斯维辛的拳击手', '日本色气', '漫长的婚约', '何为真正生活', '不似骄阳', '剧本结构论']

Process finished with exit code 0

豆瓣demo

上面的代码会照成,书籍名称与作者无法一一对应,那咱们修改呢?

豆瓣最终代码:

import requests
import re
content = requests.get('https://book.douban.com/').text
pattern = re.compile('<li.*?cover.*?href="(.*?)".*?title="(.*?)".*?more-meta.*?author">(.*?)</span>.*?year">(.*?)</span>.*?</li>', re.S)
results = re.findall(pattern, content)
for result in results:
    url, name, author, date = result
    author = re.sub('\s', '', author)
    date = re.sub('\s', '', date)
    print(url, name, author, date)
    
# Ps:原本可以使用 strip() 方法来提出 \n 等,不过学了正则表达式,我们可以使用 re.sub() 方法来操作。

欢迎关注我公众号:AI悦创,有更多更好玩的等你发现!

公众号:AI悦创【二维码】

AI悦创·编程一对一

AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发、Web全栈、Linux」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh

C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh

方法一:QQ

方法二:微信:Jiabcdefh

你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
通知
关于编程私教&加密文章

Your primary language is en-US, do you want to switch to it?

您的首选语言是 en-US,是否切换到该语言?