python第三方库beatifulSoup使用

python中BeautifulSoup使用

  BeautifulSoup是一个可以从Html或xml文件中提取数据的python库,它能够通过不同的转换器实现惯用的文档导航、查找、修改文档的方式。
  首先看看它的一些基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# !/usr/bin/env python
# -*- coding: utf-8 -*-
from BeautifulSoup import BeautifulSoup

html_doc = """<html><head><title>The Dormouse's story</title></head>

<body>

<p class="title"><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>
</body>
</html>"""
soup = BeautifulSoup(html_doc)
# 通过prettify方法格式化文档
# print(soup.prettify())
# 输出文件对象的title标签内容
print soup.title
# 输出标签的名字
print soup.title.name
# 输出标签的文本内容
print soup.title.string
# 输出标签title的父节点名字
print soup.title.parent.name
# 输出标签的属性class的值,!前提是该标签有定义了该属性,要不会报错
print soup.p["class"]
# 输出文档中所有a标签的节点
print soup.findAll("a")
# 输出节点带有id属性且id属性值为"link3"的节点
print soup.find(id="link3")
# 从文档中找到所有a标签的链接
for link in soup.findAll("a"):
print(link.get("href"))
# 从文档中获取所有文字内容,text属性是调用getText()方法,getText()方法可以添加分隔符,有的本版是这个方法名:get_text()
print(soup.text)

  大致浏览了下BeautifulSoup用法,接着我们看看如何在中用。

#首先是安装

  1. ubuntu或者Debain系统安装

    1
    $ apt-get install Python-bs4
  2. 直接命令安装,Beautiful Soup 4 通过PyPi发布,所以如果你无法使用系统包管理安装,那么也可以通过 easy_install 或 pip 来安装。包的名字是 beautifulsoup4 ,这个包兼容Python2和Python3。

    1
    2
    c:\> easy_install beautifulsoup4
    c:\> pip install beautifulsoup4

  注意在PyPi中还有一个名字是 BeautifulSoup 的包,但那可能不是你想要的,那是 Beautiful Soup3 的发布版本,因为很多项目还在使用BS3, 所以 BeautifulSoup 包依然有效。但是如果你在编写新项目,那么你应该安装的 beautifulsoup4。

  1. 通过下载源码安装,解压进入源码包,然后运行:

    1
    python setup.py install
  2. 通过ide辅助安装,在代码中输入一下内容:

    1
    2
    # 版本 3.x.x引入方式
    from BeautifulSoup import BeautifulSoup

或者

1
2
# 版本4以上含,引入方式
from bs4 import BeautifulSoup

  然后ide会提示BeautifulSoup找不到,再根据提示安装即可。

安装完成后可能遇到的问题


  1. 代码中抛出ImportError的异常:“No module named HTMLParser”,这是因为你在Python3版本中执行Python2版本的代码。
  2. 代码抛出了 ImportError 的异常: “No module named html.parser”, 这是因为你在Python2版本中执行Python3版本的代码.
      如果遇到上述2种情况,最好的解决方法是重新安装BeautifulSoup4。

#安装解析器
  Beautiful Soup支持Python标准库中的HTML解析器,还支持一些第三方的解析器,其中一个是 lxml .根据操作系统不同,可以选择下列方法来安装lxml:

1
2
3
$ apt-get install Python-lxml
$ easy_install lxml
$ pip install lxml

  另一个可供选择的解析器是纯Python实现的 html5lib , html5lib的解析方式与浏览器相同,可以选择下列方法来安装html5lib:

1
2
3
$ apt-get install Python-html5lib
$ easy_install html5lib
$ pip install html5lib

