likes
comments
collection
share

『Python工具篇』Beautiful Soup 解析网页内容

作者站长头像
站长
· 阅读数 9

本文简介

点赞 + 关注 + 收藏 = 学会了

  1. 爬取数据
  2. 解析数据
  3. 存储数据

而在解析数据时使用的是 Beautiful Soup 这个库,直译过来就是“靓汤”,这是广东人最喜欢的库。

Beautiful Soup 的作用是解析爬取回来的网页数据,也就是解读 HMTL 内容。

对于前端开发者来说,这类解析网页内容的工具其实有点像 CSS 选择器,所以前端开发者学起来会非常快。

我也会以前端的角度去讲解 Beautiful Soup

安装和引入

Beautiful Soup 不是 Python 的内置库,所以使用之前需要先安装和引入。

安装

pip install beautifulsoup4

引入

from bs4 import BeautifulSoup

基础用法

解析器

Beautiful Soup 中,解析器的作用是将原始的 HTMLXML 文档解析成一个树形结构,以便于我们可以方便地浏览、搜索和修改其中的元素。解析器负责解析标记语言中的标签、属性和文本,并将其转换成一个可以被程序操作的数据结构,比如树形结构或者 DOM 树。这样我们就可以通过编程的方式来访问、提取和操作网页中的数据了。

不同类型的文档可能需要不同的解析器来处理,因为它们可能具有不同的语法、结构和特性。在选择解析器时,通常会考虑解析速度、性能、准确性以及适用的文档类型等因素。

Beautiful Soup 支持几种解析器,其中一种是 Python 标准库中的 HTML 解析器,另外还支持第三方的 lxml parserhtml5lib

引用 Beautiful Soup 官方文档对解释器的介绍:

解析器使用方法优势劣势
Python 标准库BeautifulSoup(markup, "html.parser")- Python的内置标准库- 执行速度较快- 容错能力强- 速度没有 lxml 快,容错没有 html5lib强
lxml HTML 解析器BeautifulSoup(markup, "lxml")- 速度快- 容错能力强- 额外的 C 依赖
lxml XML 解析器BeautifulSoup(markup, ["lxml-xml"])``BeautifulSoup(markup, "xml")- 速度快- 唯一支持 XML 的解析器- 额外的 C 依赖
html5libBeautifulSoup(markup, "html5lib")- 最好的容错性- 以浏览器的方式解析文档- 生成 HTML5 格式的文档- 速度慢- 额外的 Python 依赖

官方推荐使用 lxml 来获得更高的速度。

也就是这么用:

BeautifulSoup('<div>雷猴</div>', 'lxml')

到此,相信各位工友对于 BeautifulSoup 的用法还是有点懵的。没关系,先知道有这几种解析器,接下来的内容会开始讲解用法。

自动补全

如果把缺少闭合标签的 HTML 代码丢给 BeautifulSoup 解析, BeautifulSoup 会自动补全闭合标签。

from bs4 import BeautifulSoup

html = """
<ul>
		<li>
        <span>雷猴</span>
    </li>
	<li>
        <span>鲨鱼辣椒
    </li>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup)

输出结果:

<html><body><ul>
<li>
<span>雷猴</span>
</li>
<li>
<span>鲨鱼辣椒
    </span></li>
</ul></body></html>

在上面这个例子中,“鲨鱼辣椒”后面少了一个 </span> ,也少了最后一个 </ul>。当把这段 HTML 代码丢给 BeautifulSoup 解析后,它会自动帮我们把这两个标签补全,同时也会将 <body><html> 标签给补全。

标签选择器

HTML 里的标签有 <h1><div><span><a> 等一大堆。这些都叫标签。当我们获取到一段 HTML 代码后,用 BeautifulSoup 提供的标签选择器(也叫节点选择器)就可以提取出对应标签的内容。

先来看看代码

from bs4 import BeautifulSoup

html = """
<ul>
		<li>
        <span>雷猴</span>
    </li>
	<li>
        <span>鲨鱼辣椒</span>
    </li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.li)

输出结果:

