如果说要把 python 作为专业开发工具,那么就必须理解爬虫;如果想要悟透爬虫,那么就必须掌握正则表达式。
突破常规,先吟一曲小诗表达一下我作本文的那份信心:
爬虫路漫远兮,
python求索叹奇。
一文看尽正则,
嘴扬心笑生焉。本文将用最直白的描述、最简单的实例详尽讲解正则表达式的入门思想;
待你读完本文,怡然回首,定会叹曰:“呵!正则!不过如此~~~”
目录
1 项目概览
1.1 概念引入
首先我们理解两个概念:
①爬虫:说白了,爬虫就是能够按照制定规则自动浏览网络信息的程序,并且能够存储我们需要的信息。
②正则表达式:简单而言,就是对字符串过滤用的;
具体而言,就是对字符串的一种逻辑公式,即用事先定义好的特定字符,以及这些字符的组合,组合成一个“规则字符串”,并用这个“规则字符串”表达对字符串的过滤。
事实上,正则表达式不只限用于python爬虫;比如高效判别身份证号码真伪、验证Email地址、正确匹配ip地址等都是离不开对于正则表达式的理解和掌握的。广义上来讲,所有的编程开发人员都必须掌握正则表达式。
那么我们为什么要掌握正则表达式呢?
正则表达式目的:①判断字符串是否符合正则表达式的逻辑;
②通过正则表达式从特定字符串中获取我们需要的特定部分。
1.2 实例开发准备
①介绍正则表达式所用实例开发语言为 python语言 ;
②python环境: python 3.8.2 ;
③python编译器:JetBrains PyCharm 2018.1.2 x64 ;
④主要用到的包库有:URL处理模块——urllib 模块包、re 正则表达式模块 等。
⑤主体内容及简介:
- 第一部分(实战解说):任务是下载小说《斗罗大陆》。具体而言主要通过正则表达式将HTML文件中每一章节的 URL 获取到并通过for循环将每一章节内的文字部分提取并下载到指定文件夹中的 .txt 文件中。
- 第二部分(学以致用):目标是爬取b站中的视频弹幕并生成词云。具体而言,是爬取名为我的NBA手办真的会打球!!!视频中的上千条实时弹幕,并通过jieba库和wordcloud库生成词云然后以图片形式输出到本地。
2 实战解说
这一部分我们以小说网站全书网为例,解析并下载网站内的连载小说《斗罗大陆》。
2.1 获取目标页面及对应的HTML文件。
获取网站对应HTML文件我们的核心代码是用对象.函数(参数).调用对象返回的方法()实现的。具体代码如下:
-
from urllib
import request
# 导入url处理模块urllib包
-
-
# 获取目标页面 url ,并存入first_url中
-
first_url =
"http://www.quanshuwang.com/book/44/44683"
-
-
#访问小说首页面,打印输出相应的HTML文件
-
html = request.urlopen(first_url).read()
# html = 对象.函数(参数).调用对象返回的方法()
-
print(html)
上图是通过代码得到的目标页面对应HTML文件在PyCharm中的实现结果,
下图是在Google Chrome浏览器中F12键显示的Elements。
仔细对比,我们发现获得的就是目标页面对应的HTML文件,只不过按照横排排列罢了
细心的小伙伴们还会注意到输出内容最前面有小写字母b,其含义是提示我们输出的全部都是二进制数据
因此我们获得的是二进制文件,而不是字符串文件,这是就涉及到了转码问题
二进制转化为字符串是需要编码的,具体而言用decode()即可实现
转码格式是什么呢?这时就需要知道该HTML文件的编码
如下图所示,在浏览器页面Elements对应元素发现charset=gbk,其中的gbk就是转码格式。
按照这一思路,在核心代码里面加上decode('gbk'),输出发现小写字母b没了,也就意味着二进制文件转字符串成功啦!
html = request.urlopen(first_url).read().decode('gbk')
转码为字符串格式后HTML文件输出结果截图(部分)
2.2 引入正则表达式
获得到HTML文件意味着本文讲解重点才刚刚开始。
开篇提到,这一部分我们的任务是获取每个章节对应的 url 并下载到指定文件夹下的 .txt 文件中。
在输出地HTML文件中我们可以清晰看到每个章节的 url,但是你要知道,将近700章的内容,也就是有近700个 url,很显然,一个一个手动获取是不现实的。
为了快速获取近700个< href >标签中的url,这时我们正式引入正则表达式。
接下来我们采取步步深入策略带领大家探索奥妙奇趣的正则表达式世界!
引子
在正式讲解正则表达式之前,我们先对相关知识做一铺垫;“地基”打好了,盖楼就容易多了。
在python中,re 模块是不需要另外下载安装的,也就是生来就有的。
这里我们介绍一下 re 模块中的 search() 方法和 findall() 方法。
search() 方法介绍:
该方法从前到后遍历整个字符串,寻找特定位置,找到则立即返回。
-
import re
-
-
string =
'llabcdabcs'
-
res = re.search(
'abc',string)
-
print(res)
我们发现,在字符串 'llabcdabcs' 中寻找有没有子串 'abc' ,有,即返回字符串对应位置且只返回一个。
注:字符串下标从 0 开始。
findall() 方法介绍:
该方法输出结果是列表 ,且把所有匹配的子串全部返回回来(有多少个返回多少个)。
-
import re
-
-
string =
'llabcdabcs'
-
res = re.findall(
'abc',string)
-
print(res)
有了铺垫的这部分知识,我们在刚刚解析出来的HTML文件中打印出指定的字符串部分就水到渠成了。
正则表达式实例详解
假设我们想要获取部分为下图蓝色背景下的作者名及小说书名,怎么做呢?
首先,获取小说书名:
第一步,将想要获取的内容复制粘贴到代码中的 novel_info 部分,就像这样(仅展示核心代码):
novel_info['title'] = re.findall('<div class="chapName"><span class="r">作者:1416338685</span><strong>斗罗大陆</strong>',html)
如果至此就要运行的话,会出什么问题呢?
事实上这样是无法解析的,因为复制过去的内容中含有许多符号,需要转义;
这时,我们只需在前面加上 r 即可,这样就完美避免了一一转义的繁琐步骤。
同样,这里展示核心代码中改进后的代码片段:
novel_info['title'] = re.findall(r'<div class="chapName"><span class="r">作者:1416338685</span><strong>斗罗大陆</strong>',html)
第二步,解析并获取正则表达式的匹配模式,并替换掉上一步中 novel_info 的对应部分
首先给出获取小说书名的最终代码及结果截图,以便进一步讲解:
-
from urllib
import request
-
import re
-
-
first_url =
"http://www.quanshuwang.com/book/44/44683"
-
-
#访问小说首页面
-
html = request.urlopen(first_url).read().decode(
'gbk')
-
# 小说的信息
-
novel_info = {}
-
# 小说的名字
-
novel_info[
'title'] = re.findall(
r'<div class="chapName">.*?<strong>(.*?)</strong>',html)
-
print(novel_info)
-
#print(html)
心心念念的正则表达式它终于来啦,就是这么一串:
'<div class="chapName">.*?<strong>(.*?)</strong>'
我们就是要通过这么一串字符,输出想要获取的指定内容(在这里即获取小说书名)。
其中有一个 .*? 带有括号,含义是分组并返回括号内所匹配的内容。
事实上,.*? 组合是最常用到的一种组合形式,那么具体是什么意思呢?
. 代表可以匹配一个任意字符(不包括不可见的换行字符)
* 代表可以任意次()地匹配它前面的子表达式(即“贪婪匹配”)
*? 代表“非贪婪匹配”
() 代表子表达式,把指定的内容放入缓存并返回
与前面讲解思路类似,先通过一个简单字符实例阐明一下其中的扼要:
-
import re
-
-
string =
'zoooo'
-
res = re.match(
'zo*',string)
-
print(res)
由下图运行结果所示可知,我们匹配并得到了 * 号之前的以 zo 开头的所有且任意长度的值(有多少几把这些全部匹配出来),即得到了 zooo 。
还是上述这个例子,仅在其中加一 ?号看一下“非贪婪匹配”是什么结果,并与“贪婪匹配”做一对比。
-
import re
-
-
string =
'zoooo'
-
res = re.match(
'zo*?',string)
-
print(res)
通过对比我们发现,在“非贪婪匹配”情况下,返回的值是贪婪匹配情况下返回的最小值(即任意数=0时的值)
即仅输出 z 一个字符。
这时我们就很好理解本本分首先引入的含正则表达式的代码了
我们只想得到小说书名,即“斗罗大陆”这几个字,我们关键通过下面这一句程序实现的
novel_info['title'] = re.findall(r'<div class="chapName">.*?<strong>(.*?)</strong>',html)
为什么要写两个 .*? 呢?
因为整个 HTML 文件中的 <strong> …… </strong>标签可能是非常多的,只写一个的话可能还匹配到其他具有同样标签的值,这种情况是我们不想看到的,所以写了两个,令其仅特定指到我们想要的那部分。
而且稍前部分提到过,加括号 () 的原因是我们只想要获得输出括号内指定的数据。
哦,对了,有一点要特别注意,那就是输出是以列表形式输出的。不注意这点的话,编写的代码很可能报错。
下面给出仅输出小说书名的代码(列表形式输出):
-
from urllib
import request
-
import re
-
-
first_url =
"http://www.quanshuwang.com/book/44/44683"
-
-
#访问小说首页面
-
html = request.urlopen(first_url).read().decode(
'gbk')
-
# 小说的信息
-
novel_info = {}
-
# 小说的名字
-
novel_info[
'title'] = re.findall(
r'<div class="chapName">.*?<strong>(.*?)</strong>',html)
-
print(novel_info[
'title'])
-
#print(html)
若不想以列表形式输出,仅想得到列表中的值,应该怎么做呢?
想必大家都已经想到了,就是在核心代码部分加 [0] 取出列表中的第一个值。
改进后的代码如下(输出列表中的值,即“斗罗大陆”这四个字:)
-
from urllib
import request
-
import re
-
-
first_url =
"http://www.quanshuwang.com/book/44/44683"
-
-
#访问小说首页面
-
html = request.urlopen(first_url).read().decode(
'gbk')
-
# 小说的信息
-
novel_info = {}
-
# 小说的名字
-
novel_info[
'title'] = re.findall(
r'<div class="chapName">.*?<strong>(.*?)</strong>',html)[
0]
-
print(novel_info[
'title'])
-
#print(html)
按照获取思路,我们再试一下获取小说作者:
思路完全一样,这里就不再赘述了,仅给出代码供大家参考:
(提示:(.*?)中内容取自 <div class="chapName"><span class="r"> 和 </span>之间)
-
from urllib
import request
-
import re
-
-
first_url =
"http://www.quanshuwang.com/book/44/44683"
-
-
#访问小说首页面
-
html = request.urlopen(first_url).read().decode(
'gbk')
-
# 小说的信息
-
novel_info = {}
-
# 作者名字
-
novel_info[
'author'] = re.findall(
r'<div class="chapName"><span class="r">(.*?)</span>',html)[
0]
-
print(novel_info[
'author'])
3.3 正则表达式在实例中的应用
前面介绍了这么多,不要忘了我们最初的目的:爬取并下载整本小说的所有内容
分析HTML文件,我们发现,近七百章节的内容在<DIV> …… </DIV>中存放
于是乎,这里采取逐步缩小查找区间的方法查找我们想要获取的所有URL。
-
import re
-
-
first_url =
"http://www.quanshuwang.com/book/44/44683"
-
-
#访问小说首页面
-
html = request.urlopen(first_url).read().decode(
'gbk')
-
-
div_info = re.findall(
r'<DIV class="clearfix dirconone">(.*?)</div>',html)
-
print(div_info)
运行结果为空列表(说明这则表达式不对),那么为什么不对呢?
事实上,前面已经提到过,因为 . 只能匹配任意一个字符,因此为我们还需要把缺的参数补上,即缺少 re.S 参数。
re.S 可以让 . 匹配到任意字符(包括换行符)
于是代码变成了这样:
-
import re
-
-
first_url =
"http://www.quanshuwang.com/book/44/44683"
-
-
#访问小说首页面
-
html = request.urlopen(first_url).read().decode(
'gbk')
-
-
div_info = re.findall(
r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S)
-
print(div_info)
细心的读者会发现,两个div大小写是不一样的;事实上,这样得到的依旧是一个空列表。
这时我们就需要忽略大小写,即在参数部分后面再加 re.I , 并用 | 隔开。
下面是再一次改进后的代码:
-
import re
-
-
first_url =
"http://www.quanshuwang.com/book/44/44683"
-
-
# 获取小说主页面HTML
-
html = request.urlopen(first_url).read().decode(
'gbk')
-
-
div_info = re.findall(
r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)
-
print(div_info)
我们进一步发现,返回的列表是不为空了,但是得到的列表中还有好多没用的标签,
于是想要把它们剔除掉,即只保留其中的<a>……</a>标签。
这时我们只需加一句代码就可以实现,原理和前面正则表达式获取字符串一样,这里给出核心部分代码:
-
div_info = re.findall(
r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)[
0]
-
tag_a = re.findall(
r'<a.*?</a>',div_info)
这时输出结果达到了我们的预期,即仅获取所有(近700章节)的<a>……</a>标签。
接下来要做的就是在得到的此列表中取出每一个<a>……</a>标签,并返回对应章节名字及其url 。
首先获得第一个<a>……</a>标签中的章节url,这里给出核心代码:
-
div_info = re.findall(
r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)[
0]
-
tag_a = re.findall(
r'<a.*?</a>',div_info)
-
chapter_url = re.findall(
r'href="(.*?)"',tag_a[
0])[
0]
-
print(chapter_url)
为了显示更清晰一点的话,我们还要获得对应 url 的章节名字,这里亦给出核心代码:
-
div_info = re.findall(
r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)[
0]
-
tag_a = re.findall(
r'<a.*?</a>',div_info)
-
chapter_url = re.findall(
r'href="(.*?)"',tag_a[
0])[
0]
-
chapter_title = re.findall(
r'title="(.*?)"',tag_a[
0])[
0]
-
print(chapter_title)
-
print(chapter_url)
下面获取近七百个章节的 url
使用for 循环遍历核心代码段即可(这里循环650次获取前六百五十章的所有章节名字及url)
-
from urllib
import request
-
import re
-
-
first_url =
"http://www.quanshuwang.com/book/44/44683"
-
-
#访问小说首页面 对象.函数(参数).调用对象返回的方法
-
html = request.urlopen(first_url).read().decode(
'gbk')
-
# 小说的信息
-
novel_info = {}
-
# 小说的名字
-
novel_info[
'title'] = re.findall(
r'<div class="chapName">.*?<strong>(.*?)</strong>',html)
-
# 作者名字
-
#novel_info['author'] = re.findall(r'<div class="chapName"><span class="r">(.*?)</span>',html)[0]
-
for i
in range(
650):
-
div_info = re.findall(
r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)[
0]
-
tag_a = re.findall(
r'<a.*?</a>',div_info)
-
chapter_url = re.findall(
r'href="(.*?)"',tag_a[i])[
0]
-
chapter_title = re.findall(
r'title="(.*?)"',tag_a[i])[
0]
-
print(chapter_title)
-
print(chapter_url)
再进一步,进入到每篇文章内部,同样分析网址,可以得到该章节内所有文字
这里以《引子》为例,部分提取其中文字,这里直接给出代码
-
from urllib
import request
-
import re
-
-
first_url =
"http://www.quanshuwang.com/book/44/44683/15379609.html"
-
-
html = request.urlopen(first_url).read().decode(
'gbk')
-
-
for i
in range(
10):
-
div_info = re.findall(
r'<div class="mainContenr" id="content">(.*?) <div style="margin:6px 1px 6px 1px;text-align:center;">', html, re.S | re.I)[
0]
-
tag_a = re.findall(
r'</script> (.*?)<script type="text/javascript">', div_info,re.S)
-
chapter_contant = re.findall(
r'\r\n<br />\r\n  (.*?)<br />\r\n<br />',tag_a[
0],re.S)[i]
-
print(chapter_contant)
就按照此思路一步一步分析,由于目标程序太多,运行要得到最终结果需要等较长时间,故这里只给出最终代码,最后结果即文章全部内容输出到显示频上,读者感兴趣可亲自尝试。
-
from urllib
import request
-
import re
-
-
first_url =
"http://www.quanshuwang.com/book/44/44683"
-
-
html = request.urlopen(first_url).read().decode(
'gbk')
-
-
for i
in range(
650):
-
div_info = re.findall(
r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)[
0]
-
tag_a = re.findall(
r'<a.*?</a>',div_info)
-
chapter_url = re.findall(
r'href="(.*?)"',tag_a[i])[
0]
-
chapter_title = re.findall(
r'title="(.*?)"',tag_a[i])[
0]
-
for j
in range(
15):
-
content_html = request.urlopen(chapter_url).read().decode(
'gbk')
-
content_div_info = re.findall(
r'<div class="mainContenr" id="content">(.*?) <div style="margin:6px 1px 6px 1px;text-align:center;">',content_html, re.S | re.I)[
0]
-
contant_tag_a = re.findall(
r'</script> (.*?)<script type="text/javascript">', content_div_info, re.S)
-
chapter_contant = re.findall(
r'\r\n<br />\r\n  (.*?)<br />\r\n<br />', contant_tag_a[
0], re.S)[j]
-
print(chapter_contant)
至此,正则表达式的入门已经讲解完毕了,由于正则表达式元字符着实太多,仅仅这点篇幅是不可能全部介绍完的,所以这里只介绍思想,感兴趣读者可自行查阅相关元字符,进一步操作。
这里给出常见的部分元字符供大家参考:
3 学以致用
学习了正则表达式,并系统学习了正则表达式在应用中的实例。接下来,我们再举一个例子对正则表达式做进一步的理解。
b站视频选取的是5月7日的热门视频:我的NBA手办真的会打球!!!
第一步,F12键找到弹幕对应的list标签。
打开list标签 Request URL,得到一条条的弹幕,这正是我们所谓的目标页面。
第二步,获取弹幕网URL,采用正则表达式的匹配模式,得到所有弹幕并输出到指定文件夹的指定文件中。
-
import requests
#发出请求
-
import re
#内置库 用于匹配正则表达式
-
import csv
#文件格式
-
-
# 获取到的弹幕网URL
-
url =
'https://api.bilibili.com/x/v1/dm/list.so?oid=186621670'
-
-
# 设置请求头 伪装浏览器
-
headers = {
-
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
-
}
-
-
# 发起请求 获得响应
-
response = requests.get(url,headers=headers)
-
html_doc = response.content.decode(
'utf-8')
-
-
# 正则表达式的匹配模式
-
res = re.compile(
'<d.*?>(.*?)</d>')
-
# 根据模式提取网页数据
-
danmu = re.findall(res,html_doc)
-
print(danmu)
-
-
for i
in danmu:
-
with open(
'F:/b站视频弹幕.txt',
'a',newline=
'',encoding=
'utf-8-sig')
as file:
-
writer = csv.writer(file)
-
danmu = []
-
danmu.append(i)
-
writer.writerow(danmu)
第三步,采用jieba库分词,并用wordcloud库美化得到图片文件。
-
import jieba
#中文分词
-
import wordcloud
#绘制词云
-
-
f = open(
'F:/b站视频弹幕.txt',encoding=
'utf-8')
# 打开刚刚获取到的所有弹幕包含在的txt文件
-
-
txt = f.read()
-
txt_list = jieba.lcut(txt)
-
string =
' '.join((txt_list))
-
print(string)
-
-
-
w = wordcloud.WordCloud(width=
1000,
-
height=
700,
-
background_color=
'white',
-
font_path=
'C:/Windows/SIMLI.TTF',
-
scale=
15,
-
stopwords={
' '},
-
contour_width=
5,
-
contour_color=
'red'
-
)
-
-
w.generate(string)
-
w.to_file(
'bzhanwordcloud.png')
当然,我们还可以按照已有的图片绘制出图片内图形样式的词云图:
比如通过下图的蓝球图片,得到其对应样式的词云图。
下面给出实现上述操作的全部代码,谨供参考:
-
import imageio
as imageio
#加载图片
-
import requests
#发出请求
-
import re
#内置库 用于匹配正则表达式
-
import csv
#文件格式
-
import jieba
#中文分词
-
import wordcloud
#绘制词云
-
-
# 目标网站(即我们获取到的URL)
-
url =
'https://api.bilibili.com/x/v1/dm/list.so?oid=186803402'
-
-
# 设置请求头 伪装浏览器
-
headers = {
-
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
-
}
-
-
# 发起请求 获得响应
-
response = requests.get(url,headers=headers)
-
html_doc = response.content.decode(
'utf-8')
-
-
# 正则表达式的匹配模式
-
res = re.compile(
'<d.*?>(.*?)</d>')
-
# 根据模式提取网页数据
-
danmu = re.findall(res,html_doc)
-
-
# 保存数据
-
for i
in danmu:
-
with open(
'b站弹幕.csv',
'a',newline=
'',encoding=
'utf-8-sig')
as file:
-
writer = csv.writer(file)
-
danmu = []
-
danmu.append(i)
-
writer.writerow(danmu)
-
-
# 显示数据
-
-
f = open(
'F:/b站视频弹幕.txt',encoding=
'utf-8')
-
-
txt = f.read()
-
txt_list = jieba.lcut(txt)
-
# print(txt_list)
-
string =
' '.join((txt_list))
-
print(string)
-
-
# 很据得到的弹幕数据绘制词云图
-
-
mk = imageio.imread(
r'F:/basketball.png')
-
-
w = wordcloud.WordCloud(width=
1000,
-
height=
700,
-
background_color=
'white',
-
font_path=
'C:/Windows/SIMLI.TTF',
-
mask=mk,
-
scale=
15,
-
stopwords={
' '},
-
contour_width=
5,
-
contour_color=
'red'
-
)
-
-
w.generate(string)
-
w.to_file(
'gaijinwordcloud.png')
代码运行最终效果展示:
这里附录一篇我写的关于爬虫的原创文章:Python爬虫:10行代码真正实现“可见即可爬”
以及在安装python第三方库方面还迷茫的小伙伴可以参考我的原创博文:一文教你安遍所有python第三方库
大家也可以关注我原创的分类专栏:
①在王者荣耀角度下看程序设计模式(共25篇,已更新完)
②《数字图像处理》学习笔记(更新中……)
③《机器学习》学习笔记(更新中……)
更多原创文章请点击我的→主页
★版权声明:本文为CSDN博主「IT_change」的原创文章,遵循CC 4.0 BY-SA版权协议。
转载请附上原文出处链接及本声明。
感谢阅读 ! 感谢支持 ! 感谢关注 !
希望本文能对读者学习正则表达式和使用爬虫技术有所帮助,并请读者批评指正!
2020年5月于山西大同
END
转载:https://blog.csdn.net/IT_charge/article/details/105977578