python中主要的一些解析器和各自优缺点:

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

  推荐使用lxml作为解析器,因为效率更高. 在Python2.7.3之前的版本和Python3中3.2.2之前的版本,必须安装lxml或html5lib, 因为那些Python版本的标准库中内置的HTML解析方法不够稳定.

  提示 如果一段HTML或XML文档格式不正确的话,那么在不同的解析器中返回的结果可能是不一样的,查看 [解析器之间的区别] (http://doc.iplaypy.com/bs4/#id49)了解更多细节。

#使用
  将一段文档传入BeautifulSoup 的构造方法,就能得到一个文档的对象, 可以传入一段字符串或一个文件句柄。

1
2
3
4
from bs4 import BeautifulSoup

soup = BeautifulSoup(open("index.html"))
soup = BeautifulSoup("<html>data</html>")

  首先文档被转换成Unicode,并且HTML的实例都被转换成Unicode编码,然后,Beautiful Soup选择最合适的解析器来解析这段文档,如果手动指定解析器那么Beautiful Soup会选择指定的解析器来解析文档。

##对象的种类
  Beautiful Soup将复杂HTML文档转换成一个复杂的树形结构,每个节点都是Python对象,所有对象可以归纳为4种: Tag ,NavigableString ,BeautifulSoup,Comment 。

###Tag
  Tag 对象与XML或HTML原生文档中的tag相同:

1
2
3
4
soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b
type(tag)
# <class 'bs4.element.Tag'>

  Tag有很多方法和属性,在遍历文档树和搜索文档树中有详细解释。现在介绍一下tag中最重要的属性: name和attributes

  1. Name:每个tag都有自己的名字,通过 .name 来获取:

    1
    2
    tag.name
    # u'b'
  2. Attributes:一个tag可能有很多个属性.。tag 有一个 “class” 的属性,值为 “boldest” 。 tag的属性的操作方法与字典相同:

    1
    2
    tag['class']
    # u'boldest'

也可以直接”点“取属性,比如:.attrs:

1
2
tag.attrs
# {u'class': u'boldest'}

  tag的属性可以被添加,删除或修改。tag的属性操作方法与字典一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tag['class'] = 'verybold'
tag['id'] = 1
tag
# <blockquote class="verybold" id="1">Extremely bold</blockquote>

del tag['class']
del tag['id']
tag
# <blockquote>Extremely bold</blockquote>

tag['class']
# KeyError: 'class'

print(tag.get('class'))
# None

  1. 多值属性:HTML 4定义了一系列可以包含多个值的属性。在HTML5中移除了一些,却增加更多。最常见的多值的属性是 class (一个tag可以有多个CSS的class). 、。还有一些属性 rel , rev , accept-charset , headers , accesskey 。在Beautiful Soup中多值属性的返回类型是list:
    1
    2
    3
    4
    5
    6
    7
    css_soup = BeautifulSoup('<p class="body strikeout"></p>')
    css_soup.p['class']
    # ["body", "strikeout"]

    css_soup = BeautifulSoup('<p class="body"></p>')
    css_soup.p['class']
    # ["body"]

  如果某个属性看起来好像有多个值,但在任何版本的HTML定义中都没有被定义为多值属性,那么Beautiful Soup会将这个属性作为字符串返回。

1
2
3
id_soup = BeautifulSoup('<p id="my id"></p>')
id_soup.p['id']
# 'my id'

  将tag转换成字符串时,多值属性会合并为一个值。

1
2
3
4
5
6
7
rel_soup = BeautifulSoup('<p>Back to the <a rel="index">homepage</a></p>')
rel_soup.a['rel']
# ['index']

rel_soup.a['rel'] = ['index', 'contents']
print(rel_soup.p)
# <p>Back to the <a rel="index contents">homepage</a></p>

  如果转换的文档是XML格式,那么tag中不包含多值属性。

1
2
3
4
xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml')

xml_soup.p['class']
# u'body strikeout'

###NavigableString:可以遍历的字符串
  字符串常被包含在tag内。Beautiful Soup用 NavigableString 类来包装tag中的字符串:

1
2
3
4
5
tag.string
# u'Extremely bold'

type(tag.string)
# <class 'bs4.element.NavigableString'>

  一个 NavigableString 字符串与Python中的Unicode字符串相同,并且还支持包含在 遍历文档树 和 搜索文档树 中的一些特性. 通过 unicode() 方法可以直接将 NavigableString 对象转换成Unicode字符串:

1
2
3
4
5
6
7
unicode_string = unicode(tag.string)

unicode_string
# u'Extremely bold'

type(unicode_string)
# <type 'unicode'>

  tag中包含的字符串不能编辑,但是可以被替换成其它的字符串,用 replaceWith() 方法:

1
2
3
4
tag.string.replaceWith("No longer bold")

tag
# <blockquote>No longer bold</blockquote>

  NavigableString 对象支持 遍历文档树 和 搜索文档树 中定义的大部分属性, 并非全部。尤其是一个字符串不能包含其它内容(tag能够包含字符串或是其它tag),字符串不支持 .contents 或 .string 属性或 find() 方法。
  如果想在Beautiful Soup之外使用 NavigableString 对象,需要调用 unicode() 方法,将该对象转换成普通的Unicode字符串,否则就算Beautiful Soup的方法已经执行结束,该对象的输出也会带有对象的引用地址,这样会浪费内存。

###BeautifulSoup
  BeautifulSoup 对象表示的是一个文档的全部内容。大部分时候,可以把它当作 Tag 对象,它支持遍历文档树 和 搜索文档树 中描述的大部分的方法。
  因为 BeautifulSoup 对象并不是真正的HTML或XML的tag,所以它没有name和attribute属性。但有时查看它的 .name 属性是很方便的,所以 BeautifulSoup 对象包含了一个值为 “[document]” 的特殊属性 .name。

1
2
soup.name
# u'[document]'

###Comment
  注释及特殊字符串,Tag , NavigableString , BeautifulSoup 几乎覆盖了html和xml中的所有内容,但是还有一些特殊对象。容易让人担心的内容是文档的注释部分:

1
2
3
4
5
6
markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
soup = BeautifulSoup(markup)
comment = soup.b.string

type(comment)
# <class 'bs4.element.Comment'>

  Comment 对象是一个特殊类型的 NavigableString 对象:

1
2
comment
# u'Hey, buddy. Want to buy a used parser'

  但是当它出现在HTML文档中时, Comment 对象会使用特殊的格式输出:

1
2
3
4
print(soup.b.prettify())
# <b>
# <!--Hey, buddy. Want to buy a used parser?-->
# </b>

  Beautiful Soup中定义的其它类型都可能会出现在XML的文档中: CData , ProcessingInstruction , Declaration , Doctype 。与 Comment 对象类似,这些类都是 NavigableString 的子类,只是添加了一些额外的方法的字符串独享。下面是用CDATA来替代注释的例子:

1
2
3
4
5
6
7
8
from bs4 import CData
cdata = CData("A CDATA block")
comment.replaceWith(cdata)

print(soup.b.prettify())
# <b>
# <![CDATA[A CDATA block]]>
# </b>

##遍历文档树
  还拿开头的html_doc文档来做例子

###子节点
  一个Tag可能包含多个字符串或其它的Tag,这些都是这个Tag的子节点.Beautiful Soup提供了许多操作和遍历子节点的属性.
注意: Beautiful Soup中字符串节点不支持这些属性,因为字符串没有子节点。

  1. tag的名字

  操作文档树最简单的方法就是告诉它你想获取的tag的name.如果想获取 标签,只要用 soup.head :

1
2
3
4
5
soup.head
# <head><title>The Dormouse's story</title></head>

soup.title
# <title>The Dormouse's story</title>

  这是个获取tag的小窍门,可以在文档树的tag中多次调用这个方法。下面的代码可以获取标签中的第一个标签:

1
2
soup.body.b
# <b>The Dormouse's story</b>

  通过取属性的方式只能获得当前名字的第一个tag:

1
2
soup.a
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

  如果想要得到所有的标签,或是通过名字得到比一个tag更多的内容的时候,就需要用到 Searching the tree 中描述的方法,比如: find_all()

1
2
3
4
5
soup.find_all('a')

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

  1. .contents 和 .children

  tag的 .contents 属性可以将tag的子节点以列表的方式输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
head_tag = soup.head
head_tag
# <head><title>The Dormouse's story</title></head>

head_tag.contents
[<title>The Dormouse's story</title>]

title_tag = head_tag.contents[0]
title_tag
# <title>The Dormouse's story</title>

title_tag.contents
# [u'The Dormouse's story']

  BeautifulSoup 对象本身一定会包含子节点,也就是说标签也是 BeautifulSoup 对象的子节点:

1
2
3
4
len(soup.contents)
# 1
soup.contents[0].name
# u'html'

  字符串没有 .contents 属性,因为字符串没有子节点:

1
2
3
text = title_tag.contents[0]
text.contents
# AttributeError: 'NavigableString' object has no attribute 'contents'

  通过tag的 .children 生成器,可以对tag的子节点进行循环:

1
2
3
for child in title_tag.children:
print(child)
# The Dormouse's story

  1. .descendants

  .contents 和 .children 属性仅包含tag的直接子节点。例如,标签只有一个直接子节点<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">head_tag.contents</span><br><span class="line"><span class="comment"># [<title>The Dormouse's story</title>]</span></span><br></pre></td></tr></table></figure>

  但是标签也包含一个子节点:字符串 “The Dormouse’s story”,这种情况下字符串 “The Dormouse’s story”也属于<head>标签的子孙节点. .descendants 属性可以对所有tag的子孙节点进行递归循环 :<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> child <span class="keyword">in</span> head_tag.descendants:</span><br><span class="line"> print(child)</span><br><span class="line"> <span class="comment"># <title>The Dormouse's story</title></span></span><br><span class="line"> <span class="comment"># The Dormouse's story</span></span><br></pre></td></tr></table></figure></head>

  1. .string

  如果tag只有一个 NavigableString 类型子节点,那么这个tag可以使用 .string 得到子节点:

1
2
title_tag.string
# u'The Dormouse's story'

  如果一个tag仅有一个子节点,那么这个tag也可以使用 .string 方法,输出结果与当前唯一子节点的 .string 结果相同:

1
2
3
4
5
head_tag.contents
# [<title>The Dormouse's story</title>]

head_tag.string
# u'The Dormouse's story'

  如果tag包含了多个子节点,tag就无法确定 .string 方法应该调用哪个子节点的内容, .string 的输出结果是 None :

1
2
print(soup.html.string)
# None

  1. .stings和stipped_strings
      如果tag中包含多个字符串 ,可以使用 .strings 来循环获取:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    for string in soup.strings:
    print(repr(string))

    # u"The Dormouse's story"
    # u'\n\n'
    # u"The Dormouse's story"
    # u'\n\n'
    # u'Once upon a time there were three little sisters; and their names were\n'
    # u'Elsie'
    # u',\n'
    # u'Lacie'
    # u' and\n'
    # u'Tillie'
    # u';\nand they lived at the bottom of a well.'
    # u'\n\n'
    # u'...'
    # u'\n'

  输出的字符串中可能包含了很多空格或空行,使用 .stripped_strings 可以去除多余空白内容:

1
2
3
4
5
6
7
8
9
10
11
12
for string in soup.stripped_strings:
print(repr(string))
# u"The Dormouse's story"
# u"The Dormouse's story"
# u'Once upon a time there were three little sisters; and their names were'
# u'Elsie'
# u','
# u'Lacie'
# u'and'
# u'Tillie'
# u';\nand they lived at the bottom of a well.'
# u'...'

  全部是空格的行会被忽略掉,段首和段末的空白会被删除。

###父节点
  每个tag或字符串都有父节点:被包含在某个tag中.

  1. .parent

  通过 .parent 属性来获取某个元素的父节点.在例子“爱丽丝”的文档中,标签是标签的父节点:<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">title_tag = soup.title</span><br><span class="line">title_tag</span><br><span class="line"><span class="comment"># <title>The Dormouse's story</title></span></span><br><span class="line"></span><br><span class="line">title_tag.parent</span><br><span class="line"><span class="comment"># <head><title>The Dormouse's story</title></head></span></span><br></pre></td></tr></table></figure>

  文档title的字符串也有父节点:标签<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">title_tag.string.parent</span><br><span class="line"><span class="comment"># <title>The Dormouse's story</title></span></span><br></pre></td></tr></table></figure>

  文档的顶层节点比如的父节点是 BeautifulSoup 对象:

1
2
3
html_tag = soup.html
type(html_tag.parent)
# <class 'bs4.BeautifulSoup'>

  BeautifulSoup 对象的 .parent 是None:

1
2
print(soup.parent)
# None

  1. .parents

  通过元素的 .parents 属性可以递归得到元素的所有父辈节点,下面的例子使用了 .parents 方法遍历了a标签到根节点的所有节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
link = soup.a
link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
for parent in link.parents:
if parent is None:
print(parent)
else:
print(parent.name)
# p
# body
# html
# [document]
# None

###兄弟节点
  节点同属于同一个元素的子节点,那么这些节点可以被称为兄弟节点。一段文档以标准格式输出时,兄弟节点有相同的缩进级别。在代码中也可以使用这种关系。

  1. .next_sibling和.previous_sibling

  在文档树中,使用 .next_sibling 和 .previous_sibling 属性来查询兄弟节点:

1
2
3
4
5
6
7
sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></b></a>")

print sibling_soup.b.next_sibling
# <c>text2</c>

print sibling_soup.c.previous_sibling
# <b>text1</b>

  b标签有 .next_sibling 属性,但是没有 .previous_sibling 属性,因为b标签在同级节点中是第一个.同理,c标签有 .previous_sibling 属性,却没有 .next_sibling 属性:

1
2
3
4
5
print(sibling_soup.b.previous_sibling)
# None

print(sibling_soup.c.next_sibling)
# None

  例子中的字符串“text1”和“text2”不是兄弟节点,因为它们的父节点不同:

1
2
3
4
5
sibling_soup.b.string
# u'text1'

print(sibling_soup.b.string.next_sibling)
# None

  1. .next_siblings 和 .previous_siblings

  通过 .next_siblings 和 .previous_siblings 属性可以对当前节点的兄弟节点迭代输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for sibling in soup.a.next_siblings:
print(repr(sibling))

# u',\n'
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
# u' and\n'
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
# u'; and they lived at the bottom of a well.'
# None


for sibling in soup.find(id="link3").previous_siblings:
print(repr(sibling))

# ' and\n'
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
# u',\n'
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
# u'Once upon a time there were three little sisters; and their names were\n'
# None

###回退和前进

  1. .next_element 和 .previous_element
      .next_element 属性指向解析过程中下一个被解析的对象(字符串或tag),结果可能与 .next_sibling 相同,但通常是不一样的.
      这是“爱丽丝”文档中最后一个a标签,它的 .next_sibling 结果是一个字符串,因为当前的解析过程 [2] 因为当前的解析过程因为遇到了a标签而中断了:
    1
    2
    3
    4
    5
    6
    last_a_tag = soup.find("a", id="link3")
    print last_a_tag
    # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

    print last_a_tag.next_sibling
    # '; and they lived at the bottom of a well.'

  但这个a标签的 .next_element 属性结果是在a标签被解析之后的解析内容,不是a标签后的句子部分,应该是字符串”Tillie”:

1
2
print last_a_tag.next_element
# u'Tillie'

  这是因为在原始文档中,字符串“Tillie” 在分号前出现,解析器先进入a标签,然后是字符串“Tillie”,然后关闭a标签,然后是分号和剩余部分.分号与a标签在同一层级,但是字符串“Tillie”会被先解析.

  .previous_element 属性刚好与 .next_element 相反,它指向当前被解析的对象的前一个解析对象:

1
2
3
4
5
print last_a_tag.previous_element
# u' and\n'

print last_a_tag.previous_element.next_element
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

  1. .next_elements 和 .previous_elements

  通过 .next_elements 和 .previous_elements 的迭代器就可以向前或向后访问文档的解析内容,就好像文档正在被解析一样:

1
2
3
4
5
6
7
8
9
for element in last_a_tag.next_elements:
print(repr(element))
# u'Tillie'
# u';\nand they lived at the bottom of a well.'
# u'\n\n'
# <p class="story">...</p>
# u'...'
# u'\n'
# None

###搜索文档树
  Beautiful Soup定义了很多搜索方法,这里着重介绍2个: find() 和 find_all() ,其它方法的参数和用法类似,请读者举一反三。

  1. 过滤器

  介绍 find_all() 方法前,先介绍一下过滤器的类型 ,这些过滤器贯穿整个搜索的API。过滤器可以被用在tag的name中,节点的属性中,字符串中或他们的混合中。

  • 字符串:最简单的过滤器是字符串.在搜索方法中传入一个字符串参数,Beautiful Soup会查找与字符串完整匹配的内容,下面的例子用于查找文档中所有的b标签:
    1
    2
    soup.find_all('b')
    # [<b>The Dormouse's story</b>]

  如果传入字节码参数,Beautiful Soup会当作UTF-8编码,可以传入一段Unicode 编码来避免Beautiful Soup解析编码出错。

  • 正则表达式:如果传入正则表达式作为参数,Beautiful Soup会通过正则表达式的 match() 来匹配内容。下面例子中找出所有以b开头的标签,这表示body和b标签都应该被找到:
    1
    2
    3
    4
    5
    6
    import re

    for tag in soup.find_all(re.compile("^b")):
    print(tag.name)
    # body
    # b

下面代码找出所有名字中包含”t”的标签:

1
2
3
4
5
for tag in soup.find_all(re.compile("t")):
print(tag.name)

# html
# title

  1. 列表

  如果传入列表参数,Beautiful Soup会将与列表中任一元素匹配的内容返回.下面代码找到文档中所有a标签和b标签:

1
2
3
4
5
6
soup.find_all(["a", "b"])

# [<b>The Dormouse's story</b>,
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

  1. True

  True 可以匹配任何值,下面代码查找到所有的tag,但是不会返回字符串节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for tag in soup.find_all(True):
print(tag.name)

# html
# head
# title
# body
# p
# b
# p
# a
# a
# a
# p

  1. 方法

  如果没有合适过滤器,那么还可以定义一个方法,方法只接受一个元素参数 ,如果这个方法返回 True 表示当前元素匹配并且被找到,如果不是则反回 False。
  下面方法校验了当前元素,如果包含 class 属性却不包含 id 属性,那么将返回 True:

1
2
def has_class_but_no_id(tag):
return tag.has_attr('class') and not tag.has_attr('id')

  将这个方法作为参数传入 find_all() 方法,将得到所有p标签:

1
2
3
4
5
soup.find_all(has_class_but_no_id)

# [<p class="title"><b>The Dormouse's story</b></p>,
# <p class="story">Once upon a time there were...</p>,
# <p class="story">...</p>]

  返回结果中只有p标签没有a标签,因为a标签还定义了”id”,没有返回html和head,因为html和head中没有定义”class”属性.
  通过一个方法来过滤一类标签属性的时候, 这个方法的参数是要被过滤的属性的值, 而不是这个标签. 下面的例子是找出 href 属性不符合指定正则的 a 标签:

1
2
3
4
5
6
7
def not_lacie(href):
return href and not re.compile("lacie").search(href)

soup.find_all(href=not_lacie)

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

  标签过滤方法可以使用复杂方法. 下面的例子可以过滤出前后都有文字的标签.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from bs4 import NavigableString

def surrounded_by_strings(tag):

return (isinstance(tag.next_element, NavigableString)

and isinstance(tag.previous_element, NavigableString))

for tag in soup.find_all(surrounded_by_strings):
print tag.name

# p
# a
# a
# a
# p

##find_all()方法剖析
find_all( name , attrs , recursive , string , **kwargs )

find_all() 方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件。

  1. name参数

  name 参数可以查找所有名字为 name 的tag,字符串对象会被自动忽略掉。
  重申: 搜索 name 参数的值可以使任一类型的 过滤器 ,字符窜,正则表达式,列表,方法或是 True 。

  1. keyword 参数

  如果一个指定名字的参数不是搜索内置的参数名,搜索时会把该参数当作指定名字tag的属性来搜索,如果包含一个名字为 id 的参数,Beautiful Soup会搜索每个tag的”id”属性:

1
2
soup.find_all(id='link2')
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

  如果传入 href 参数,Beautiful Soup会搜索每个tag的”href”属性:

1
2
3
soup.find_all(href=re.compile("elsie"))

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

  重申:搜索指定名字的属性时可以使用的参数值包括 字符串 , 正则表达式 , 列表, True 。
  使用多个指定名字的参数可以同时过滤tag的多个属性:

1
2
3
soup.find_all(href=re.compile("elsie"), id='link1')

# [<a class="sister" href="http://example.com/elsie" id="link1">three</a>]

  1. attrs
      有些tag属性在搜索不能使用,比如HTML5中的 data-* 属性:
    1
    2
    3
    data_soup = BeautifulSoup('<div data-foo="value">foo!</div>')
    data_soup.find_all(data-foo="value")
    # SyntaxError: keyword can't be an expression

  但是可以通过 find_all() 方法的 attrs 参数定义一个字典参数来搜索包含特殊属性的tag:

1
2
data_soup.find_all(attrs={"data-foo": "value"})
# [<div data-foo="value">foo!</div>]

  1. 按CSS搜索

  按照CSS类名搜索tag的功能非常实用,但标识CSS类名的关键字 class 在Python中是保留字,使用 class 做参数会导致语法错误.从Beautiful Soup的4.1.1版本开始,可以通过 class_ 参数搜索有指定CSS类名的tag:

1
2
3
4
5
6
7
soup.find_all("a", class_="sister")

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,

# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,

# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

重申:class_ 参数同样接受不同类型的 过滤器 ,字符串,正则表达式,方法或 True :

1
2
3
4
5
6
7
8
9
10
11
12
soup.find_all(class_=re.compile("itl"))
# [<p class="title"><b>The Dormouse's story</b></p>]

def has_six_characters(css_class):
return css_class is not None and len(css_class) == 6

soup.find_all(class_=has_six_characters)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,

# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,

# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

  tag的 class 属性是 多值属性 。按照CSS类名搜索tag时,可以分别搜索tag中的每个CSS类名:

1
2
3
4
5
6
7
8
css_soup = BeautifulSoup('<p class="body strikeout"></p>')

css_soup.find_all("p", class_="strikeout")

# [<p class="body strikeout"></p>]
css_soup.find_all("p", class_="body")

# [<p class="body strikeout"></p>]

搜索 class 属性时也可以通过CSS值完全匹配:

1
2
css_soup.find_all("p", class_="body strikeout")
# [<p class="body strikeout"></p>]

完全匹配 class 的值时,如果CSS类名的顺序与实际不符,将搜索不到结果:

1
2
3
4
5
6
7
soup.find_all("a", attrs={"class": "sister"})

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,

# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,

# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

  1. string 参数

  通过 string 参数可以搜搜文档中的字符串内容。与 name 参数的可选值一样,,重申string 参数接受 字符串 , 正则表达式 , 列表, True . 看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
soup.find_all(string="Elsie")

# [u'Elsie']

soup.find_all(string=["Tillie", "Elsie", "Lacie"])
# [u'Elsie', u'Lacie', u'Tillie']

soup.find_all(string=re.compile("Dormouse"))

[u"The Dormouse's story", u"The Dormouse's story"]

def is_the_only_string_within_a_tag(s):

""Return True if this string is the only child of its parent tag.""
return (s == s.parent.string)

soup.find_all(string=is_the_only_string_within_a_tag)
# [u"The Dormouse's story", u"The Dormouse's story", u'Elsie', u'Lacie', u'Tillie', u'...']

  虽然 string 参数用于搜索字符串,还可以与其它参数混合使用来过滤tag。Beautiful Soup会找到 .string 方法与 string 参数值相符的tag.下面代码用来搜索内容里面包含“Elsie”的a标签:

1
2
3
soup.find_all("a", string="Elsie")

# [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>]

  1. limit 参数

  find_all() 方法返回全部的搜索结构,如果文档树很大那么搜索会很慢;如果我们不需要全部结果,可以使用 limit 参数限制返回结果的数量。效果与SQL中的limit关键字类似,当搜索到的结果数量达到 limit 的限制时,就停止搜索返回结果。
  文档树中有3个tag符合搜索条件,但结果只返回了2个,因为我们限制了返回数量:

1
2
3
4
5
soup.find_all("a", limit=2)

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,

# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

  1. recuresive参数
      调用tag的 find_all() 方法时,Beautiful Soup会检索当前tag的所有子孙节点,如果只想搜索tag的直接子节点,可以使用参数 recursive=False 。
    1
    2
    3
    4
    5
    soup.html.find_all("title")
    # [<title>The Dormouse's story</title>]

    soup.html.find_all("title", recursive=False)
    # []

注意Beautiful Soup 提供了多种DOM树搜索方法。这些方法都使用了类似的参数定义。 比如这些方法: find_all(): name, attrs, text, limit. 但是只有 find_all() 和 find() 支持 recursive 参数。

  1. 像调用 find_all() 一样调用tag

  find_all() 几乎是Beautiful Soup中最常用的搜索方法,所以我们定义了它的简写方法. BeautifulSoup 对象和 tag 对象可以被当作一个方法来使用,这个方法的执行结果与调用这个对象的 find_all() 方法相同,下面两行代码是等价的:

1
2
3
4
5
soup.find_all("a")
soup("a")

soup.title.find_all(string=True)
soup.title(string=True)

##find()方法剖析
find( name , attrs , recursive , string , **kwargs )

  find_all() 方法将返回文档中符合条件的所有tag,尽管有时候我们只想得到一个结果.比如文档中只有一个标签,那么使用 find_all() 方法来查找body标签就不太合适, 使用 find_all 方法并设置 limit=1 参数不如直接使用 find() 方法。下面两行代码是等价的:

1
2
3
4
5
soup.find_all('title', limit=1)
# [<title>The Dormouse's story</title>]

soup.find('title')
# <title>The Dormouse's story</title>

  唯一的区别是 find_all() 方法的返回结果是值包含一个元素的列表,而 find() 方法直接返回结果。
  find_all() 方法没有找到目标是返回空列表,find() 方法找不到目标时,返回 None 。

##find_parents() 和 find_parent()
find_parents( name , attrs , recursive , string , kwargs )
find_parent( name , attrs , recursive , string ,
kwargs )

  我们已经用了很大篇幅来介绍 find_all() 和 find() 方法,Beautiful Soup中还有10个用于搜索的API.它们中的五个用的是与 find_all() 相同的搜索参数,另外5个与 find() 方法的搜索参数类似.区别仅是它们搜索文档的不同部分.

  记住: find_all() 和 find() 只搜索当前节点的所有子节点,孙子节点等. find_parents() 和 find_parent() 用来搜索当前节点的父辈节点,搜索方法与普通tag的搜索方法相同,搜索文档搜索文档包含的内容. 我们从一个文档中的一个叶子节点开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
a_string = soup.find(string="Lacie")

a_string

# u'Lacie'



a_string.find_parents("a")

# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]



a_string.find_parent("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>



a_string.find_parents("p", class="title")

# []

  文档中的一个a标签是是当前叶子节点的直接父节点,所以可以被找到.还有一个p标签,是目标叶子节点的间接父辈节点,所以也可以被找到.包含class值为”title”的p标签不是不是目标叶子节点的父辈节点,所以通过 find_parents() 方法搜索不到.

  find_parent() 和 find_parents() 方法会让人联想到 .parent 和 .parents 属性.它们之间的联系非常紧密.搜索父辈节点的方法实际上就是对 .parents 属性的迭代搜索.

##find_next_siblings()和find_next_sibling()
find_next_siblings( name , attrs , recursive , string , **kwargs )

find_next_sibling( name , attrs , recursive , string , **kwargs )

  这2个方法通过 .next_siblings 属性对当tag的所有后面解析 的兄弟tag节点进行迭代, find_next_siblings() 方法返回所有符合条件的后面的兄弟节点, find_next_sibling() 只返回符合条件的后面的第一个tag节点.

##find_previous_siblings() 和 find_previous_sibling()
find_previous_siblings( name , attrs , recursive , string , kwargs )
find_previous_sibling( name , attrs , recursive , string ,
kwargs )

  这2个方法通过 .previous_siblings 属性对当前tag的前面解析 的兄弟tag节点进行迭代, find_previous_siblings() 方法返回所有符合条件的前面的兄弟节点, find_previous_sibling() 方法返回第一个符合条件的前面的兄弟节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
last_link = soup.find("a", id="link3")

last_link
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

last_link.find_previous_siblings("a")

# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,

# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

first_story_paragraph = soup.find("p", "story")
first_story_paragraph.find_previous_sibling("p")
# <p class="title"><b>The Dormouse's story</b></p>

##find_all_next() 和 find_next()
  这2个方法通过 .next_elements 属性对当前tag的之后的 tag和字符串进行迭代, find_all_next() 方法返回所有符合条件的节点, find_next() 方法返回第一个符合条件的节点:

1
2
3
4
5
6
7
8
9
10
first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_all_next(string=True)
# [u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',
# u';\nand they lived at the bottom of a well.', u'\n\n', u'...', u'\n']

first_link.find_next("p")
# <p class="story">...</p>

  第一个例子中,字符串 “Elsie”也被显示出来,尽管它被包含在我们开始查找的a标签的里面.第二个例子中,最后一个p标签也被显示出来,尽管它与我们开始查找位置的a标签不属于同一部分.例子中,搜索的重点是要匹配过滤器的条件,并且在文档中出现的顺序而不是开始查找的元素的位置.

##find_all_previous() 和 find_previous()
find_all_previous( name , attrs , recursive , string , kwargs )
find_previous( name , attrs , recursive , string ,
kwargs )

  这2个方法通过 .previous_elements 属性对当前节点前面 的tag和字符串进行迭代, find_all_previous() 方法返回所有符合条件的节点, find_previous() 方法返回第一个符合条件的节点.

1
2
3
4
5
6
7
8
9
10
first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_all_previous("p")
# [<p class="story">Once upon a time there were three little sisters; ...</p>,
# <p class="title"><b>The Dormouse's story</b></p>]

first_link.find_previous("title")
# <title>The Dormouse's story</title>

  find_all_previous(“p”) 返回了文档中的第一段(class=”title”的那段),但还返回了第二段,p标签包含了我们开始查找的a标签.不要惊讶,这段代码的功能是查找所有出现在指定a标签之前的p标签,因为这个p标签包含了开始的a标签,所以p标签一定是在a之前出现的.

##CSS选择器
Beautiful Soup支持大部分的CSS选择器 , 在 Tag 或 BeautifulSoup 对象的 .select() 方法中传入字符串参数, 即可使用CSS选择器的语法找到tag:

1
2
3
4
5
soup.select("title")
# [<title>The Dormouse's story</title>]

soup.select("p nth-of-type(3)")
# [<p class="story">...</p>]

通过tag标签逐层查找:

1
2
3
4
5
6
7
8
soup.select("body a")

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("html head title")
# [<title>The Dormouse's story</title>]

找到某个tag标签下的直接子标签 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
soup.select("head > title")
# [<title>The Dormouse's story</title>]

soup.select("p > a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("p > a:nth-of-type(2)")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

soup.select("p > #link1")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select("body > a")
# []

找到兄弟节点标签:

1
2
3
4
5
6
soup.select("#link1 ~ .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("#link1 + .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

通过CSS的类名查找:

1
2
3
4
5
6
7
8
9
10
11
通过CSS的类名查找:

soup.select(".sister")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("[class~=sister]")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

通过tag的id查找:

1
2
3
4
5
soup.select("#link1")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select("a#link2")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

同时用多种CSS选择器查询元素:

1
2
3
soup.select("#link1,#link2")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

通过是否存在某个属性来查找:

1
2
3
4
5
soup.select('a[href]')

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

通过属性的值来查找:

1
2
3
4
5
6
7
8
9
10
11
12
13
soup.select('a[href="http://example.com/elsie"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select('a[href^="http://example.com/"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select('a[href$="tillie"]')
# [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select('a[href*=".com/el"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

通过语言设置来查找:

1
2
3
4
5
6
7
8
9
10
11
12
multilingual_markup = """
<p lang="en">Hello</p>
<p lang="en-us">Howdy, y'all</p>
<p lang="en-gb">Pip-pip, old fruit</p>
<p lang="fr">Bonjour mes amis</p>
"""

multilingual_soup = BeautifulSoup(multilingual_markup)
multilingual_soup.select('p[lang|=en]')
# [<p lang="en">Hello</p>,
# <p lang="en-us">Howdy, y'all</p>,
# <p lang="en-gb">Pip-pip, old fruit</p>]

返回查找到的元素的第一个:

1
2
3
soup.select_one(".sister")

# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

  对于熟悉CSS选择器语法的人来说这是个非常方便的方法.Beautiful Soup也支持CSS选择器API, 如果你仅仅需要CSS选择器的功能,那么直接使用 lxml 也可以, 而且速度更快,支持更多的CSS选择器语法,但Beautiful Soup整合了CSS选择器的语法和自身方便使用API。

##修改文档树
BeautifuSoup的强项是文档树的搜索,当同时也可以方便的修改文档树。

  1. 修改tag的名称和属性

  在 Attributes 的章节中已经介绍过这个功能,但是再看一遍也无妨. 重命名一个tag,改变属性的值,添加或删除属性:

1
2
3
4
5
6
7
8
9
10
11
12
soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b
tag.name = "blockquote"
tag['class'] = 'verybold'
tag['id'] = 1
tag
# <blockquote class="verybold" id="1">Extremely bold</blockquote>

del tag['class']
del tag['id']
tag
# <blockquote>Extremely bold</blockquote>

  1. 修改.string

  给tag的 .string 属性赋值,就相当于用当前的内容替代了原来的内容:

1
2
3
4
5
6
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a
tag.string = "New link text."
tag
# <a href="http://example.com/">New link text.</a>

注意 如果当前的tag包含了其它tag,那么给它的 .string 属性赋值会覆盖掉原有的所有内容包括子tag。

  1. append()
      Tag.append() 方法想tag中添加内容,就好像Python的列表的 .append() 方法:

    1
    2
    3
    4
    5
    6
    soup = BeautifulSoup("<a>Foo</a>")
    soup.a.append("Bar")
    soup
    # <html><head></head><body><a>FooBar</a></body></html>
    soup.a.contents
    # [u'Foo', u'Bar']
  2. NavigableString() 和 .new_tag()

  如果想添加一段文本内容到文档中也没问题,可以调用Python的 append() 方法 或调用 NavigableString 的构造方法:

1
2
3
4
5
6
7
8
9
soup = BeautifulSoup("<b></b>")
tag = soup.b
tag.append("Hello")
new_string = NavigableString(" there")
tag.append(new_string)
tag
# <b>Hello there.</b>
tag.contents
# [u'Hello', u' there']

  如果想要创建一段注释,或 NavigableString 的任何子类, 只要调用 NavigableString 的构造方法:

1
2
3
4
5
6
7
from bs4 import Comment
new_comment = soup.new_string("Nice to see you.", Comment)
tag.append(new_comment)
tag
# <b>Hello there<!--Nice to see you.--></b>
tag.contents
# [u'Hello', u' there', u'Nice to see you.']

这是Beautiful Soup 4.2.1 中新增的方法

  • 创建一个tag最好的方法是调用工厂方法 BeautifulSoup.new_tag() :
1
2
3
4
5
6
7
8
9
soup = BeautifulSoup("<b></b>")
original_tag = soup.b
new_tag = soup.new_tag("a", href="http://www.example.com")
original_tag.append(new_tag)
original_tag
# <b><a href="http://www.example.com"></a></b>
new_tag.string = "Link text."
original_tag
# <b><a href="http://www.example.com">Link text.</a></b>

  第一个参数作为tag的name,是必填,其它参数选填。

  1. insert()

  Tag.insert() 方法与 Tag.append() 方法类似,区别是不会把新元素添加到父节点 .contents 属性的最后,而是把元素插入到指定的位置.与Python列表总的 .insert() 方法的用法下同:

1
2
3
4
5
6
7
8
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a
tag.insert(1, "but did not endorse ")
tag
# <a href="http://example.com/">I linked to but did not endorse <i>example.com</i></a>
tag.contents
# [u'I linked to ', u'but did not endorse', <i>example.com</i>]

  1. insert_before()和insert_after()

insert_before() 方法在当前tag或文本节点前插入内容:

1
2
3
4
5
6
soup = BeautifulSoup("<b>stop</b>")
tag = soup.new_tag("i")
tag.string = "Don't"
soup.b.string.insert_before(tag)
soup.b
# <b><i>Don't</i>stop</b>

insert_after() 方法在当前tag或文本节点后插入内容:

1
2
3
4
5
soup.b.i.insert_after(soup.new_string(" ever "))
soup.b
# <b><i>Don't</i> ever stop</b>
soup.b.contents
# [<i>Don't</i>, u' ever ', u'stop']

  1. clear()

Tag.clear() 方法移除当前tag的内容:

1
2
3
4
5
6
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a
tag.clear()
tag
# <a href="http://example.com/"></a>

  1. extract()

PageElement.extract() 方法将当前tag移除文档树,并作为方法结果返回:

1
2
3
4
5
6
7
8
9
10
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a
i_tag = soup.i.extract()
a_tag
# <a href="http://example.com/">I linked to</a>
i_tag
# <i>example.com</i>
print(i_tag.parent)
None

这个方法实际上产生了2个文档树: 一个是用来解析原始文档的 BeautifulSoup 对象,另一个是被移除并且返回的tag.被移除并返回的tag可以继续调用 extract 方法:

1
2
3
4
5
6
7
my_string = i_tag.string.extract()
my_string
# u'example.com'
print(my_string.parent)
# None
i_tag
# <i></i>

  1. decompose()

Tag.decompose() 方法将当前节点移除文档树并完全销毁:

1
2
3
4
5
6
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a
soup.i.decompose()
a_tag
# <a href="http://example.com/">I linked to</a>

  1. replace_with()

PageElement.replace_with() 方法移除文档树中的某段内容,并用新tag或文本节点替代它:

1
2
3
4
5
6
7
8
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a
new_tag = soup.new_tag("b")
new_tag.string = "example.net"
a_tag.i.replace_with(new_tag)
a_tag
# <a href="http://example.com/">I linked to <b>example.net</b></a>

replace_with() 方法返回被替代的tag或文本节点,可以用来浏览或添加到文档树其它地方.

  1. wrap()

    PageElement.wrap() 方法可以对指定的tag元素进行包装 ,并返回包装后的结果:

    1
    2
    3
    4
    5
    soup = BeautifulSoup("<p>I wish I was bold.</p>")
    soup.p.string.wrap(soup.new_tag("b"))
    # <b>I wish I was bold.</b>
    soup.p.wrap(soup.new_tag("div"))
    # <div><p><b>I wish I was bold.</b></p></div>

注意该方法在 Beautiful Soup 4.0.5 中添加

  1. unwra()
    Tag.unwrap() 方法与 wrap() 方法相反.将移除tag内的所有tag标签,该方法常被用来进行标记的解包:
    1
    2
    3
    4
    5
    6
    markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
    soup = BeautifulSoup(markup)
    a_tag = soup.a
    a_tag.i.unwrap()
    a_tag
    # <a href="http://example.com/">I linked to example.com</a>

与 replace_with() 方法相同, unwrap() 方法返回被移除的tag

##输出

  1. 格式化输出

  prettify() 方法将Beautiful Soup的文档树格式化后以Unicode编码输出,每个XML/HTML标签都独占一行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
soup.prettify()
# '<html>\n <head>\n </head>\n <body>\n <a href="http://example.com/">\n...'
print(soup.prettify())
# <html>
# <head>
# </head>
# <body>
# <a href="http://example.com/">
# I linked to
# <i>
# example.com
# </i>
# </a>
# </body>
# </html>

BeautifulSoup 对象和它的tag节点都可以调用 prettify() 方法.

  1. 压缩输出

  如果只想得到结果字符串,不重视格式,那么可以对一个 BeautifulSoup 对象或 Tag 对象使用Python的 unicode() 或 str() 方法:

1
2
3
4
str(soup)
# '<html><head></head><body><a href="http://example.com/">I linked to <i>example.com</i></a></body></html>'
unicode(soup.a)
# u'<a href="http://example.com/">I linked to <i>example.com</i></a>'

  str() 方法返回UTF-8编码的字符串,可以指定 编码 的设置.还可以调用 encode() 方法获得字节码或调用 decode() 方法获得Unicode.

  1. 输出格式

Beautiful Soup输出是会将HTML中的特殊字符转换成Unicode,比如“&lquot;”:

1
2
3
soup = BeautifulSoup("&ldquo;Dammit!&rdquo; he said.")
unicode(soup)
# u'<html><head></head><body>\u201cDammit!\u201d he said.</body></html>'

如果将文档转换成字符串,Unicode编码会被编码成UTF-8.这样就无法正确显示HTML特殊字符了:

1
2
str(soup)
# '<html><head></head><body>\xe2\x80\x9cDammit!\xe2\x80\x9d he said.</body></html>'

  1. get_text()

如果只想得到tag中包含的文本内容,那么可以调用 get_text() 方法,这个方法获取到tag中包含的所有文版内容包括子孙tag中的内容,并将结果作为Unicode字符串返回:

1
2
3
4
5
6
markup = '<a href="http://example.com/">\nI linked to <i>example.com</i>\n</a>'
soup = BeautifulSoup(markup)
soup.get_text()
u'\nI linked to example.com\n'
soup.i.get_text()
u'example.com'

可以通过参数指定tag的文本内容的分隔符:

1
2
# soup.get_text("|")
u'\nI linked to |example.com|\n'

还可以去除获得文本内容的前后空白:

1
2
# soup.get_text("|", strip=True)
u'I linked to|example.com'

或者使用 .stripped_strings 生成器,获得文本列表后手动处理列表:

1
2
[text for text in soup.stripped_strings]
# [u'I linked to', u'example.com']

##解析器

  1. 指定文档的解析器

  如果仅是想要解析HTML文档,只要用文档创建 BeautifulSoup 对象就可以了.Beautiful Soup会自动选择一个解析器来解析文档.但是还可以通过参数指定使用那种解析器来解析当前文档.

  BeautifulSoup 第一个参数应该是要被解析的文档字符串或是文件句柄,第二个参数用来标识怎样解析文档.如果第二个参数为空,那么Beautiful Soup根据当前系统安装的库自动选择解析器,解析器的优先数序: lxml, html5lib, Python标准库.在下面两种条件下解析器优先顺序会变化:

  • 要解析的文档是什么类型: 目前支持, “html”, “xml”, 和 “html5”
  • 指定使用哪种解析器: 目前支持, “lxml”, “html5lib”, 和 “html.parser”
      安装解析器 章节介绍了可以使用哪种解析器,以及如何安装.

  如果指定的解析器没有安装,Beautiful Soup会自动选择其它方案.目前只有 lxml 解析器支持XML文档的解析,在没有安装lxml库的情况下,创建 beautifulsoup 对象时无论是否指定使用lxml,都无法得到解析后的对象。

  1. 解析器之间的区别

  Beautiful Soup为不同的解析器提供了相同的接口,但解析器本身时有区别的.同一篇文档被不同的解析器解析后可能会生成不同结构的树型文档.区别最大的是HTML解析器和XML解析器,看下面片段被解析成HTML结构:

1
2
BeautifulSoup("<a><b /></a>")
# <html><head></head><body><a><b></b></a></body></html>

  因为空标签b不符合HTML标准,所以解析器把它解析成b>/b
  同样的文档使用XML解析如下(解析XML需要安装lxml库).注意,空标签b 依然被保留,并且文档前添加了XML头,而不是被包含在html标签内:

1
2
3
BeautifulSoup("<a><b /></a>", "xml")
# <?xml version="1.0" encoding="utf-8"?>
# <a><b/></a>

  HTML解析器之间也有区别,如果被解析的HTML文档是标准格式,那么解析器之间没有任何差别,只是解析速度不同,结果都会返回正确的文档树.

  但是如果被解析文档不是标准格式,那么不同的解析器返回结果可能不同.下面例子中,使用lxml解析错误格式的文档,结果p标签被直接忽略掉了:

1
2
BeautifulSoup("<a></p>", "lxml")
# <html><body><a></a></body></html>

  使用html5lib库解析相同的文档会得到不同的结果

1
2
BeautifulSoup("<a></p>", "html5lib")
# <html><head></head><body><a><p></p></a></body></html>

  html5lib库没有忽略掉p标签,而是自动补全了标签,还给文档树添加了head标签.使用pyhton内置库解析结果如下:

1
2
BeautifulSoup("<a></p>", "html.parser")
# <a></a>

  与lxml库类似的,Python内置库忽略掉了p标签,与html5lib库不同的是标准库没有尝试创建符合标准的文档格式或将文档片段包含在body标签内,与lxml不同的是标准库甚至连html标签都没有尝试去添加.
  因为文档片段“a/p”是错误格式,所以以上解析方式都能算作”正确”,html5lib库使用的是HTML5的部分标准,所以最接近”正确”.不过所有解析器的结构都能够被认为是”正常”的.
  不同的解析器可能影响代码执行结果,如果在分发给别人的代码中使用了 BeautifulSoup ,那么最好注明使用了哪种解析器,以减少不必要的麻烦.

##编码
  任何HTML或XML文档都有自己的编码方式,比如ASCII 或 UTF-8,但是使用Beautiful Soup解析后,文档都被转换成了Unicode:

1
2
3
4
5
6
markup = "<h1>Sacr\xc3\xa9 bleu!</h1>"
soup = BeautifulSoup(markup)
soup.h1
# <h1>Sacré bleu!</h1>
soup.h1.string
# u'Sacr\xe9 bleu!'

  这不是魔术(但很神奇),Beautiful Soup用了 编码自动检测 子库来识别当前文档编码并转换成Unicode编码. BeautifulSoup 对象的 .original_encoding 属性记录了自动识别编码的结果:

1
2
soup.original_encoding
'utf-8'

  编码自动检测 功能大部分时候都能猜对编码格式,但有时候也会出错.有时候即使猜测正确,也是在逐个字节的遍历整个文档后才猜对的,这样很慢.如果预先知道文档编码,可以设置编码参数来减少自动检查编码出错的概率并且提高文档解析速度.在创建 BeautifulSoup 对象的时候设置 from_encoding 参数.
  下面一段文档用了ISO-8859-8编码方式,这段文档太短,结果Beautiful Soup以为文档是用ISO-8859-7编码:

1
2
3
4
5
6
markup = b"<h1>\xed\xe5\xec\xf9</h1>"
soup = BeautifulSoup(markup)
soup.h1
<h1>νεμω</h1>
soup.original_encoding
# 'ISO-8859-7'

通过传入 from_encoding 参数来指定编码方式:

1
2
3
4
5
soup = BeautifulSoup(markup, from_encoding="iso-8859-8")
soup.h1
<h1>םולש</h1>
soup.original_encoding
# 'iso8859-8'

&esnp; 如果仅知道文档采用了Unicode编码, 但不知道具体编码. 可以先自己猜测, 猜测错误(依旧是乱码)时, 可以把错误编码作为 exclude_encodings 参数, 这样文档就不会尝试使用这种编码了解码了. 译者备注: 在没有指定编码的情况下, BS会自己猜测编码, 把不正确的编码排除掉, BS就更容易猜到正确编码.

1
2
3
4
5
soup = BeautifulSoup(markup, exclude_encodings=["ISO-8859-7"])
soup.h1
<h1>םולש</h1>
soup.original_encoding
# 'WINDOWS-1255'

&esnp; 猜测结果是 Windows-1255 编码, 猜测结果可能不够准确, 但是 Windows-1255 编码是 ISO-8859-8 的扩展集, 所以猜测结果已经十分接近了, 并且不影响使用. (exclude_encodings 参数是 4.4.0版本的新功能)

&esnp; 少数情况下(通常是UTF-8编码的文档中包含了其它编码格式的文件),想获得正确的Unicode编码就不得不将文档中少数特殊编码字符替换成特殊Unicode编码,“REPLACEMENT CHARACTER” (U+FFFD, �) [9] . 如果Beautifu Soup猜测文档编码时作了特殊字符的替换,那么Beautiful Soup会把 UnicodeDammit 或 BeautifulSoup 对象的 .contains_replacement_characters 属性标记为 True .这样就可以知道当前文档进行Unicode编码后丢失了一部分特殊内容字符.如果文档中包含�而 .contains_replacement_characters 属性是 False ,则表示�就是文档中原来的字符,不是转码失败.

  1. 输出编码

&esnp; 通过Beautiful Soup输出文档时,不管输入文档是什么编码方式,输出编码均为UTF-8编码,下面例子输入文档是Latin-1编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
markup = b'''
<html>
<head>
<meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type" />
</head>
<body>
<p>Sacr\xe9 bleu!</p>
</body>
</html>
'''
soup = BeautifulSoup(markup)
print(soup.prettify())
# <html>
# <head>
# <meta content="text/html; charset=utf-8" http-equiv="Content-type" />
# </head>
# <body>
# <p>
# Sacré bleu!
# </p>
# </body>
# </html>

&esnp; 注意,输出文档中的meta标签的编码设置已经修改成了与输出编码一致的UTF-8.

&esnp; 如果不想用UTF-8编码输出,可以将编码方式传入 prettify() 方法:

1
2
3
4
5
print(soup.prettify("latin-1"))
# <html>
# <head>
# <meta content="text/html; charset=latin-1" http-equiv="Content-type" />
# ...

  还可以调用 BeautifulSoup 对象或任意节点的 encode() 方法,就像Python的字符串调用 encode() 方法一样:

1
2
3
4
soup.p.encode("latin-1")
# '<p>Sacr\xe9 bleu!</p>'
soup.p.encode("utf-8")
# '<p>Sacr\xc3\xa9 bleu!</p>'

  如果文档中包含当前编码不支持的字符,那么这些字符将呗转换成一系列XML特殊字符引用,下面例子中包含了Unicode编码字符SNOWMAN:

1
2
3
markup = u"<b>\N{SNOWMAN}</b>"
snowman_soup = BeautifulSoup(markup)
tag = snowman_soup.b

  SNOWMAN字符在UTF-8编码中可以正常显示(看上去像是☃),但有些编码不支持SNOWMAN字符,比如ISO-Latin-1或ASCII,那么在这些编码中SNOWMAN字符会被转换成“&#9731”:

1
2
3
4
5
6
print(tag.encode("utf-8"))
# <b>☃</b>
print tag.encode("latin-1")
# <b>&#9731;</b>
print tag.encode("ascii")
# <b>&#9731;</b>

  1. Unicode, Dammit! (乱码, 靠!)

  译者备注: UnicodeDammit 是BS内置库, 主要用来猜测文档编码.

  编码自动检测 功能可以在Beautiful Soup以外使用,检测某段未知编码时,可以使用这个方法:

1
2
3
4
5
6
from bs4 import UnicodeDammit
dammit = UnicodeDammit("Sacr\xc3\xa9 bleu!")
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'utf-8'

  如果Python中安装了 chardet 或 cchardet 那么编码检测功能的准确率将大大提高. 输入的字符越多,检测结果越精确,如果事先猜测到一些可能编码, 那么可以将猜测的编码作为参数,这样将优先检测这些编码:

1
2
3
4
5
dammit = UnicodeDammit("Sacr\xe9 bleu!", ["latin-1", "iso-8859-1"])
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'latin-1'

  1. 智能引导

使用Unicode时,Beautiful Soup还会智能的把引号 转换成HTML或XML中的特殊字符:

1
2
3
4
5
markup = b"<p>I just \x93love\x94 Microsoft Word\x92s smart quotes</p>"
UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="html").unicode_markup
# u'<p>I just &ldquo;love&rdquo; Microsoft Word&rsquo;s smart quotes</p>'
UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup
# u'<p>I just &#x201C;love&#x201D; Microsoft Word&#x2019;s smart quotes</p>'

也可以把引号转换为ASCII码:

1
2
UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="ascii").unicode_markup
# u'<p>I just "love" Microsoft Word\'s smart quotes</p>'

很有用的功能,但是Beautiful Soup没有使用这种方式.默认情况下,Beautiful Soup把引号转换成Unicode:

1
2
UnicodeDammit(markup, ["windows-1252"]).unicode_markup
# u'<p>I just \u201clove\u201d Microsoft Word\u2019s smart quotes</p>'

  1. 矛盾的编码

  有时文档的大部分都是用UTF-8,但同时还包含了Windows-1252编码的字符,就像微软的智能引号 一样. 一些包含多个信息的来源网站容易出现这种情况. UnicodeDammit.detwingle() 方法可以把这类文档转换成纯UTF-8编码格式,看个简单的例子:

1
2
3
snowmen = (u"\N{SNOWMAN}" * 3)
quote = (u"\N{LEFT DOUBLE QUOTATION MARK}I like snowmen!\N{RIGHT DOUBLE QUOTATION MARK}")
doc = snowmen.encode("utf8") + quote.encode("windows_1252")

  这段文档很杂乱,snowmen是UTF-8编码,引号是Windows-1252编码,直接输出时不能同时显示snowmen和引号,因为它们编码不同:

1
2
3
4
5
print(doc)
# ☃☃☃�I like snowmen!�

print(doc.decode("windows-1252"))
# ☃☃☃“I like snowmen!”

  如果对这段文档用UTF-8解码就会得到 UnicodeDecodeError 异常,如果用Windows-1252解码就回得到一堆乱码. 幸好, UnicodeDammit.detwingle() 方法会吧这段字符串转换成UTF-8编码,允许我们同时显示出文档中的snowmen和引号:

1
2
3
new_doc = UnicodeDammit.detwingle(doc)
print(new_doc.decode("utf8"))
# ☃☃☃“I like snowmen!”

  UnicodeDammit.detwingle() 方法只能解码包含在UTF-8编码中的Windows-1252编码内容,但这解决了最常见的一类问题.

  在创建 BeautifulSoup 或 UnicodeDammit 对象前一定要先对文档调用 UnicodeDammit.detwingle() 确保文档的编码方式正确.如果尝试去解析一段包含Windows-1252编码的UTF-8文档,就会得到一堆乱码,比如: ☃☃☃“I like snowmen!”.
  UnicodeDammit.detwingle() 方法在Beautiful Soup 4.1.0版本中新增

###比较对象是否相同
  两个 NavigableString 或 Tag 对象具有相同的HTML或XML结构时, Beautiful Soup就判断这两个对象相同. 这个例子中, 2个 b标签在 BS 中是相同的, 尽管他们在文档树的不同位置, 但是具有相同的表象: “b pizza /b”

1
2
3
4
5
6
7
markup = "<p>I want <b>pizza</b> and more <b>pizza</b>!</p>"
soup = BeautifulSoup(markup, 'html.parser')
first_b, second_b = soup.find_all('b')
print first_b == second_b
# True
print first_b.previous_element == second_b.previous_element
# False

如果想判断两个对象是否严格的指向同一个对象可以通过 is 来判断

1
2
print first_b is second_b
# False

###复制Beautiful Soup对象
copy.copy() 方法可以复制任意 Tag 或 NavigableString 对象

1
2
3
4
import copy
p_copy = copy.copy(soup.p)
print p_copy
# <p>I want <b>pizza</b> and more <b>pizza</b>!</p>

复制后的对象跟与对象是相等的, 但指向不同的内存地址

1
2
3
4
print soup.p == p_copy
# True
print soup.p is p_copy
# False

  源对象和复制对象的区别是源对象在文档树中, 而复制后的对象是独立的还没有添加到文档树中. 复制后对象的效果跟调用了 extract() 方法相同.

1
2
print p_copy.parent
# None

这是因为相等的对象不能同时插入相同的位置

###解析部分文档
  如果仅仅因为想要查找文档中的a>标签而将整片文档进行解析,实在是浪费内存和时间.最快的方法是从一开始就把a>标签以外的东西都忽略掉. SoupStrainer 类可以定义文档的某段内容,这样搜索文档时就不必先解析整篇文档,只会解析在 SoupStrainer 中定义过的文档. 创建一个 SoupStrainer 对象并作为 parse_only 参数给 BeautifulSoup 的构造方法即可

  1. SoupStrainer
      SoupStrainer 类接受与典型搜索方法相同的参数:name , attrs , recursive , string , **kwargs 。下面举例说明三种 SoupStrainer 对象:
    1
    2
    3
    4
    5
    6
    from bs4 import SoupStrainer
    only_a_tags = SoupStrainer("a")
    only_tags_with_id_link2 = SoupStrainer(id="link2")
    def is_short_string(string):
    return len(string) < 10
    only_short_strings = SoupStrainer(string=is_short_string)

再拿“爱丽丝”文档来举例,来看看使用三种 SoupStrainer 对象做参数会有什么不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><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>
"""
print(BeautifulSoup(html_doc, "html.parser", parse_only=only_a_tags).prettify())
# <a class="sister" href="http://example.com/elsie" id="link1">
# Elsie
# </a>
# <a class="sister" href="http://example.com/lacie" id="link2">
# Lacie
# </a>
# <a class="sister" href="http://example.com/tillie" id="link3">
# Tillie
# </a>
print(BeautifulSoup(html_doc, "html.parser", parse_only=only_tags_with_id_link2).prettify())
# <a class="sister" href="http://example.com/lacie" id="link2">
# Lacie
# </a>
print(BeautifulSoup(html_doc, "html.parser", parse_only=only_short_strings).prettify())
# Elsie
# ,
# Lacie
# and
# Tillie
# ...
#

还可以将 SoupStrainer 作为参数传入 搜索文档树 中提到的方法.这可能不是个常用用法,所以还是提一下:

1
2
3
4
soup = BeautifulSoup(html_doc)
soup.find_all(only_short_strings)
# [u'\n\n', u'\n\n', u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',
# u'\n\n', u'...', u'\n']

###常见问题

  1. 代码诊断

  如果想知道Beautiful Soup到底怎样处理一份文档,可以将文档传入 diagnose() 方法(Beautiful Soup 4.2.0中新增),Beautiful Soup会输出一份报告,说明不同的解析器会怎样处理这段文档,并标出当前的解析过程会使用哪种解析器:

1
2
3
4
5
6
7
8
9
10
11
12
from bs4.diagnose import diagnose
data = open("bad.html").read()
diagnose(data)

# Diagnostic running on Beautiful Soup 4.2.0
# Python version 2.7.3 (default, Aug 1 2012, 05:16:07)
# I noticed that html5lib is not installed. Installing it may help.
# Found lxml version 2.3.2.0
#
# Trying to parse your data with html.parser
# Here's what html.parser did with the document:
# ...

  diagnose() 方法的输出结果可能帮助你找到问题的原因,如果不行,还可以把结果复制出来以便寻求他人的帮助.

  1. 文档解析错误

  文档解析错误有两种.一种是崩溃,Beautiful Soup尝试解析一段文档结果却抛除了异常,通常是 HTMLParser.HTMLParseError;一种异常情况,是Beautiful Soup解析后的文档树看起来与原来的内容相差很多.

  这些错误几乎都不是Beautiful Soup的原因,这不是因为Beautiful Soup得代码写的太优秀,而是因为Beautiful Soup没有包含任何文档解析代码.异常产生自被依赖的解析器,如果解析器不能很好的解析出当前的文档,那么最好的办法是换一个解析器.更多细节查看 安装解析器 章节.

  最常见的解析错误是 HTMLParser.HTMLParseError: malformed start tag 和 HTMLParser.HTMLParseError: bad end tag .这都是由Python内置的解析器引起的,解决方法是 安装lxml或html5lib

  最常见的异常现象是当前文档找不到指定的Tag,而这个Tag光是用眼睛就足够发现的了. find_all() 方法返回 [] ,而 find() 方法返回 None .这是Python内置解析器的又一个问题: 解析器会跳过那些它不知道的tag.解决方法还是 安装lxml或html5lib

  1. 版本错误

;

  • SyntaxError: Invalid syntax (异常位置在代码行: ROOT_TAG_NAME = u’[document]’ ),因为Python2版本的代码没有经过迁移就在Python3中窒息感
  • ImportError: No module named HTMLParser 因为在Python3中执行Python2版本的Beautiful Soup
  • ImportError: No module named html.parser 因为在Python2中执行Python3版本的Beautiful Soup
  • ImportError: No module named BeautifulSoup 因为在没有安装BeautifulSoup3库的Python环境下执行代码,或忘记了BeautifulSoup4的代码需要从 bs4 包中引入
  • ImportError: No module named bs4 因为当前Python环境下还没有安装BeautifulSoup4

###解析成XML

  默认情况下,Beautiful Soup会将当前文档作为HTML格式解析,如果要解析XML文档,要在 BeautifulSoup 构造方法中加入第二个参数 “xml”:

1
soup = BeautifulSoup(markup, "xml")

当然,还需要 安装lxml
解析器的错误

  • 如果同样的代码在不同环境下结果不同,可能是因为两个环境下使用不同的解析器造成的.例如这个环境中安装了lxml,而另一个环境中只有html5lib, 解析器之间的区别 中说明了原因.修复方法是在 BeautifulSoup 的构造方法中中指定解析器
  • 因为HTML标签是 大小写敏感 的,所以3种解析器再出来文档时都将tag和属性转换成小写.例如文档中的 TAG会被转换为 tag.如果想要保留tag的大写的话,那么应该将文档 解析成XML .

杂项错误

  • UnicodeEncodeError: ‘charmap’ codec can’t encode character u’\xfoo’ in position bar (或其它类型的 UnicodeEncodeError )的错误,主要是两方面的错误(都不是Beautiful Soup的原因),第一种是正在使用的终端(console)无法显示部分Unicode,参考 Python wiki ,第二种是向文件写入时,被写入文件不支持部分Unicode,这时只要用 u.encode(“utf8”) 方法将编码转换为UTF-8.
  • KeyError: [attr] 因为调用 tag[‘attr’] 方法而引起,因为这个tag没有定义该属性.出错最多的是 KeyError: ‘href’ 和 KeyError: ‘class’ .如果不确定某个属性是否存在时,用 tag.get(‘attr’) 方法去获取它,跟获取Python字典的key一样
  • AttributeError: ‘ResultSet’ object has no attribute ‘foo’ 错误通常是因为把 find_all() 的返回结果当作一个tag或文本节点使用,实际上返回结果是一个列表或 ResultSet 对象的字符串,需要对结果进行循环才能得到每个节点的 .foo 属性.或者使用 find() 方法仅获取到一个节点
  • AttributeError: ‘NoneType’ object has no attribute ‘foo’ 这个错误通常是在调用了 find() 方法后直节点取某个属性 .foo 但是 find() 方法并没有找到任何结果,所以它的返回值是 None .需要找出为什么 find() 的返回值是 None .

如何提高效率

  Beautiful Soup对文档的解析速度不会比它所依赖的解析器更快,如果对计算时间要求很高或者计算机的时间比程序员的时间更值钱,那么就应该直接使用 lxml .
  换句话说,还有提高Beautiful Soup效率的办法,使用lxml作为解析器.Beautiful Soup用lxml做解析器比用html5lib或Python内置解析器速度快很多.
  安装 cchardet 后文档的解码的编码检测会速度更快
  解析部分文档 不会节省多少解析时间,但是会节省很多内存,并且搜索时也会变得更快.

需要的解析器
  Beautiful Soup 3曾使用Python的 SGMLParser 解析器,这个模块在Python3中已经被移除了.Beautiful Soup 4默认使用系统的 html.parser ,也可以使用lxml或html5lib扩展库代替.查看 安装解析器 章节

  因为解析器 html.parser 与 SGMLParser 不同. BS4 和 BS3 处理相同的文档会产生不同的对象结构. 使用lxml或html5lib解析文档的时候, 如果添加了 html.parser 参数, 解析的对象又回发生变化. 如果发生了这种情况, 只能修改对应的处文档结果处理代码了.

方法名的变化

  • renderContents -> encode_contents
  • replaceWith -> replace_with
  • replaceWithChildren -> unwrap
  • findAll -> find_all
  • findAllNext -> find_all_next
  • findAllPrevious -> find_all_previous
  • findNext -> find_next
  • findNextSibling -> find_next_sibling
  • findNextSiblings -> find_next_siblings
  • findParent -> find_parent
  • findParents -> find_parents
  • findPrevious -> find_previous
  • findPreviousSibling -> find_previous_sibling
  • findPreviousSiblings -> find_previous_siblings
  • nextSibling -> next_sibling
  • previousSibling -> previous_sibling

Beautiful Soup构造方法的参数部分也有名字变化:

  • BeautifulSoup(parseOnlyThese=…) -> BeautifulSoup(parse_only=…)
  • BeautifulSoup(fromEncoding=…) -> BeautifulSoup(from_encoding=…)

为了适配Python3,修改了一个方法名:

  • Tag.has_key() -> Tag.has_attr()

修改了一个属性名,让它看起来更专业点:

  • Tag.isSelfClosing -> Tag.is_empty_element

  修改了下面3个属性的名字,以免与Python保留字冲突.这些变动不是向下兼容的,如果在BS3中使用了这些属性,那么在BS4中这些代码无法执行.

  • UnicodeDammit.Unicode -> UnicodeDammit.Unicode_markup``
  • Tag.next -> Tag.next_element
  • Tag.previous -> Tag.previous_element

生成器
  将下列生成器按照PEP8标准重新命名,并转换成对象的属性:

  • childGenerator() -> children
  • nextGenerator() -> next_elements
  • nextSiblingGenerator() -> next_siblings
  • previousGenerator() -> previous_elements
  • previousSiblingGenerator() -> previous_siblings
  • recursiveChildGenerator() -> descendants
  • parentGenerator() -> parents

  BS3中有的生成器循环结束后会返回 None 然后结束.这是个bug.新版生成器不再返回 None .

  BS4中增加了2个新的生成器, .strings 和 stripped_strings . .strings 生成器返回NavigableString对象, .stripped_strings 方法返回去除前后空白的Python的string对象.

XML

  BS4中移除了解析XML的 BeautifulStoneSoup 类.如果要解析一段XML文档,使用 BeautifulSoup 构造方法并在第二个参数设置为“xml”.同时 BeautifulSoup 构造方法也不再识别 isHTML 参数.

  Beautiful Soup处理XML空标签的方法升级了.旧版本中解析XML时必须指明哪个标签是空标签. 构造方法的 selfClosingTags 参数已经不再使用.新版Beautiful Soup将所有空标签解析为空元素,如果向空元素中添加子节点,那么这个元素就不再是空元素了.