<li>
<span>雷猴</span>
</li>

这段 HTML 代码中有多个 <li> 标签,BeautifulSoup 的标签选择器只会选中第一个匹配的节点,后面的同名节点全部会忽略掉。

import requests
from bs4 import BeautifulSoup

# 请求 http://books.toscrape.com/
res = requests.get ("http://books.toscrape.com/")
resHTML = res.text

# 将请求回来的页面丢给 BeautifulSoup 解析
soup = BeautifulSoup(resHTML, 'lxml')

# 输出这个页面中的第一个 li 标签的内容
print(soup.li)

输出结果:

<li>
<a href="index.html">Home</a>
</li>

获取文本内容

前面的“标签选择器”例子中,获取了 <li> 标签的内容里包含里 <span> 标签。如果只想要 <li> 标签里的文本内容,而且不包含 <span> 标签的话可以用 text 属性获取。

html = """
<ul>
		<li>
        <span>雷猴</span>
    </li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.li.text)

此时打印的内容是"雷猴"。

除了 text 外,还可以使用 string 属性获取文本内容

html = """
<ul>
	<li>
        <span>雷猴</span>
    </li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.span.string)

此时还是输出“雷猴”,但需要注意的是,前面使用 text 的标签是 <li> ,而这里使用 string 的标签是 <span>

textstring 是有区别的,text 支持从多节点中提取文本信息,而 string 只支持从单节点中提取文本信息。

获取标签名

通过 name 属性可以获取节点的名称。

html = """
<ul>
		<li>
        <span>雷猴</span>
    </li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.span.name)

输出:span

这个例子看上去好像有点多此一举,通过 span.name 获取 span 节点的名字,就像问“先生请问你的性别是?”。

但其实它也是有用的,比如通过其他查询条件获取到的内容你是不知道它们用了什么标签的,此时就可以通过 name 属性查出来了。

获取标签的属性

什么是属性?拿下面这段 HTML 代码举例。

在这个 <a> 标签中有3个属性,分别是 hrefidclass

Beautiful Soup 里可以通过 attrs 一次获取这些属性。

输出结果:

也可以通过指定的属性名获取。

# 省略部分代码

print(soup.a.attrs['href'])
print(soup.a.attrs['id'])
print(soup.a.attrs['class'])

输出:

attrs 返回的值有时候是字符串,有时候是列表,其原因是有些属性确实是字符串就能表示了,而像 class 这种属性是可以存放多个值的,这种情况就使用列表。

上面获取指定属性的写法还是有点复杂,可以简化成这样。

# 省略部分代码

# 以下两句的输出结果是一样的
print(soup.a.attrs['href'])
# 简化版
print(soup.a['href'])

嵌套选择

可以通过嵌套选择的方式精准选择元素。

html = """
<ul>
    <li>雷猴</li>
</ul>
<ol>
    <li>鲨鱼辣椒</li>
</ol>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.ol.li)

输出结果:

<li>鲨鱼辣椒</li>

通过 soup.ol.li 选择了 <ol> 里面的 <li>

子选择器

CSS 中,子选择器使用 ">" 符号,它选择某个元素的直接子元素,而不包括孙子元素及更深层次的后代元素。这意味着子选择器只会选择目标元素的直接子元素,不会选择其后代元素。

例如:

<div id="parent">
    <p>第一个段落</p>
    <div>
        <p>第二个段落</p>
    </div>
    <p>第三个段落</p>
</div>

我们使用子选择器 #parent > p,它将选择 id 为 "parent" 的 div 元素下的直接子元素 p,即第一个段落和第三个段落,而不会选择第二个段落,因为第二个段落是位于 div 的子元素的子元素。

而在 BeautifulSoup 中可以使用 contents 属性获取某元素的直接子元素。

html = """
<ul>
    <li>雷猴</li>
    <li>鲨鱼辣椒</li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.ul.contents)

输出结果:

['\n', <li>雷猴</li>, '\n', <li>鲨鱼辣椒</li>, '\n']

除此之外,还可以使用 children 属性获取子元素,它返回的是一个生成器类型,需要遍历才能获取到里面的值。

html = """
<ul>
    <li>雷猴</li>
    <li>鲨鱼辣椒</li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
lis = soup.ul.children
for i, child in enumerate(lis):
    print(f"{i} => {child}")

输出结果:

0 => 

1 => <li>雷猴</li>
2 => 

3 => <li>鲨鱼辣椒</li>
4 => 

不管是用 contents 还是 children 都能看到筛选出一些“空行”,这是因为上面那段 HTML 里确实有换行。

后代选择器

使用 descendants 属性可以获取某元素的所有后代元素。

html = """
<div>
<ul>
    <li>雷猴</li>
    <li>鲨鱼辣椒</li>
</ul>
</div>
"""

soup = BeautifulSoup(html, 'lxml')
for i, child in enumerate(soup.div.descendants):
    print(f"{i} => {child}")

输出结果:

0 => 

1 => <ul>
<li>雷猴</li>
<li>鲨鱼辣椒</li>
</ul>
2 => 

3 => <li>雷猴</li>
4 => 雷猴
5 => 

6 => <li>鲨鱼辣椒</li>
7 => 鲨鱼辣椒
8 => 

9 => 

descendants 返回的结果是生成器类型的,需要遍历输出。

父选择器

使用parent 属性可以获取直接父元素。

html = """
<div>
    <ul>
        <li>雷猴</li>
        <li>鲨鱼辣椒</li>
    </ul>
</div>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.li.parent)

输出结果:

<ul>
<li>雷猴</li>
<li>鲨鱼辣椒</li>
</ul>

祖先选择器

使用 parents 属性可以获取祖先节点,爸爸的爸爸级别的元素也能获取到。

html = """
<div>
    <ul>
        <li>雷猴</li>
        <li>鲨鱼辣椒</li>
    </ul>
</div>
"""

soup = BeautifulSoup(html, 'lxml')
print(list(enumerate(soup.li.parents)))

输出结果:

[(0, <ul>
<li>雷猴</li>
<li>鲨鱼辣椒</li>
</ul>), (1, <div>
<ul>
<li>雷猴</li>
<li>鲨鱼辣椒</li>
</ul>
</div>), (2, <body><div>
<ul>
<li>雷猴</li>
<li>鲨鱼辣椒</li>
</ul>
</div>
</body>), (3, <html><body><div>
<ul>
<li>雷猴</li>
<li>鲨鱼辣椒</li>
</ul>
</div>
</body></html>), (4, <html><body><div>
<ul>
<li>雷猴</li>
<li>鲨鱼辣椒</li>
</ul>
</div>
</body></html>)]

这里使用了 list() 方法,使 parents 返回的生成器类型数据装成列表类型。

兄弟选择器

兄弟选择器的作用是获取同级别的节点,一共有这4个属性供我们使用:

  1. next_sibling: 获取下一个兄弟节点
  2. previous_sibling: 获取上一个兄弟节点
  3. next_siblings: 获取后面的所有兄弟节点
  4. previous_siblings: 获取前面的所有兄弟节点

演示一下:

html = """
<h1>标题1</h1><h2>标题2</h2><h3>标题3</h3><h4>标题4</h4><h5>标题5</h5><h6>标题6</h6>
"""

soup = BeautifulSoup(html, 'lxml')
print(f"next sibling: {soup.h3.next_sibling}")
print(f"previous sibling: {soup.h3.previous_sibling}")
print(f"next siblings: {list(enumerate(soup.h3.next_siblings))}")
print(f"previous siblings: {list(enumerate(soup.h3.previous_siblings))}")

输出结果:

next sibling: <h4>标题4</h4>
previous sibling: <h2>标题2</h2>
next siblings: [(0, <h4>标题4</h4>), (1, <h5>标题5</h5>), (2, <h6>标题6</h6>), (3, '\n')]
previous siblings: [(0, <h2>标题2</h2>), (1, <h1>标题1</h1>)]

方法选择器

前面介绍的方法都是基于标签名(节点名)来选择的,但这不够灵活。

如果你想通过属性名等条件选择标签,可以使用 find_allfind 方法。

举个例子,我要获取所有 <li> 标签

html = """
<ul>
    <li name="n1">雷猴</li>
    <li name="n1">鲨鱼辣椒</li>
    <li name="n2">蝎子莱莱</li>
    <li name="n2">蟑螂恶霸</li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(name="li"))

