BeautifulSoup全面总结

2,089 阅读12分钟
原文链接: zhuanlan.zhihu.com

(注:网页解析库的代码都比较通俗易懂,看理论讲解不如直接看代码,自己多写就能对常用方法了然于胸。本文是从整体框架上进行总结,更适合在对库有基本的了解之后再详细跟着文章思路查缺补漏。所以建议本文阅读顺序为:先不看文字,挑代码来看(这时挑简单的来看,看不懂的代码不要管),知道那些代码都是做什么的,自己动手写,多试错,然后看后面的实战。了解库的基本使用流程之后,能自己解析一些东西了,再跟着文章的思路把细节补上,之后就能运用自如了。)

beautifulsoup库应该是初学爬虫听的最多的一个解析库了,本文就来讲解一下这个库的用法。

本文分为如下几个部分

  • 解析html流程说明
  • 识别标签
  • 提取内容
  • 查看标签信息
  • 其他提取细节

解析html流程说明

爬取网页信息,可以理解成从html代码中抽取我们需要的信息,而html代码由众多标签组成,我们要做的就分为两个部分

  • 精确定位标签
  • 从标签中提取内容

先说第二个,在找到对应标签位置后,我们要的信息一般会存储在标签的两个位置中

  • 标签中的内容
  • 属性值

比如下面这行标签

<p><a href='www.wenzi.com'>文字</a></p>

一般要提取“文字”部分,或者那个链接www.wenzi.com部分

那么如何精确定位到标签呢?只能依靠标签名和标签属性来识别。下面一个例子提供识别的大致思路,之后会具体总结

<title>标题</title>
<body>
    <ul class='list1'>
        <li>列表1第1项</li>
        <li>列表1第2项</li>
    </ul>
    <p class='first'>文字1</p>
    <p class='second'>文字2</p>
    <ul class='list2'>
        <li>列表2第1项</li>
        <li>列表2第2项</li>
    </ul>
</body>
  • 如果要提取“标题”,只需要使用title标签名来识别,因为只出现过一次title标签
  • 如果要提取“文字1”,不能只使用p标签,因为“文字2”也对应了p标签,所以要用p标签且class属性是'second'来识别
  • 如果“文字1”和“文字2”都要,就可以循环获取所有p标签提取内容
  • 如果想提取列表1中的两项,就不能直接循环获取li标签了,因为列表2中也有li标签。此时需要先识别其父节点,先定位到<ul class='list1'>这个标签上(通过ul标签和class属性是list1定位)。此时在这个标签里,所有li都是我们想要的了,直接循环获取就可以了

这里展示一下使用beautifulsoup实现上述提取的代码,先对这个库的提取思路有一个大概的印象

a = '''<title>标题</title>
<body>
    <ul class='list1'>
        <li>列表1第1项</li>
        <li>列表1第2项</li>
    </ul>
    <p class='first'>文字1</p>
    <p class='second'>文字2</p>
    <ul class='list2'>
        <li>列表2第1项</li>
        <li>列表2第2项</li>
    </ul>
</body>'''

from bs4 import BeautifulSoup
soup = BeautifulSoup(a, "html.parser")
soup.title.text # '标题'
soup.find('p', attrs={'class':'first'}).text # '文字1'
soup.find_all('p') # [<p class="first">文字1</p>, <p class="second">文字2</p>], 再分别从中提取文字
soup.find('ul', attrs={'class':'list1'}).find_all('li') # [<li>列表1第1项</li>, <li>列表1第2项</li>]

识别的大致思路就是这样。

从细节上看,一个完整解析库需要实现三个部分的功能

从提取内容上看

  • 提取标签内容
  • 提取标签属性值

从识别标签上看

  • 只根据标签来识别
    • 找到名为a的标签(只有一个a标签时)
    • 找到所有名为a的标签
    • 找到名为a或b的标签
    • 根据正则表达式提取标签
  • 同时根据标签和属性识别
    • (要求在能识别标签的前提下,能相似地识别属性)比如
    • 找到 标签名为a且B属性的值是b 的标签
    • 找到 标签名为a且B属性的值是b且C属性是c 的标签
    • 找到 标签名为a且B属性的值是b或c 的标签
    • 找到 标签名为a且(不)包含B属性 的标签
    • 找到 标签名为a且B属性的值包含b字符串(各种正则式匹配) 的标签
  • 根据标签内内容来识别,也是类似如上划分,最好能用正则表达式匹配内容
  • 根据位置识别
    • 找到 第i个a标签
    • 找到 第i个和第j个a标签

