3.3 正则表达式
前面两篇博客介绍了lxml
跟BeautifulSoup
对信息的提取,这节来介绍最后一种提取信息的方式----正则表达式。
正则表达式相信大家都听过,那什么是正则表达式呢?通俗的讲,正则表达式就是按照一定的规则,从某个字符串中匹配出满足规则的字符串。
先来简单的看一下正则表达式的使用:
import re
text = 'hello re'
reg = re.match('he',text)
print(reg.group()) # 输出he
match(pattern,string) 作用:在string
中匹配以pattern
开头的字符串,若匹配到则返回pattern
,否则返回None
.
group() 作用:暂时知道它是返回匹配到的字符串就可以了。后面再详细讲。
接下来看看一些在正则表达式中具有特殊意义的字符。
- 匹配单个字符
. 匹配除换行符以外的任意字符
[] 匹配里面的任意一个字符
[^ ] 除了里面的任意一个字符
\d 匹配数字(大写的意思就是与小写相反) 如 \D 表示除了数字的都匹配
\w 等价于 [0~9A~Za~z_] \W 表示除了[0~9A~Za~z_]都匹配
\s 匹配空白符,如[\f\n\r\t ] (空白也在里面) \S 同理
[0~9A~Za~z_] 表示匹配0到1、A到Z、a到z、_
中的任意一个。
\D 等价于 [^0~9]
、[^\d]
。
tips:如果要匹配一些符号,比如/ 、, < >
等,可以使用 \S 来匹配
import re
text = '12A'
reg = re.match('\d',text)
print(reg.group()) # 输出1
phone = '125421'
print(re.match('[21]',phone).group()) # 输出1
# print(re.match('[245]',phone).group()) # 报错,说明没找到以2或4或5开头的字符串
tips:若string
中拥有特殊意义的字符时需转义r''
。
若pattern
中拥有特殊意义的字符时需要使用 \
转义。因为python 本身要转义一次,放到正则还要在转义一次。所以 匹配\
需写成 '\\\\'
,匹配+
需写成 '\+'
-
锚字符:
^
以…开头(与[]
里面的^
不是一个意思)
$
以…结尾import re text1 = 'hello re' # 以he开头 等价于match reg = re.search('^he', text1) # search用法后面有详解 print(reg.group()) # 以re结尾 r = re.search('re$', text1) print(r.group())
-
匹配多个字符:
特殊符号 作用 例子 * 匹配任意多个字符 x* 匹配任意个x ? 匹配0个或1个字符(解除贪婪模式) x? 匹配0个或1个x + 匹配1个或多个字符(最少都要匹配到1个) x+ 匹配1个或多个x () 将括号里面的字符看成一个整体 (xyz) 匹配xyz {n} 匹配n个字符 x{4} 匹配4个x {n,m} 匹配n~m个字符 x{2,5} 匹配2到5个x | 或 x|y 匹配x或者y
小练习:
import re
# 1.验证手机号码
phone = '14512545888'
reg = re.match(r'1[345678]\d{9}', phone)
print(reg.group())
1[345678]\d{9}
表示 第一位为1,第二位可以是345678的任意一个,接下来的9位是任意数字。
# 2.验证邮箱(邮箱可以包含小数点)
email = '524615@qq.com'
em = re.match(r'[\w.]+@\w+\.\w+', email)
print(em.group())
email2 = r'bill.gates@microsoft.com'
em2 = re.match(r'[\w.]+@\w+\.com$', email2)
print(em2.group())
[\w.]+
表示 至少要匹配到\w
或 .
@
表示 匹配@
符
\w+\.com$
表示 .
前面至少有一个字符,.
后面必须以com
结尾
#3.验证URL
URL = 'https://baidu.com/item/python'
u = re.match(r'(http|https|ftp)://\w+\.\S+', URL)
print(u.group())
(http|https|ftp)
表示匹配http、https、ftp
的其中一个。注意这里不要使用 [http|https|ftp] 因为此时匹配的是[]
里面的任意一个字符。
贪婪模式:尽可能多的匹配字符。
何为贪婪模式?比如 有一个字符串 <h1>段落</h1>
若你使用<\w+>
去匹配,则会匹配到整个字符串,这就叫贪婪模式。而如果你此时只想匹配到<h1>
,可以<\w+?>
加个问号解除贪婪匹配。
group分组:
用 () 表示要提取的分组 组的排序:从外到内,从左到右
import re
text = '154-555'
m = re.search(r"(?P<name1>\d{3})-(?P<name2>\d{3})", text)
print(m.group('name1')) # 154
print(m.group(0), '***', m.group()) # 154-555 *** 154-555
print(m.group(1)) # 154
print(m.group(2)) # 555
print(m.groups()) # ('154', '555')
注意:
?P<name1>
是给\d{3}
所匹配到的该组字符串取名为name1
,而不是匹配的意思。- 需要注意
groups()
跟m.group()
的区别
re模块常用函数:
函数 | 作用 |
---|---|
match(pattern,string[,flags=0]) | 从string开头匹配pattern,有则返回,不是开头或者没有返回none |
search(pattern,string[,flags=0]) | 返回第一个匹配成功的pattern |
findall(pattern,string[,flags=0]) | 返回一个所有成功匹配数据的列表 |
finditer(pattern,string[,flags=0]) | 返回一个所有成功匹配数据的迭代器,可以用next()迭代 |
sub(pattern,repl,string[,count=0,flags=0]) | 返回在string 中利用repl 替换满足pattern 的字符串,count 表示替换多少个满足的字符串 |
split(pattern, string[, maxsplit]) | 按照满足pattern 的字符串将string分割后返回列表 |
flags:
编译标志位,用于修改正则表达式的匹配方式,如:是否区分大小写,多行匹配等。
re.I
忽略大小写re.M
多行匹配,影响^和$re.S
或者**re.DOTALL
** 使.
可以匹配包括换行符在内的所有字符
import re
s= '12 34\n56 78\n90'
re_f = re.findall(r'^\d+', s, re.M) # 匹配每行的开头
print(re_f) # ['12', '56', '90']
print(re.search(r'.+', s, flags=re.S).group()) # 12 34\n56 78\n90
compile:
在上表的前四个函数执行之前,都会执行一个compile函数,即正则表达式会先编译模式(即前面的pattern)是否正确然后再进行匹配。
reg = re.search(r'.+', s, re.S)
等价于
re_com = re.compile(r'.+', re.S)
reg = re_com.search(s)
看到这里你也许会想,直接search
就完事了,compile
还需要两步这么麻烦,还不如直接用search
呢。我一开始也是这么觉得的,知道后来,我发现事情并不那么简单。
如果你需要对许多的字符串进行相同模式的匹配,此时compile
就派上了大用处,你只需要编译一次就可以一直使用。不信你看我下面:
re_com = re.compile(r'.+')
reg1 = re_com.search(s1)
reg2 = re_com.search(s2)
reg3 = re_com.search(s3)
......
而如果你直接search
的话,就要编译好多次,如果你要匹配几百万条数据,每条都要编译,那得花多少时间啊。
sub:
string = 'I must be 1个 shuaige.'
string = re.sub(r'\d个', '', string) # 把 1个 删除
print(string) # I must be shuaige.
split:
text = 'hello$$world'
reg = re.split('\W', text)
print(reg) # ['hello', '', 'world']
综合案例:爬取哔哩哔哩热门视频
使用正则表达式进行匹配时,不会像前两章的那两种提取方式有一种结构在里面,可以直接通过方法来筛选,正则能匹配的只有字符串,也就是说即使你匹配的整个网页,在正则看来就是一串很长的字符串而已。所以我们来转变下思路,不要先一个一个的爬取,而是把全部的标题,链接、观看数等一次性爬取下来,分别放到数组里,然后再对数组进行整合即可得到相同效果。
import requests
import re
# 输出视频的详情 如链接、标题、播放量、评论数、作者
def print_detail(msg_lists):
for i in msg_lists:
print(i)
# 解析页面
def html_parse(url):
msg_lists = [] # 存放所有的视频信息
res = requests.get(url)
text = res.text
# 获取链接跟标题
msg = re.findall(r'<a href="(.*?)".*?class="title">(.*?)</a>', text)
# 获取播放量、评论数以及作者名称
detail = re.findall(r'<span class="data-box">.*?</i>(.*?)</span>', text)
# 获取前20个视频信息
for i in range(20):
detail_dist = {} # 每次存放单个视频的详细信息
detail_dist['视频链接'] = msg[i][0] # 存储视频链接
detail_dist['标题'] = msg[i][1] # 存储标题
detail_dist['播放量'] = detail[i * 3] # 存储播放量
detail_dist['评论数'] = detail[i * 3 + 1] # 存储评论数
detail_dist['作者'] = detail[i * 3 + 2] # 存储作者
msg_lists.append(detail_dist) # 把每个视频信息存放到列表中
# 输出视频信息
print_detail(msg_lists)
if __name__ == '__main__':
url = 'https://www.bilibili.com/ranking'
html_parse(url)
分析正则表达式:
<a href="(.*?)".*?class="title">(.*?)</a>
对需要获取的信息加上 ()
匹配成功时就会返回该括号内匹配到的内容。
.*?
表示提取任意多的除换行外的字符,问号的作用是接触贪婪模式。此时"(.*?)"
模式就会告诉正则说,“ ”
里面的内容随便你匹配,匹配到了就返回给我。
.*?class
表示前面的内容随便你匹配,反正到了class
这里你就给我停止。
所以,msg[i]
存放了第i
个视频的链接跟标题。
<span class="data-box">.*?</i>(.*?)</span>
你分析了页面之后,你会发现,这个正则表达式第一次匹配到了播放量、第二次匹配到了评论数、第三次匹配到了作者名称,第四次才再次匹配到下一条视频的播放量,第五次才再次匹配到下一条视频的评论数….
所以才会有 detail[i * 3]
detail[i * 3 + 1]
detail[i * 3 + 2]
小结:
- 正则表达式没有那种层次结构,在它的世界里只有字符串。
- 在需要匹配的地方加
()
好啦,正则表达式就介绍到这里,小伙伴们要多多练习,有问题可以在评论区一起讨论。