🌟想系统化学习爬虫技术?看看这个:[数据抓取] Python 网络爬虫 - 学习手册-CSDN博客
0x01:Beautiful Soup 简介
Beautiful Soup 是一个用于从 HTML 文档或 XML 文档中提取目标数据的 Python 库。它历经了众多版本,其中 Beautiful Soup 3 已经停止开发与维护,官方推荐使用 Beautiful Soup 4(简称 bs4)进行程序开发。
为了快速解析 HTML 文档或 XML 文档的数据,bs4 不仅提供了多种类型的解析器,还支持 CSS 选择器。bs4 通过解析器可以将 HTML 或 XML 文档、片段转换成节点树、节点树中的每一个节点都对应一个 Python 类的对象。
0x02:Beautiful Soup 安装 & 使用介绍
Beautiful Soup 的安装十分简单,输入下面的 pip
命令即可:
pip install beautifulsoup4 # 这个是解析库
pip install lxml # 这个是解析器
bs4 库的使用一般分为如下 3 个步骤:
-
根据 HTML 或 XML 文档、片段创建 BeautifulSoup 类的对象。
-
通过 BeautifulSoup 类的对象的查找方法或 CSS 选择器定位节点。
-
通过访问节点的属性或节点的名称提取文本。
0x03:创建 BeautifulSoup 类的对象
0x0301:BeautifulSoup 构造方法声明解析
要想使用 bs4 解析网页数据,首先就需要使用构造方法创建 BeautifulSoup 类的对,BeautifulSoup 类的构造方法声明如下:
BeautifulSoup(markup="",
features=None,
builder=None,
parse_only=None,
from_encoding=None,
exclude_encodings=None,
element_classes=None,
**kwargs)
上述方法中的常用参数含义如下:
-
markup: 必选参数,表示待解析的内容,可以取值为字符串或类似文件的对象。
-
features: 可选参数,表示指定的解析器。该参数可以接收解析器名称或标记类型。其中,解析器名称包括 lxml、lxml-xml、html.parser 和 html5lib,标记类型包括 html、html5 和 xml。
-
parse_only: 可选参数,指定只解析部分文档。该参数需要接收一个 SoupStrainer 类的对象。当文档太大而无法全部放入内存时,便可以考虑只解析一部分文档。
-
from_encoding: 可选参数,指定待解析文档的编码格式。
值得一提的是,如果我们只需要解析 HTML 文档,那么在创建 BeautifulSoup 类的对象时就可以不用指定解析器。此时 Beautiful Soup 会根据当前系统安装的库自动选择解析器。解析器的选择顺序为 lxml => html5lib => Python 标准库,但该选择会在以下两种情况下发生变化:
-
要解析的文档是什么类型?目前支持 html、xml 和 html5。
-
指定使用哪种解析器?目前支持 lxml、html5lib 和 html.parser
如果指定的解析器没有安装,那么 Beautiful Soup 会自动选择其它方案。不过,目前只有解析器 lxml 支持 XML 文档的解析。在当前系统中没有安装 lxml 解析器的情况下,即使创建 BeautifulSoup 对象时明确指定使用解析器 lxml,也无法得到解析后的内容。
下面我们通过一张表来区分上面介绍的 4 种解析器的优势与劣势:
解析器 | 优势 | 劣势 |
---|---|---|
lxml(lxml 的 HTML 解析器) | 1. 执行速度快; 2. 文档容错能力强; | 需要安装 C 语言库 |
lxml-xml(lxml 的 XML 解析器) | 1. 执行速度快; 2. 唯一适用于 XML 文档的解析器 | 需要安装 C 语言库 |
html.parser(bs4 的 HTML 解析器) | 1. Python 内置的标准库; 2. 执行速度适中; 3. 文档容错能力强; | Python 2.7.3 或 Python 3.2.2 之前的版本中,文档容错能力差 |
html5lib | 1. 拥有 4 种解析器中最好的容错能力; 2. 以浏览器的方式解析文档; 3. 生成 HTML5 格式的文档; | 1. 执行速度慢 2. 不依赖外部扩展 |
0x0302:BeautifulSoup 构造方法演示案例
下面是对于 BeautifulSoup 构造方法的一个演示案例,保存后直接运行,控制台中会打印经过格式化处理的 HTML 页面数据:
from bs4 import BeautifulSoup
html_doc = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BeautifulSoup 入门案例</title>
</head>
<body>
<p class="title"><b>1001 页</b></p>
<p class="story">1001 页是一个很好的故事,它里面有这么几个故事:<br>
<a href="http://www.hack3rx.cn/1.html" class="link" id="new">1.html => 国王的新衣<br></a>
<a href="http://www.hack3rx.cn/2.html" class="link" id="light">2.html => 阿里巴巴和它的神灯<br></a>
</p>
<p class="story">....</p>
</body>
</html>
"""
# 根据 html_doc 创建 BeautifulSoup 类的对象,并指定使用 lxml 解析器解析文档
soup = BeautifulSoup(html_doc, features='lxml')
print(soup.prettify())
0x04:节点选取 — find() & find_all()
0x0401:find() & find_all() 声明解析
BeautifulSoup 类提供了一些基于 HTML 或 XML 节点树选取节点的方法,其中比较主流的两个方法是 find()
方法和 find_all()
方法:
-
find()
方法用于查找符合条件的第一个节点; -
find_all()
方法用于查找所有符合条件的节点,并以列表的形式返回;
由于 find()
方法与 find_all()
方法的参数相同,所以我们这里以 find_all()
方法为例进行介绍,find_all()
方法的声明如下:
find_all(self, name=None, attrs={}, recursive=True, text=None, limit=None, **kwargs)
上述方法中的各参数解析如下:
-
name: 待查找的节点名称,支持字符串、正则表达式、列表三种类型的取值。
-
字符串:查找名称与字符串完全相同的的所有节点。
-
正则表达式:查找名称符合正则表达式模式的所有节点。
-
列表:查找名称与列表中任意元素相同的所有节点。
-
-
attrs: 待查找的属性节点,接收一个字典,字典中的键为属性名称,值为该属性对应的值。
-
recursive: 是否对当前节点的所有子孙节点进行查找。
-
text: 待查找的文本节点,支持字符串、正则表达式、列表三种类型取值,作用与
name
参数一致。 -
limit: 待查找的节点数量。如果节点树非常大,查询会很慢,通过该参数可以限定查找结果的数量,一旦数量超过了
limit
的值,就会停止查找。 -
kwargs:
支持以关键字形式传递的任意一个参数。在节点树中查找节点时,会将关键字参数的名称作为节点的属性名称,值作为属性值。当我们要查询的关键字为
class时,需要特别设置为
class_。
0x0402:find() & find_all() 演示案例
以下是笔者整理的,针对 find_all()
方法的使用示例,请读者运行后细细的看,内容不多,但是只要掌握了这个,你处理起 HTML 页面就是手拿把恰:
import re
from bs4 import BeautifulSoup
html_doc = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BeautifulSoup 入门案例</title>
</head>
<body>
<h1>这个才是标题</h1>
<p class="title" name="whoami"><b>1001 页</b></p>
<p class="story">1001 页是一个很好的故事,它里面有这么几个故事:<br>
<a href="http://www.hack3rx.cn/1.html" class="link" id="new1">1<br></a>
<a href="http://www.hack3rx.cn/1.html" class="link" id="new2">2<br></a>
<a href="http://www.hack3rx.cn/2.html" class="link" id="light">2.html => 阿里巴巴和它的神灯<br></a>
</p>
<p class="story2">....</p>
</body>
</html>
"""
# 根据 html_doc 创建 BeautifulSoup 类的对象,并指定使用 lxml 解析器解析文档
soup = BeautifulSoup(html_doc, features='lxml')
print("*" * 30 + " find_all() 方法 name 参数示例 " + "*" * 30)
print(soup.find_all('title')) # 选取 title 节点
print(soup.find_all(id=re.compile('new*'))) # 选取 id 符合正则表达式的所有节点
print(soup.find_all(['title', 'h1'])) # 选取被 title 或 h1 包裹的节点
print("*" * 30 + " find_all() 方法 attrs 参数示例 " + "*" * 30)
print(soup.find_all(attrs={'id' : 'light'})) # 选取 id 属性为 light 的节点
print("*" * 30 + " find_all() 方法 revursive 参数示例 " + "*" * 30)
print(soup.find_all('title', recursive=False)) # 从 html 节点开始,不查找子节点,要求找到 title 节点 => 结果为空
print(soup.find_all('title', recursive=True)) # 从 html 节点开始,查找子节点,要求找到 title 节点
print("*" * 30 + " find_all() 方法 text 参数示例 " + "*" * 30) # => 这个不像是选择节点,更像是直接匹配内容
print(soup.find_all(text="1")) # 选取文本为 1 的节点
print(soup.find_all(text=re.compile('\d'))) # 选取以数字开头的节点
print(soup.find_all(text=['1', '2'])) # 选取节点文本内容要么为 1,要么为 2 的节点
print("*" * 30 + " find_all() 方法 limit 参数示例 " + "*" * 30)
print(soup.find_all('a', limit=1)) # 选取至多 1 个 a 标签
print("*" * 30 + " find_all() 方法 **kwargs 参数示例 " + "*" * 30)
print(soup.find_all(id='light')) # 选取 id= 'light' 的节点
print(soup.find_all("p", class_='story2')) # 选取 class_='story2' 的 p 标签
print("*" * 30 + " 进阶版,多层筛选 " + "*" * 30)
print(soup.find_all('head')[0].find_all('title')) # 首先选择出所有 head 标签(结果是列表),然后再选择出 'title' 标签
0x05:内容获取 — 从节点中提取属性 & 内容
前面我们介绍了如何使用 BeautifuleSoup 库选取节点,下面我们介绍如何提取我们想要的内容。从网页中筛选的数据,内容要么在标签内部,比如 <a>whoami</a>
中的 whoami
;要么就是直接以属性形式出现在标签中,比如 <a href="http://www.baidu.com">百度以下</a>
中的 href 的内容。
那么本节,就教你如何从选取的节点中提取我们想要的数据,示例如下:
from bs4 import BeautifulSoup
html_doc = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BeautifulSoup 入门案例</title>
</head>
<body>
<h1>这个才是标题</h1>
<p class="title" name="whoami"><b>1001 页</b></p>
<p class="story">1001 页是一个很好的故事,它里面有这么几个故事:<br>
<a href="http://www.hack3rx.cn/1.html" class="link" id="new1">1<br></a>
<a href="http://www.hack3rx.cn/1.html" class="link" id="new2">2<br></a>
<a href="http://www.hack3rx.cn/2.html" class="link mna" id="light" redirecturl name="Goto">2.html => 阿里巴巴和它的神灯<br></a>
</p>
<p class="story2">....</p>
</body>
</html>
"""
# 根据 html_doc 创建 BeautifulSoup 类的对象,并指定使用 lxml 解析器解析文档
soup = BeautifulSoup(html_doc, features='lxml')
# 提取标签内部文本内容
element = soup.find(id='light')
print(element)
print(element.attrs) # 打印该标签包含的属性
print(element.text) # 打印 id=light 的标签的内部文本
# 提取标签属性内容
print(element['href']) # 尝试获取标签中的 href 对应的属性
# 推荐通过 get 方法获取属性,这样如果属性不存在就会返回 None,而不是报错
print(element.get('class')) # class 属性会以列表返回
print(element.get('id')) # id 属性会直接返回对应字符内容
# 拓展:提取属性中包含 redirecturl 的标签
print(soup.find(attrs={'redirecturl':True}))