3

python爬虫之BeautifulSoup4使用 - 钢铁侠的知识库

 2 years ago
source link: https://www.cnblogs.com/jiba/p/16613724.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

钢铁知识库,一个学习python爬虫、数据分析的知识库。人生苦短,快用python。

上一章我们讲解针对结构化的htmlxml数据,使用Xpath实现网页内容爬取。本章我们再来聊另一个高效的神器:Beautiful Soup4。相比于传统正则表达方式去解析网页源代码,这个就简单得多,实践是检验真理的唯一标准,话不多说直接上号开搞验证。

Beautiful Soup 简介

首先说说BeautifulSoup是什么。简单来说,这是Python的一个HTML或XML的解析库,我们可以用它方便从网页中提取数据,官方解释如下:

BeautifulSoup 提供一些简单的、Python 式的函数用来处理导航、搜索、修改分析树等功能。它是一个工具箱,通过解析文档为用户提供需要抓取的数据,因为简单,所以不需要多少代码就可以写出一个完整的应用程序。 BeautifulSoup 自动将输入文档转换为 Unicode 编码,输出文档转换为 utf-8 编码。你不需要考虑编码方式,除非文档没有指定一个编码方式,这时你仅仅需要说明一下原始编码方式就可以了。 BeautifulSoup 已成为和 lxml、html5lib 一样出色的 Python 解释器,为用户灵活地提供不同的解析策略或强劲的速度。

所以,利用它可以省去很多繁琐的提取工作,提高解析效率。

BeautifulSoup 安装

BeautifulSoup3 目前已经停止开发,推荐使用 BeautifulSoup4,不过它也被移植到bs4了,也就是说导入时我们需要import bs4

在开始之前,请确保已经正确安装beautifulsoup4lxml,使用pip安装命令如下:

pip install beautifulsoup4
pip install lxml

BeautifulSoup在解析时实际上依赖解析器。除了支持Python标准库中的HTML解析器,还支持一些第三方的解析器,如果不安装它,则Python会使用默认的解析器。

下面列出BeautifulSoup支持的解析器

解析器 使用方法 优势 劣势
Python 标准库 BeautifulSoup(markup, "html.parser") Python 的内置标准库、执行速度适中 、文档容错能力强 Python 2.7.3 or 3.2.2) 前的版本中文容错能力差
LXML HTML 解析器 BeautifulSoup(markup, "lxml") 速度快、文档容错能力强 需要安装 C 语言库
LXML XML 解析器 BeautifulSoup(markup, "xml") 速度快、唯一支持 XML 的解析器 需要安装 C 语言库
html5lib BeautifulSoup(markup, "html5lib") 最好的容错性、以浏览器的方式解析文档、生成 HTML5 格式的文档 速度慢、不依赖外部扩展

通过上面可以看出,lxml 有解析HTML和XML的功能,相比默认的HTML解析器更加强大,速度,容错能力强。

推荐使用它,下面统一使用lxml进行演示。使用时只需在初始化时第二个参数改为 lxml 即可。

from bs4 import BeautifulSoup
soup = BeautifulSoup('<p>Hello</p>', 'lxml')
print(soup.p.string)
'''
Hello
'''

下面举个实例来看看BeautifulSoup的基本用法:

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')  # 初始化
print(soup.prettify())
print(soup.title.string)

运行结果,你们也可以将上面代码复制到编辑器执行看看:

<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
 <body>
  <p class="title" name="dromouse">
   <b>
    The Dormouse's story
   </b>
  </p>
  <p class="story">
   Once upon a time there were three little sisters; and their names were
   <a class="sister" href="http://example.com/elsie" id="link1">
    <!-- Elsie -->
   </a>
   ,
   <a class="sister" href="http://example.com/lacie" id="link2">
    Lacie
   </a>
   and
   <a class="sister" href="http://example.com/tillie" id="link3">
    Tillie
   </a>
   ;
and they lived at the bottom of a well.
  </p>
  <p class="story">
   ...
  </p>
 </body>
</html>
The Dormouse's story

首先声明一个html变量,它是一个HTML字符串,注意html和body标签都没有闭合。

经过初始化,使用prettify()方法把要解析的字符串以标准缩进格式输出,发现结果中自动补全了html和body标签。这一步不是prettify()方法做的,而是在初始化BeautifulSoup时就完成了。然后调用soup.title.string拿到title里面的文本内容。

通过简单调用几个属性完成文本提取,是不是非常方便呢?

节点选择器

直接调用节点的名称就可以选择节点元素,再调用 string 属性就可以得到节点内的文本了,这种选择方式速度非常快。如果单个节点结构层次非常清晰,可以选用这种方式来解析。

还是以上面的HTML代码为例,详细说明选择元素的方法:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.title)
print(type(soup.title))
print(soup.title.string)
print(soup.head)
print(soup.p)
'''
<title>The Dormouse's story</title>
<class 'bs4.element.Tag'>
The Dormouse's story
<head><title>The Dormouse's story</title></head>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
'''