提供一些查看当前标签信息的方法,方便调试

  • 获得标签名
  • 获得标签所有属性及值
  • 检查标签是否有某属性

除此之外这个库还有一些提取细节

  • 多层嵌套标签问题
  • find_all的简写方法
  • find_all的其他参数

下面按照上述实现的功能顺序来讲解

(其实到最后会发现这个库唯一一个要重点掌握的方法是find_all)

首先加载库

from bs4 import BeautifulSoup

识别标签

本节分为如下几个部分

  • 只根据标签来识别
  • 根据标签和属性识别
  • 根据标签内内容来识别
  • 根据位置识别

只根据标签来识别

这部分分为如下几种情况

  • 找到名为a的标签(查找唯一标签)
  • 找到所有名为a的标签
  • 找到名为a或b的标签
  • 根据正则表达式提取标签

查找唯一标签时,beautifulsoup库中提供三种方法

  • 对象的属性调用,直接提取该名字的标签,但是如果有很多该标签只能找到第一个
  • find方法,也只能找到第一个
  • find_all方法,找到所有该标签,返回一个list,如果只找到一个也是返回list,用[0]提取
a = '''
<h1>标题1</h1>
<h2>标题2</h2>
<h2>标题3</h2>
'''

# 将提取到的字符串转化为beautifulsoup的对象
soup = BeautifulSoup(a, "html.parser")

# 提取唯一标签
soup.h1
soup.find('h1')
soup.find_all('h1')[0]
# 上面三条结果都是
# <h1>标题1</h1>

如果要找到所有某标签,或者某几种标签及根据正则表达式提取,只能用find_all,返回一个列表

soup.find_all('h2')
# [<h2>标题2</h2>, <h2>标题3</h2>]

soup.find_all(['h1','h2'])
# [<h1>标题1</h1>, <h2>标题2</h2>, <h2>标题3</h2>]

# 使用正则表达式
import re
soup.find_all(re.compile('^h'))
# [<h1>标题1</h1>, <h2>标题2</h2>, <h2>标题3</h2>]

根据标签和属性识别

这部分分为如下几种情况

  • (要求在能识别标签的前提下,能用相似的方法识别属性)比如
  • 找到 标签名为a且B属性的值是b 的标签
  • 找到 标签名为a且B属性的值是b且C属性是c 的标签
  • 找到 标签名为a且B属性的值是b或c 的标签
  • 找到 标签名为a且(不)包含B属性 的标签
  • 找到 标签名为a且B属性的值包含b字符串(各种正则式匹配) 的标签

要参考属性提取标签时,只有findfind_all两种方法,find总是返回找到的第一个,而find_all会返回所有,如果想要第一个就提取即可,因此这里全用find_all来讲,其实只是加一个attrs参数

a = '''
<p id='p1'>段落1</p>
<p id='p2'>段落2</p>
<p class='p3'>段落3</p>
<p class='p3' id='pp'>段落4</p>
'''

soup = BeautifulSoup(a, "html.parser")

# 第一种,直接将属性名作为参数名,但是有些属性不行,比如像a-b这样的属性
soup.find_all('p', id = 'p1') # 一般情况
soup.find_all('p', class_='p3') # class是保留字比较特殊,需要后面加一个_

# 最通用的方法
soup.find_all('p', attrs={'class':'p3'}) # 包含这个属性就算,而不是只有这个属性
soup.find_all('p', attrs={'class':'p3','id':'pp'}) # 使用多个属性匹配
soup.find_all('p', attrs={'class':'p3','id':False}) # 指定不能有某个属性
soup.find_all('p', attrs={'id':['p1','p2']}) # 属性值是p1或p2

# 正则表达式匹配
import re
soup.find_all('p', attrs={'id':re.compile('^p')}) # 使用正则表达式
soup.find_all('p', attrs={'class':True}) # 含有class属性即可

根据标签内内容来识别

这部分还是使用find_all函数,增加text参数

a = '''
<p id='p1'>段落1</p>
<p class='p3'>段落2</p>
<p class='p3'>文章</p>
<p></p>
'''

soup = BeautifulSoup(a, "html.parser")

soup.find_all('p', text='文章')
soup.find_all('p', text=['段落1','段落2'])

# 正则表达式
import re
soup.find_all('p', text=re.compile('段落'))
soup.find_all('p',text=True)

# 传入函数
def nothing(c):
    return c not in ['段落1','段落2','文章']
soup.find_all('p',text=nothing)

# 同上
def nothing(c):  
    return c is None
soup.find_all('p',text=nothing)

