1. 前言
什么是 Beautiful Soup 4?
Beautiful Soup 4(简称 BS4,前面的 4 示意最新版本)是一个 Python 第三方库,具备解析 HTML 页面的性能,爬虫程序能够应用 BS4 剖析页面无素、精准查找出所须要的页面数据。有 BS4 的爬虫程序匍匐过程惬意且轻快。
BS4 特点是功能强大、应用简略。相比拟只应用正则表达式的费神费劲,BS4 有着弹指一挥间的豪放和洒脱。
2. 装置 Beautiful Soup 4
BS4 是 Python 第三库,应用之前须要装置。
pip install beautifulsoup4
2.1 BS4 的工作原理
要真正意识、把握 BS4,则须要对其底层工作机制有所理解。
BS4 查找页面数据之前,须要加载 HTML 文件 或 HTML 片段,并在内存中构建一棵与 HTML 文档齐全一一映射的树形对象(相似于 W3C 的 DOM 解析。为了不便,前面简称 BS 树),这个过程称为解析。
BS4 本身并没有提供解析的实现,而是提供了接口,用来对接第三方的解析器(这点是很牛逼的,BS4 具备很好的扩展性和开发性)。无论应用何种解析器,BS4 屏蔽了底层的差异性,对外提供了对立的操作方法(查问、遍历、批改、增加……)。
意识 BS4 先从结构 BeautifulSoup 对象开始。BeautifulSoup 是对整个文档树的援用,或是进入文档树的入口对象。
剖析 BeautifulSoup 构造方法,可发现在结构 BeautifulSoup 对象时,能够传递很多参数。但个别只须要思考前 2 个参数。其它参数采纳默认值,BS4 就能工作很好(约定大于配置的榜样)。
def __init__(self, markup="", features=None, builder=None,
parse_only=None, from_encoding=None, exclude_encodings=None,element_classes=None, **kwargs):
- markup: HTML 文档。能够是字符串格局的 HTML 片段、也能够是一个文件对象。
from bs4 import BeautifulSoup
# 应用 HTML 代码片段
html_code = "<h1>BeautifulSoup 4 简介 </h1>"
bs = BeautifulSoup(html_code, "lxml")
print(bs)
以下应用文件对象做为参数。
from bs4 import BeautifulSoup
file = open("d:/hello.html", encoding="utf-8")
bs = BeautifulSoup(file, "lxml")
print(bs)
Tip: 应用文件对象时,编码方式请抉择 unicode 编码(utf-8 是 unicode 的具体实现)。
-
features: 指定解析器程序。解析器是 BS4 的灵魂所在,否则 BS4 就是一个无本之源的空壳子。
BS4 反对 Python 内置的 HTML 解析器,还反对第三方解析器:lxml、html5lib……
Tip: 任何人都能够定制一个本人的解析器,但请务必遵循 BS4 的接口标准。
所以说即便 谷歌浏览器 的解析引擎很牛逼,但因和 BS4 接口不吻合,彼此之间也只能惺惺相惜一番。
如果要应用是第三方解析器,应用之前请提前装置:
装置 lxml:
pip install lxml
装置 html5lib:
pip install html5lib
几种解析器的纵横比拟:
解析器 | 应用办法 | 劣势 | 劣势 |
---|---|---|---|
Python 规范库 | BeautifulSoup(markup, “html.parser”) | 执行速度适中 文档容错能力强 |
Python 2.7.3 or 3.2.2 前的版本文档容错能力差 |
lxml HTML 解析器 | BeautifulSoup(markup, “lxml”) | 速度快 文档容错能力强 |
须要 C 语言库的反对 |
lxml XML 解析器 | BeautifulSoup(markup, [“lxml-xml”]) BeautifulSoup(markup, “xml”) | 速度快 惟一反对 XML 的解析器 |
须要 C 语言库的反对 |
html5lib | BeautifulSoup(markup, “html5lib”) | 最好的容错性 以浏览器的形式解析文档 生成 HTML5 格局的文档 |
速度慢 不依赖内部扩大 |
每一种解析器都有本人的长处,如 html5lib 的容错性就十分好,但个别优先应用 lxml 解析器,更多时候速度更重要。
2.2 解析器的差异性
解析器的性能是加载 HTML(XML)代码,在内存中构建一棵层次分明的对象树(前面简称 BS 树)。尽管 BS4 从利用层面对立了各种解析器的应用标准,但各有本人的底层实现逻辑。
当然,解析器在解析格局正确、完全符合 HTML 语法标准的文档时,除了速度上的差异性,大家体现的还是可圈可点的。想想,这也是它们应该提供的最根底性能。
然而,当文档格局不规范时,不同的解析器在解析时会遵循本人的底层设计,会弱显出差异性。
看来,BS4 也无奈主持人家底层逻辑的差异性。
2.2.1 lxml
应用 lxml 解析 HTML 代码段。
from bs4 import BeautifulSoup
html_code = "<a><p><p>"
bs = BeautifulSoup(html_code, "lxml")
print(bs)
'''
输入后果
<html><body><a><p></p><p></p></a></body></html>
'''
lxml 在解析时,会主动增加上 html、body 标签。并主动补全没有 完结语法结构 的标签。如上 a 标签是前面 2 个标签的父标签,第一个 p 标签是第二 p 标签的为兄弟关系。
应用 lxml 解析如下 HTML 代码段。
from bs4 import BeautifulSoup
html_code = "<a></p>"
bs = BeautifulSoup(html_code, "lxml")
print(bs)
'''
输入后果
<html><body><a></a></body></html>
'''
lxml 会认定 只有完结语法没有开始语法 的标签构造是非法的,回绝解析(也是挺刚的)。即便是非法,抛弃是天经地义的。
2.2.2 html5lib
应用 html5lib 解析不残缺的 HTML 代码段。
from bs4 import BeautifulSoup
html_code = "<a><p><p>"
bs = BeautifulSoup(html_code, "html5lib")
print(bs)
'''
输入后果
<html><head></head><body><a><p></p><p></p></a></body></html>
'''
html5lib 在解析 j 时,会主动加上 html、head、body 标签。除此之外如上解析后果和 lxml 没有太大区别,在没有完结标签语法上,大家还是英雄所见略同的。
应用 html5lib 解析上面的 HTML 代码段。
from bs4 import BeautifulSoup
html_code = "<a></p>"
bs = BeautifulSoup(html_code, "html5lib")
print(bs)
'''
输入后果:
<html><head></head><body><a><p></p></a></body></html>
'''
html5lib 对于没有 完结语法结构 的标签,会为其补上 开始语法结构,html5lib 遵循的是 HTML5 的局部规范。意思是既然都来了,也就不要走了,html5lib 都会尽可能补全。
2.2.3 pyhton 内置解析器
from bs4 import BeautifulSoup
html_code = "<a><p><p>"
bs = BeautifulSoup(html_code, "html.parser")
print(bs)
'''
输入后果
<a><p><p></p></p></a>
'''
与后面 2 类解析器相比拟,没有增加 <html>、<head>、<body> 任一标签,会主动补全完结标签构造。但最终构造与前 2 类解析器不同。a 标签是后 2 个标签的父亲,第一个 p 标签是第二个 p 标签的父亲,而不是兄弟关系。
演绎可知:对于 lxml、html5lib、html.parser 而言,对于没有 完结语法结构 的标签都认为是能够辨认的。
from bs4 import BeautifulSoup
html_code = "<a></p>"
bs = BeautifulSoup(html_code, "html.parser")
print(bs)
'''
输入后果
<a></a>
'''
对于没有开始语法结构的标签的解决和 lxml 解析器类似,会抛弃掉。
从下面的代码的运行后果可知,html5lib 的容错能力是最强的,在对于文档要求不高的场景下,可思考应用 html5lib。在对文档格局要求高的利用场景下,可抉择 lxml。
3. BS4 树对象
BS4 内存树 是对 HTML 文档或代码段的内存映射,内存树由 4 种类型的 python 对象组成。别离是 BeautifulSoup、Tag、NavigableString 和 Comment。
-
BeautifulSoup 对象 是对整个 html 文档构造的映射,提供对整个 BS4 树操作的全局办法和属性。也是入口对象。
class BeautifulSoup(Tag): pass
-
Tag 对象(标签对象) 是对 HTML 文档中标签的映射,或称其为节点(对象名与标签名一样)对象,提供对页面标签操作的办法和属性。实质上 BeautifulSoup 对象也 Tag 对象。
Tip: 解析页面数据的要害,便是找到蕴含内容的 标签对象(Tag)。BS4 提供了很多灵便、简洁的办法。
应用 BS4 就是以 BeautifulSoup 对象开始,逐渐查找指标标签对象的过程。
-
NavigableString 对象 是对 HTML 标签中所蕴含的内容体的映射,提供有对文本信息操作的办法和属性。
Tip: 对于开发者而言,剖析页面,最终就要要获取数据,所以,把握此对象的办法和属性尤为重要。
应用 标签对象的 string 属性就能够获取。
- Comment 是对文档正文内容的映射对象。此对象用的不多。
再总结一下:应用 BS4 的的要害就是如何以一个 Tag 对象(节点对象)为参考,找到与其关联的其它 Tag 对象。刚开始出场时就一个 BeautifulSoup 对象。
为了更好的以一个节点找到其它节点,须要了解节点与节点的关系:次要有父子关系、兄弟关系。
现以一个案例逐个了解每一个对象的作用。
案例形容:爬取豆瓣电影排行榜上的最新电影信息。(https://movie.douban.com/chart),并以 CSV 文档格局保留电影信息。
3.1 查找指标 Tag
获取所需数据的要害就是要找到 指标 Tag。BS4 提供有丰盛多变的办法能帮忙开发者疾速、灵便找到所需 Tag 对象。通过上面的案例,让咱们感触到它的富裕变化多端的魔力。
先获取豆瓣电影排行榜的入口页面门路 https://movie.douban.com/chart。
应用谷歌浏览器浏览页面,应用浏览器提供的开发者工具剖析一下页面中电影信息的 HTML 代码片段。由简入深,从下载第一部电影的信息开始。
Tip: 这个排行榜随时变动,大家所看到的第一部电影和下图可能不一样。
竟然应用的是表格布局。表格布局十分有规定,这对于剖析构造十分无利。
先下载第一部电影的图片和电影名。图片当然应用的是 img 标签,应用 BS4 解析后,BS4 树 上会有一个对应的 img Tag 对象。
树上的 img Tag 对象有很多,怎么找到第一部电影的图片标签?
from bs4 import BeautifulSoup
import requests
# 服务器地址
url = "https://movie.douban.com/chart"
# 伪装成浏览器
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36'}
# 发送申请
resp = requests.get(url, headers=headers)
html_code = resp.text
# 失去 BeautifulSoup 对象。万里长征的第一步。bs = BeautifulSoup(html_code, "lxml")
# 要取得 BS4 树上的 Tag 对象,最简略的办法就是间接应用标签名。简略的不要不要的。img_tag = bs.img
# 返回的是 BS4 树上的第一个 img Tag 对象
print(type(img_tag))
print(img_tag)
'''
输入后果
<class 'bs4.element.Tag'>
<img alt="青春变形记" class=""src="https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2670448229.jpg"width="75"/>'''
这里有一个运气成分,bs.img 返回的恰好是第一部电影的图片标签(也意味着第一部电影的图片标签是整个页面的第一个图片标签)。
找到了 img 标签对象,再剖析出其图片门路就容易多了,图片门路存储在 img 标签的 src 属性中,当初只须要获取到 img 标签对象的 src 属性值就能够了。
Tag 对象提供有 attrs 属性,能够很容易失去一个 Tag 对象的任一属性值。
应用语法:
Tag["属性名"]或者应用 Tag.attrs 获取到 Tag 对象的所有属性。
上面应用 atts 获取标签对象的所有属性信息,返回的是一个 python 字典对象。
# 省略下面代码段
img_tag_attrs = img_tag.attrs
print(img_tag_attrs)
'''
输入后果:以字典格局返回 img Tag 对象的所有属性
{'src': 'https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2670448229.jpg', 'width': '75', 'alt': '青春变形记', 'class': []}
'''
单值属性返回的是单值,因 class 属性(多值属性)能够设置多个类款式,返回的是一个数组。当初只想得到图片的门路,能够应用如下形式。
img_tag_attrs = img_tag.attrs
# 第一种计划
img_tag_src=img_tag_attrs["src"]
# 第二种计划
img_tag_src = img_tag["src"]
print(img_tag_src)
'''
输入后果
https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2670448229.jpg
'''
上述代码中提供 2 种计划,其本质是一样的。有了图片门路,剩下的事件就好办了。
残缺的代码:
from bs4 import BeautifulSoup
import requests
# 服务器地址
url = "https://movie.douban.com/chart"
# 伪装成浏览器
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36'}
# 发送申请
resp = requests.get(url, headers=headers)
html_code = resp.text
bs = BeautifulSoup(html_code, "lxml")
img_tag = bs.img
# img_tag_attrs = img_tag.attrs
# img_tag_src=img_tag_attrs["src"]
img_tag_src = img_tag["src"]
# 依据图片门路下载图片并保留到本地
img_resp = requests.get(img_tag_src, headers=headers)
with open("D:/movie/movie01.jpg", "wb") as f:
f.write(img_resp.content)
3.2 过滤办法
失去图片后,怎么失去电影的名字,以及其简介。如下为电影名的代码片段。
<a href="https://movie.douban.com/subject/35284253/" class=""> 青春变形记 / <span style="font-size:13px;"> 熊抱青春记(港) / 青春养成记(台)</span></a>
电影名蕴含在一个 a 标签中。如上所述,当应用 bs. 标签名 时,返回的是整个页面代码段中的第一个同名标签对象。
显然,第一部电影名所在的 a 标签不可能是页面中的第一个(否则就是运气爆棚了),无奈间接应用 bs.a 获取电影名所在 a 标签,且此 a 标签也无特地显著的能够辨别和其它 a 标签不一样的特色。
这里就要想点其它方法。以此 a 标签向上找到其父标签 div。
<div class="pl2">
<a href="https://movie.douban.com/subject/35284253/" class=""> 青春变形记 / <span style="font-size:13px;"> 熊抱青春记(港) / 青春养成记(台)</span>
</a>
<p class="pl">2022-03-11(美国网络) / 姜晋安 / 吴珊卓 / 艾娃·摩士 / 麦特里伊·拉玛克里斯南 / 朴惠仁 / 奥赖恩·李 / 何炜晴 / 特里斯坦·艾瑞克·陈 / 吴汉章 / 菲尼亚斯·奥康奈尔 / 乔丹·费舍 / 托菲尔 - 恩戈 / 格雷森·维拉纽瓦 / 乔什·列维 / 洛瑞·坦·齐恩...</p>
<div class="star clearfix">
<span class="allstar40"></span>
<span class="rating_nums">8.2</span>
<span class="pl">(45853 人评估)</span>
</div>
</div>
同理,div 标签在整个页面代码中也有很多,又如何获到到电影名所在的 div 标签,剖析发现此 div 有一个与其它 div 不同的属性特色。class=”pl2″。能够通过这个属性特色对 div 标签进行过滤。
什么是过滤办法?
过滤办法是 BS4 Tag 标签对象的办法,用来对其子节点进行筛选。
BS4 提供有 find()、find_all() 等过滤办法。此类办法的作用如其名能够在一个群体(所有子节点)中依据个体的特色进行筛选。
Tip: 如果应用 BeautifulSoup 对象 调用这类办法,则是对整个 BS4 树上的节点进行筛选。
如果以某一个具体的 Tag 标签对象调用此类办法以,则是对 Tag 标签下的子节点进行筛选。
find()和 find_all() 办法的参数是一样的。两者的区别:前者搜寻到第一个满足条件就返回,后者会搜寻所有满足条件的对象。
find_all(name , attrs , recursive , string , **kwargs)
find(name , attrs , recursive , string , **kwargs)
参数阐明
- name: 能够是标签名、正则表达式、列表、布尔值或一个自定义办法。变化多端。
# 标签名:查找页面中的第一个 div 标签对象
div_tag = bs.find("div")
# 正则表达式:搜寻所有以 d 开始的标签
div_tag = bs.find_all(re.compile("^d"))
# 列表:查问 div 或 a 标签
div_tag = bs.find_all(["div","a"])
# 布尔值:查找所有子节点
bs.find_all(True)
#自定义办法:搜寻有 class 属性而没有 id 属性的标签对象。def has_class_but_no_id(tag):
return tag.has_attr('class') and not tag.has_attr('id')
bs.find_all(has_class_but_no_id)
- attrs: 能够接管一个字典类型。以键、值对的形式形容要搜寻的标签对象的属性特色。
# 在整个树后果中查问 class 属性值是 pl2 的标签对象
div_tag = bs.find(attrs={"class": "pl2"})
Tip: 应用此属性时,能够联合 name 参数把范畴收窄。
div_tag = bs.find("div",attrs={"class": "pl2"})
查找 class 属性值是 pl2 的 div 标签对象。
- string 参数: 此参数能够是 字符串、正则表达式、列表 布尔值。通过标签内容匹配查找。
# 搜寻标签内容是 '青春' 2 字结尾的 span 标签对象
div_tag = bs.find_all("span", string=re.compile(r"青春.*"))
- limit 参数: 能够应用
limit
参数限度返回后果的数量。 - recursive 参数: 是否递归查问节点上面的子节点,默认 是 True,设置 False 时,只查问间接子节点。
简略介绍过滤办法后,从新回到问题上来,查问第一部电影的电影名、简介。灵便应用过滤办法,则能很轻松搜寻到所须要的标签对象。
from bs4 import BeautifulSoup
import requests
# 服务器地址
url = "https://movie.douban.com/chart"
# 伪装成浏览器
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36'}
# 发送申请
resp = requests.get(url, headers=headers)
html_code = resp.text
# 使得解析器构建 BeautifulSoup 对象
bs = BeautifulSoup(html_code, "lxml")
# 应用过滤办法在整个树结构中查找 class 属性值为 pl2 的 div 对象。其实有多个,这里查找第一个
div_tag = bs.find("div", class_="pl2")
# 查问 div 标签对象下的第一个 a 标签
div_a = div_tag.find("a")
# 失去 a 标签下所有子节点
name = div_a.contents
# 失去 文本
print(name[0].replace("/", '').strip())'''
输入后果:青春变形记
'''
代码剖析:
- 应用 bs.find(“div”, class_=”pl2″) 办法搜寻到蕴含第一部电影的 div 标签。
- 电影名蕴含在 div 标签的子标签 a 中,持续应用 div_tag.find(“a”) 找到 a 标签。
<a href="https://movie.douban.com/subject/35284253/" class=""> 青春变形记 / <span style="font-size:13px;"> 熊抱青春记(港) / 青春养成记(台)</span>
</a>
- a 标签中的内容就是电影名。BS4 为标签对象提供有 string 属性,能够获取其内容,返回 NavigableString 对象。然而如果标签中既有文本又有子标签时,则不能应用 string 属性。如上 a 标签的 string 返回为 None。
- 在 BS4 树结构中文本也是节点,能够以子节点的形式获取。标签对象有 contents 和 children 属性获取子节点。前者返回一个列表,后者返回一个迭代器。另有 descendants 能够获取其间接子节点和孙子节点。
- 应用 contents 属性,从返回的列表中获取第一个子节点,即文本节点。文本节点没有 string 属性。
获取电影简介相对而言就简略的多,其内容蕴含在 div 标签的 p 子标签中。
# 获取电影的简介
div_p = div_tag.find("p")
movie_desc = div_p.string.strip()
print(movie_desc)
上面能够把电影名和电影简介以 CSV 的形式保留在文件中。残缺代码:
from bs4 import BeautifulSoup
import requests
import csv
# 服务器地址
url = "https://movie.douban.com/chart"
# 伪装成浏览器
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36'}
# 发送申请
resp = requests.get(url, headers=headers)
html_code = resp.text
bs = BeautifulSoup(html_code, "lxml")
div_tag = bs.find("div", class_="pl2")
div_a = div_tag.find("a")
div_a_name = div_a.contents
# 电影名
movie_name = div_a_name[0].replace("/", '').strip()
# 获取电影的简介
div_p = div_tag.find("p")
movie_desc = div_p.string.strip()
with open("d:/movie/movies.csv", "w", newline='') as f:
csv_writer = csv.writer(f)
csv_writer.writerow(["电影名", "电影简介"])
csv_writer.writerow([movie_name, movie_desc])
是时候小结了,应用 BS4 的根本流程:
- 通过指定解析器获取到 BS4 对象。
- 指定一个标签名获取到标签对象。如果无奈间接获取所须要的标签对象,则应用过滤器办法进行一层一层向下过滤。
- 找到指标标签对象后,能够应用 string 属性获取其中的文本,或应用 atrts 获取属性值。
- 应用获取到的数据。
3.3 遍历所有的指标
如上仅仅是找到了第一部电影的信息。如果须要查找到所有电影信息,则只须要在下面代码的根底之上增加迭代便可。
from bs4 import BeautifulSoup
import requests
import csv
all_movies = []
# 服务器地址
url = "https://movie.douban.com/chart"
# 伪装成浏览器
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36'}
# 发送申请
resp = requests.get(url, headers=headers)
html_code = resp.text
bs = BeautifulSoup(html_code, "lxml")
# 查找到所有 <div class="pl2"></div>
div_tag = bs.find_all("div", class_="pl2")
for div in div_tag:
div_a = div.find("a")
div_a_name = div_a.contents
# 电影名
movie_name = div_a_name[0].replace("/", '').strip()
# 获取电影的简介
div_p = div.find("p")
movie_desc = div_p.string.strip()
all_movies.append([movie_name, movie_desc])
with open("d:/movie/movies.csv", "w", newline='') as f:
csv_writer = csv.writer(f)
csv_writer.writerow(["电影名", "电影简介"])
for movie in all_movies:
csv_writer.writerow(movie)
本文次要解说 BS4 的应用,仅爬取了电影排行榜的第一页数据。至于数据到手后,如何应用,则依据利用场景来决定。
4. 总结
BS4 还提供有很多办法,能依据以后节点找到父亲节点、子节点、兄弟节点……但其原理都是一样的。只有找到了内容所在的标签(节点)对象,所有也就 OK 了。