输出结果:

[<li name="n1">雷猴</li>, <li name="n1">鲨鱼辣椒</li>, <li name="n2">蝎子莱莱</li>, <li name="n2">蟑螂恶霸</li>]

通过 name="li" 作为条件筛选出所有 <li> 标签出来。

但这几个 <li> 都有一个 name 属性,如果想筛选出属性 namen1 的所有 <li> 标签,需要用前面提到的 attrs 来获取。

html = """
<ul>
    <li name="n1">雷猴</li>
    <li name="n1">鲨鱼辣椒</li>
    <li name="n2">蝎子莱莱</li>
    <li name="n2">蟑螂恶霸</li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(attrs={"name": "n1"}))

输出结果:

[<li name="n1">雷猴</li>, <li name="n1">鲨鱼辣椒</li>]

还可以通过 idclass 当做过滤条件。

html = """
<ul>
    <li class="li_1">雷猴</li>
    <li class="li_1">鲨鱼辣椒</li>
    <li class="li_2">蝎子莱莱</li>
    <li class="li_2">蟑螂恶霸</li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(attrs={"class": "li_2"}))

输出结果:

[<li class="li_2" name="n2">蝎子莱莱</li>, <li class="li_2" name="n2">蟑螂恶霸</li>]

但像 idclass 这两个常用的属性,可以不使用 attrs但是,classpython 的关键字,如果要当做 CSS 的类选择器需要用 class_="xxx" 的方式去书写,也就是 class 后面加多一个下划线。

html = """
<ul>
    <li class="li_1" id="li1">雷猴</li>
    <li class="li_1" id="li2">鲨鱼辣椒</li>
    <li class="li_2" id="li3">蝎子莱莱</li>
    <li class="li_2" id="li4">蟑螂恶霸</li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(class_="li_2"))
print(soup.find_all(id="li2"))

输出结果:

[<li class="li_2" id="li3">蝎子莱莱</li>, <li class="li_2" id="li4">蟑螂恶霸</li>]
[<li class="li_1" id="li2">鲨鱼辣椒</li>]

再来,find_all() 还支持通过文本内容来匹配节点,这个可牛了!

但匹配文本需要使用正则表达式。

import re
from bs4 import BeautifulSoup

html = """
<ul>
    <li>雷猴辣椒</li>
    <li>鲨鱼辣椒</li>
    <li>蝎子莱莱</li>
    <li>蟑螂恶霸</li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(string=re.compile("辣椒")))

输出结果:

['雷猴辣椒', '鲨鱼辣椒']

介绍完 find_all() 再回头看 find(),其实它们的用法都是一样的,只是返回的结果不同而已。

find() 方法返回的是单个元素(节点),会返回第一个匹配到的元素。

用法和 find_all() 一样,这里就不重复讲述了。

CSS选择器

Beautiful Soup 支持使用 CSS 选择器,只需调用 select 方法,然后像写 CSS 那样把选择器传进去就可以了。

import re

html = """
<ul class="ul_x">
    <li class="list">雷猴辣椒</li>
    <li class="list">鲨鱼辣椒</li>
    <li class="list">蝎子莱莱</li>
    <li class="list">蟑螂恶霸</li>
</ul>
"""

soup = BeautifulSoup(html, 'lxml')
print(soup.select(".ul_x .list"))

输出结果:

[<li class="list">雷猴辣椒</li>, <li class="list">鲨鱼辣椒</li>, <li class="list">蝎子莱莱</li>, <li class="list">蟑螂恶霸</li>]

好啦,常用的方法介绍完了,想进修的工友可以看看 Beautiful Soup 官网

点赞 + 关注 + 收藏 = 学会了