最后使用的传入函数的方法,不止在这里识别内容上可以这样用,在识别标签名和属性时都可以使用。都是输入要判断东西返回布尔值,结果是True的才会被选中。只是函数定义时有点不同

  • 第一个参数识别标签,函数的参数应该是标签,比如这样用
def has_class_but_no_id(tag):
    return tag.has_attr('class') and not tag.has_attr('id')
  • attrs参数可以字典中的属性键的值中使用函数,传入字符串
  • text参数也是传入字符串

根据位置识别

  • 找到 第i个a标签
  • 找到 第i个和第j个a标签

有时三个标签的标签属性全都一样,所有东西都一样(内容可能不一样,但是类型是一样的),但是我们只想要第二个,这时就不能只通过标签属性内容这些方法提取了,可能它的位置是特殊的就可以用位置来提取。这里其实可以用find_all提取出列表,然后在列表中根据位置再提取一次

提取内容

本节分为两个部分

  • 提取标签内容:使用.text
  • 提取标签属性值,像字典一样提取
a = '''
<body>
    <h><a href='www.biaoti.com'>标题</a></h>
    <p>段落1</p>
    <p>段落2</p>
</body>
'''
soup = BeautifulSoup(a, 'html.parser')

# 提取内容
soup.p.text
for p in soup.find_all('p'):
    print(p.text)
soup.h.text # 多层嵌套也可以直接返回
soup.h.a.text # 也可以这样
soup.body.text # 里面有多个内容时 '\n标题\n段落1\n段落2\n'

# 提取属性值,像字典一样提取,以下两种方法等价
soup.h.a['href']
soup.h.a.get('href')

查看标签信息

本节分为如下内容

  • 获得标签名
  • 获得标签所有属性的字典
  • 检查标签是否有某属性
a = '''
<body>
    <h><a href='www.biaoti.com'>标题</a></h>
    <p>段落1</p>
    <p></p>
</body>
'''
soup = BeautifulSoup(a, 'html.parser')
for i in soup.body.find_all(True):
    print(i.name) # 提取标签名
    print(i.attrs) # 提取标签所有属性值
    print(i.has_attr('href')) # 检查标签是否有某属性

其他提取细节

本节包括以下几个部分

  • 多层嵌套标签时的find_all——渗透到所有层次子节点全部提取出来
  • find_all的简写方法——可以省去find_all这8个字母
  • find_all的其他参数
  • 通过css选择器来提取
  • 库中的其他函数

示例代码如下

a = '''
<span>
    <span id='s'>内容1</span>
</span>
<span>内容2</span>
'''
soup = BeautifulSoup(a, 'html.parser')

# 多层嵌套标签
soup.find_all('span') # 3个元素
    # [<span>
    #  <span>内容1</span>
    #  </span>, <span>内容1</span>, <span>内容2</span>]
# find_all的简写,标签直接加括号
soup.span('span',id='s') # 相当于调用find_all返回list

# find_all的其他参数
soup.find_all('span', limit=2) # 限制只返回前两个
soup.find_all('span', recursive=False) # 只查找子节点,不查找孙节点

总结下来,Beautifulsoup库其实主要使用find_all方法进行网页解析,审查不同元素使用不同参数即可实现,不同参数使用方法都相同,无论是列表、正则pattern还是函数都支持,让提取信息非常得心应手。

除此之外,Beautifulsoup库还提供了css选择器的select方法,这里不多讲,用css选择器提取的方法会在后面的pyquery库中讲解。

另外,对于库中的其他方法做如下说明:

  • findAll是之前版本的find_all,如果看到有其他教程用这个不要觉得奇怪
  • 库中还提供了一些查询兄弟节点等查询方法,还有对html代码进行修改的方法(有时不要的部分才有特点,就可以把不要的部分替换掉,就可以自由提取想要的了),详情可以见官网
  • 解析器的选择

BeautifulSoup(a, "html.parser")这里的第二个参数表示使用的解析器,BeautifulSoup提供了三个解析器,它们各自的优缺点如下

  • html.parser内置不依赖扩展,容错能力强,速度适中
  • lxml速度最快,容错能力强,但是依赖C扩展
  • html5hib速度最慢,容错能力最强,依赖扩展

它们不仅在这些性能方面有些许差异,而且在对文档的处理上也有差异,虽然这些差异很多时候不影响我们解析,详情见官网

使用beautifulsoup抓取网页实战可以看这篇文章 beautifulsoup+json抓取stackoverflow实战

专栏信息

专栏主页:python编程

专栏目录:目录

爬虫目录:爬虫系列目录

版本说明:软件及包版本说明