大鹏一日同风起,扶摇直上九万里。这篇文章主要讲述Python 从底层结构聊 Beautiful Soup4(内置豆瓣最新电影排行榜爬取案例)!相关的知识,希望能为你提供帮助。
1. 前言什么是 Beautiful Soup 4 ?
Beautiful Soup 4(简称 BS4,后面的 4 表示最新版本)是一个 Python 第三方库,具有解析 HTML 页面的功能,爬虫程序可以使用 BS4 分析页面无素、精准查找出所需要的页面数据。有 BS4 的爬虫程序爬行过程惬意且轻快。
BS4 特点是功能强大、使用简单。相比较只使用正则表达式的费心费力,BS4 有着弹指一挥间的豪迈和潇洒。
2. 安装Beautiful Soup 4BS4 是 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 = "BeautifulSoup 4 简介"
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)
- features: 指定解析器程序。解析器是 BS4 的灵魂所在,否则 BS4 就是一个无本之源的空壳子。
BS4 支持 Python 内置的 HTML 解析器 ,还支持第三方解析器:lxml、 html5lib……
安装 lxml :
pip install lxml
安装 html5lib:
pip install html5lib
几种解析器的纵横比较:
解析器 | 使用方法 | 优势 | 劣势 |
---|---|---|---|
Python标准库 | BeautifulSoup(markup, " html.parser" ) | 执行速度适中 < br /> 文档容错能力强 | Python 2.7.3 or 3.2.2 前的版本文档容错能力差 |
lxml HTML 解析器 | BeautifulSoup(markup, " lxml" ) | 速度快< br /> 文档容错能力强 | 需要 C 语言库的支持 |
lxml XML 解析器 | BeautifulSoup(markup, [" lxml-xml" ]) BeautifulSoup(markup, " xml" ) | 速度快 < br /> 唯一支持 XML 的解析器 | 需要 C 语言库的支持 |
html5lib | BeautifulSoup(markup, " html5lib" ) | 最好的容错性 < br /> 以浏览器的方式解析文档 < br /> 生成HTML5格式的文档 | 速度慢< br /> 不依赖外部扩展 |
2.2 解析器的差异性
【Python 从底层结构聊 Beautiful Soup4(内置豆瓣最新电影排行榜爬取案例)!】解析器的功能是加载 HTML(XML) 代码,在内存中构建一棵层次分明的对象树(后面简称BS 树)。虽然 BS4 从应用层面统一了各种解析器的使用规范,但各有自己的底层实现逻辑。
当然,解析器在解析格式正确、完全符合 HTML 语法规范的文档时,除了速度上的差异性,大家表现的还是可圈可点的。想想,这也是它们应该提供的最基础功能。
但是,当文档格式不标准时,不同的解析器在解析时会遵循自己的底层设计,会弱显出差异性。
2.2.1 lxml使用 lxml解析" < a> < p> < p> " 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解析" < a> < /p> " 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 解析" < a> < p> < p> " 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 解析" < a> < /p> " 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 对象。
- NavigableString对象 是对 HTML 标签中所包含的内容体的映射,提供有对文本信息操作的方法和属性。
- Comment 是对文档注释内容的映射对象。此对象用的不多。
文章图片
为了更好的以一个节点找到其它节点,需要理解节点与节点的关系:主要有父子关系、兄弟关系。
现以一个案例逐一理解每一个对象的作用。
案例描述:爬取豆瓣电影排行榜上的最新电影信息。(https://movie.douban.com/chart),并以CSV 文档格式保存电影信息。
3.1 查找目标 Tag
获取所需数据的关键就是要找到目标 Tag。BS4 提供有丰富多变的方法能帮助开发者快速、灵活找到所需 Tag 对象。通过下面的案例,让我们感受到它的富裕变化多端的魔力。
先获取豆瓣电影排行榜的入口页面路径 https://movie.douban.com/chart 。
使用谷歌浏览器浏览页面,使用浏览器提供的开发者工具分析一下页面中电影信息的 HTML 代码片段。 由简入深,从下载第一部电影的信息开始。
文章图片
先下载第一部电影的图片和电影名。图片当然使用的是 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>
<
imgclass="" src="http://img.readke.com/220609/0G5035133-3.jpg" />
这里有一个运气成分,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: /uploads/allimg/220609/0G5035133-3.jpg, width: 75, alt: 青春变形记, class: []
单值属性返回的是单值,因 class 属性(多值属性)可以设置多个类样式,返回的是一个数组。现在只想得到图片的路径,可以使用如下方式。
img_tag_attrs = img_tag.attrs
# 第一种方案
img_tag_src=https://www.songbingjia.com/android/img_tag_attrs["src"]
# 第二种方案
img_tag_src = https://www.songbingjia.com/android/img_tag["src"]
print(img_tag_src)输出结果
/uploads/allimg/220609/0G5035133-3.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=https://www.songbingjia.com/android/img_tag_attrs["src"]
img_tag_src = https://www.songbingjia.com/android/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 rel="nofollow" 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 rel="nofollow" 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( ) 等过滤方法。此类方法的作用如其名可以在一个群体(所有子节点)中根据个体的特征进行筛选。
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")
- 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 rel="nofollow" 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_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 获取属性值。
- 使用获取到的数据。
如上仅仅是找到了第一部电影的信息。如果需要查找到所有电影信息,则只需要在上面代码的基础之上添加迭代便可。
from bs4 import BeautifulSoup
import requests
import csvall_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 了。
推荐阅读
- ASP.NET Core 自动刷新JWT Token #yyds干货盘点#
- 游戏开发新手入门教程13:从想法到设计的过程
- 多图预警! Multi-HeadAttention | 多头注意力#51CTO博主之星评选#
- #yyds干货盘点#剑指 Offer 04. 二维数组中的查找
- #yyds干货盘点# 解决华为机试(配置文件恢复)
- 在 Flutter 中使用 NavigationRail 和 BottomNavigationBar
- 全面解读 AWS Private 5G 的革新理念
- 你在51CTO博客留下了哪些足迹(悟空熊时光机,带你开启专属回忆)
- 数据库与缓存数据一致性解决方案