首先输出title节点的选择结果,包含标签。

接下来输出它的类型,是一个bs4.element.Tag类型,Tag具有一些属性,比如string。

调用string属性可以看到输出节点的文本内容。

继续尝试head、p节点。发现p只取了第一个匹配的节点。说明当有多个节点时只取一个。

每个节点可能有多个属性比如id 、class等,选择元素后可以调用attrs获取所有属性:

print(soup.p.attrs)
print(soup.p.attrs['name'])
'''
{'class': ['title'], 'name': 'dromouse'}
dromouse
'''

可以看到attrs返回结果是字典,它把选择节点所有属性都组合成一个字典。取值直接按字典方式即可。

当然还有一种更简单的获取方式:不写attrs,直接在元素后面中括号取值也行:

print(soup.p['name'])
print(soup.p['class'])
'''
dromouse
['title']
'''

但是注意区分:有的返回字符串、有的返回字符串组成的列表。

对于class,一个节点元素可能有多个class,所以返回的是列表。

子节点和子孙节点

选取节点元素之后,如果想要获取它的直接子节点,可以调用 contents 属性,示例如下:

html4 = """
<html>
    <head>
        <title>The Dormouse's story</title>
    </head>
    <body>
        <p class="story">
            钢铁知识库
            <a href="http://a.com" class="钢铁学数据分析" id="link1">
                <span>Elsie</span>
            </a>
            <a href="http://b.com" class="钢铁学自动化" id="link2">Lacie</a> 
            and
            <a href="http://example.com" class="cccc" id="link3">Tillie</a>
            钢铁学爬虫.
        </p>
        <p class="story">...</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html4, 'lxml')
print(soup.p.contents)
'''
['\n            钢铁知识库\n            ', <a class="钢铁学数据分析" href="http://a.com" id="link1">
<span>Elsie</span>
</a>, '\n', <a class="钢铁学自动化" href="http://b.com" id="link2">Lacie</a>, ' \n            and\n            ', <a class="cccc" href="http://example.com" id="link3">Tillie</a>, '\n            钢铁学爬虫.\n        ']

'''

可以看到返回结果是列表形式。p 节点里既包含节点,又包含文本,最后统一返回列表。

需要注意,列表中的每个元素都是 p 节点的直接子节点。比如第一个 a 节点里面的span节点,这相当于子孙节点了,但返回结果并没有单独把span节点列出来。所以说,contents属性得到的结果是直接子节点的列表。

同样,我们可以调用children属性得到相应的结果:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.p.children)
for i, child in enumerate(soup.p.children):
    print(i, child)
'''
<list_iterator object at 0x0000000001D9A1C0>
0 
            钢铁知识库
            
1 <a class="钢铁学数据分析" href="http://a.com" id="link1">
<span>Elsie</span>
</a>
2 

3 <a class="钢铁学自动化" href="http://b.com" id="link2">Lacie</a>
4  
            and
            
5 <a class="cccc" href="http://example.com" id="link3">Tillie</a>
6 
            钢铁学爬虫.
'''

还是同样的 HTML 文本,这里调用了 children 属性来选择,返回结果是生成器类型。接下来,我们用 for 循环输出相应的内容。

如果要得到所有的子孙节点的话,可以调用 descendants 属性:

<generator object Tag.descendants at 0x000001D77A90E570>
0 
            钢铁知识库
            
1 <a class="钢铁学数据分析" href="http://a.com" id="link1">
<span>Elsie</span>
</a>
2 

3 <span>Elsie</span>
4 Elsie
5 

6 

7 <a class="钢铁学自动化" href="http://b.com" id="link2">Lacie</a>
8 Lacie
9  
            and
            
10 <a class="cccc" href="http://example.com" id="link3">Tillie</a>
11 Tillie
12 
            钢铁学爬虫.

此时返回结果还是生成器。遍历输出一下可以看到,这次的输出结果就包含了 span 节点。descendants 会递归查询所有子节点,得到所有的子孙节点。

除此之外,还有父节点parent 和祖先节点parents,兄弟节点next_siblingprevious_siblings 日常用得少不再演示,后续需要自行查官方文档即可。

方法选择器

前面聊的通过属性选择节点,但如果进行比较复杂的话还是比较繁琐。幸好BeautifulSoup还为我们提供另外一些查询方法,比如find_all 和 find ,调用他们传入相应参数就可以灵活查询。

find_all

顾名思义,就是查询所有符合条件的元素,可以给它传入一些属性或文本来得到符合条件的元素,功能十分强大。

它的 API 如下:

find_all(name , attrs , recursive , text , **kwargs)

我们可以根据节点名来查询元素,下面我们用一个实例来感受一下:

html5='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1">
            <li class="element">钢铁</li>
            <li class="element">知识</li>
            <li class="element">仓库</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">python</li>
            <li class="element">java</li>
        </ul>
    </div>
</div>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(html5, 'lxml')
print(soup.find_all(name='ul'))
print(type(soup.find_all(name='ul')[0]))
'''
[<ul class="list" id="list-1">
<li class="element">钢铁</li>
<li class="element">知识</li>
<li class="element">仓库</li>
</ul>, <ul class="list list-small" id="list-2">
<li class="element">python</li>
<li class="element">java</li>
</ul>]
<class 'bs4.element.Tag'>
'''

可以看到返回了一个列表,分别是两个ul长度为2,且类型依然是bs4.element.Tag类型。

因为都是Tag类型,所以依然可以继续嵌套查询,还是同样文本,查询ul节点后再继续查询内部li节点。

from bs4 import BeautifulSoup
soup = BeautifulSoup(html5, 'lxml')
for ul in soup.find_all(name='ul'):
    print(ul.find_all(name='li'))
'''
[<li class="element">钢铁</li>, <li class="element">知识</li>, <li class="element">仓库</li>]
[<li class="element">python</li>, <li class="element">java</li>]
'''

返回结果是列表类型,元素依然是Tag类型。

接下来我们可以遍历每个li获取它的文本:

for ul in soup.find_all(name='ul'):
    print(ul.find_all(name='li'))
    for li in ul.find_all(name='li'):
        print(li.string)
'''
[<li class="element">钢铁</li>, <li class="element">知识</li>, <li class="element">仓库</li>]
钢铁
知识
仓库
[<li class="element">python</li>, <li class="element">java</li>]
python
java
'''

除了 find_all 方法,还有 find 方法,不过 find 方法返回的是单个元素,也就是第一个匹配的元素,而 find_all 返回的是所有匹配的元素组成的列表。示例如下:

html5='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1">
            <li class="element">钢铁</li>
            <li class="element">知识</li>
            <li class="element">仓库</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">python</li>
            <li class="element">java</li>
        </ul>
    </div>
</div>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(html5, 'lxml')
print(soup.find(name='ul'))
print(type(soup.find(name='ul')))
print(soup.find(class_='list'))


'''
<ul class="list" id="list-1">
<li class="element">钢铁</li>
<li class="element">知识</li>
<li class="element">仓库</li>
</ul>
<class 'bs4.element.Tag'>
<ul class="list" id="list-1">
<li class="element">钢铁</li>
<li class="element">知识</li>
<li class="element">仓库</li>
</ul>
'''

返回结果不再是列表形式,而是第一个匹配的节点元素,类型依然是 Tag 类型。

另外还有许多的查询方法,用法与前面介绍的 find_all、find 方法完全相同,只不过查询范围不同,在此做一下简单的说明。

find_parents 和 find_parent:前者返回所有祖先节点,后者返回直接父节点。

find_next_siblings 和 find_next_sibling:前者返回后面所有的兄弟节点,后者返回后面第一个兄弟节点。

find_previous_siblings 和 find_previous_sibling:前者返回前面所有的兄弟节点,后者返回前面第一个兄弟节点。

find_all_next 和 find_next:前者返回节点后所有符合条件的节点,后者返回第一个符合条件的节点。

find_all_previous 和 find_previous:前者返回节点前所有符合条件的节点,后者返回第一个符合条件的节点。

CSS选择器

BeautifulSoup还提供了另外一种选择器,CSS选择器。如果对 Web 开发熟悉的话,那么对 CSS 选择器肯定也不陌生。如果不熟悉的话,可以参考 http://www.w3school.com.cn/cssref/css_selectors.asp 了解。

使用 CSS 选择器,只需要调用 select 方法,传入相应的 CSS 选择器即可,我们用一个实例来感受一下:

html5='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1">
            <li class="element">钢铁</li>
            <li class="element">知识</li>
            <li class="element">仓库</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">python</li>
            <li class="element">java</li>
        </ul>
    </div>
</div>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(html5, 'lxml')
print(soup.select('.panel .panel-heading'))
print(soup.select('ul li'))
print(soup.select('#list-2 .element'))
print(type(soup.select('ul')[0]))
'''
[<div class="panel-heading">
<h4>Hello</h4>
</div>]
[<li class="element">钢铁</li>, <li class="element">知识</li>, <li class="element">仓库</li>, <li class="element">python</li>, <li class="element">java</li>]
[<li class="element">python</li>, <li class="element">java</li>]
<class 'bs4.element.Tag'>
'''

结果为所有匹配的节点。例如select('ul li')则是所有ul节点下面的所有li节点,返回结果是列表。

select 方法同样支持嵌套选择(soup.select('ul'))、属性获取(ul['id']),以及文本获取(li.string/li.get_text())

---- 钢铁知识库 2022.08.22

到此 BeautifulSoup 的使用介绍基本就结束了,最后钢铁知识库做一下简单的总结:

  • 推荐使用 LXML 解析库,速度快、容错能力强。
  • 建议使用 find、find_all 方法查询匹配单个结果或者多个结果。
  • 如果对 CSS 选择器熟悉的话可以使用 select 匹配,可以像Xpath一样匹配所有。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK