这是我见过最牛逼,最全面的Beautiful Soup 4.2 教程!没有之一

Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式.Beautiful Soup会帮你节省数小时甚至数天的工作时间.

这篇文档介绍了BeautifulSoup4中所有主要特性,并且有小例子.让我来向你展示它适合做什么,如何工作,怎样使用,如何达到你想要的效果,和处理异常情况.

文档中出现的例子在Python2.7和Python3.2中的执行结果相同

你可能在寻找 Beautiful Soup3 的文档,Beautiful Soup 3 目前已经停止开发,我们推荐在现在的项目中使用Beautiful Soup 4, 移植到BS4

寻求帮助

如果你有关于BeautifulSoup的问题,可以发送邮件到 讨论组 .如果你的问题包含了一段需要转换的HTML代码,那么确保你提的问题描述中附带这段HTML文档的 代码诊断 [1]

快速开始

下面的一段HTML代码将作为例子被多次用到.这是 爱丽丝梦游仙境的 的一段内容(以后内容中简称为 爱丽丝 的文档):

html_doc = """

The Dormouse's story

The Dormouse's story

Once upon a time there were three little sisters; and their names were

,

and

;

and they lived at the bottom of a well.

...

"""

使用BeautifulSoup解析这段代码,能够得到一个 BeautifulSoup 的对象,并能按照标准的缩进格式的结构输出:

from bs4 import BeautifulSoup

soup = BeautifulSoup(html_doc)

print(soup.prettify())

#

#

#

<p># The Dormouse's story</p><p># </p>

#

#

#

#

# The Dormouse's story

#

#

#

# Once upon a time there were three little sisters; and their names were

#

# Elsie

#

# ,

#

# Lacie

#

# and

#

# Tillie

#

# ; and they lived at the bottom of a well.

#

#

# ...

#

#

#

几个简单的浏览结构化数据的方法:

soup.title

#

The Dormouse's story

soup.title.name

# u'title'

soup.title.string

# u'The Dormouse's story'

soup.title.parent.name

# u'head'

soup.p

#

The Dormouse's story

soup.p['class']

# u'title'

soup.a

#

soup.find_all('a')

# [ ,

# ,

# ]

soup.find(id="link3")

#

从文档中找到所有

for link in soup.find_all('a'):

print(link.get('href'))

# http://example.com/elsie

# http://example.com/lacie

# http://example.com/tillie

从文档中获取所有文字内容:

print(soup.get_text())

# The Dormouse's story

#

# The Dormouse's story

#

# Once upon a time there were three little sisters; and their names were

# Elsie,

# Lacie and

# Tillie;

# and they lived at the bottom of a well.

#

# ...

这是你想要的吗?别着急,还有更好用的

安装 Beautiful Soup

如果你用的是新版的Debain或ubuntu,那么可以通过系统的软件包管理来安装:

$ apt-get install Python-bs4

Beautiful Soup 4 通过PyPi发布,所以如果你无法使用系统包管理安装,那么也可以通过 easy_install 或 pip 来安装.包的名字是 beautifulsoup4 ,这个包兼容Python2和Python3.

$ easy_install beautifulsoup4

$ pip install beautifulsoup4

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

如果你没有安装 easy_install 或 pip ,那你也可以 下载BS4的源码 ,然后通过setup.py来安装.

$ Python setup.py install

如果上述安装方法都行不通,Beautiful Soup的发布协议允许你将BS4的代码打包在你的项目中,这样无须安装即可使用.

作者在Python2.7和Python3.2的版本下开发Beautiful Soup, 理论上Beautiful Soup应该在所有当前的Python版本中正常工作

安装完成后的问题

Beautiful Soup发布时打包成Python2版本的代码,在Python3环境下安装时,会自动转换成Python3的代码,如果没有一个安装的过程,那么代码就不会被转换.

如果代码抛出了 ImportError 的异常: “No module named HTMLParser”, 这是因为你在Python3版本中执行Python2版本的代码.

如果代码抛出了 ImportError 的异常: “No module named html.parser”, 这是因为你在Python2版本中执行Python3版本的代码.

如果遇到上述2种情况,最好的解决方法是重新安装BeautifulSoup4.

如果在ROOT_TAG_NAME = u’[document]’代码处遇到 SyntaxError “Invalid syntax”错误,需要将把BS4的Python代码版本从Python2转换到Python3. 可以重新安装BS4:

$ Python3 setup.py install

或在bs4的目录中执行Python代码版本转换脚本

$ 2to3-3.2 -w bs4

安装解析器

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

$ apt-get install Python-lxml

$ easy_install lxml

$ pip install lxml

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

$ apt-get install Python-html5lib

$ easy_install html5lib

$ pip install html5lib

下表列出了主要的解析器,以及它们的优缺点:

  • 解析器使用方法优势劣势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语言库
  • html5libBeautifulSoup(markup, "html5lib")最好的容错性
  • 以浏览器的方式解析文档
  • 生成HTML5格式的文档
  • 速度慢
  • 不依赖外部扩展

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

提示: 如果一段HTML或XML文档格式不正确的话,那么在不同的解析器中返回的结果可能是不一样的,查看 解析器之间的区别 了解更多细节

如何使用

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

from bs4 import BeautifulSoup

soup = BeautifulSoup(open("index.html"))

soup = BeautifulSoup("data")

首先,文档被转换成Unicode,并且HTML的实例都被转换成Unicode编码

BeautifulSoup("Sacré bleu!")

Sacré bleu!

然后,Beautiful Soup选择最合适的解析器来解析这段文档,如果手动指定解析器那么Beautiful Soup会选择指定的解析器来解析文档.(参考 解析成XML ).

对象的种类

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

Tag

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

soup = BeautifulSoup('Extremely bold')

tag = soup.b

type(tag)

#

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

Name

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

tag.name

# u'b'

如果改变了tag的name,那将影响所有通过当前Beautiful Soup对象生成的HTML文档:

tag.name = "blockquote"

tag

#

Extremely bold

Attributes

一个tag可能有很多个属性. tag 有一个 “class” 的属性,值为 “boldest” . tag的属性的操作方法与字典相同:

tag['class']

# u'boldest'

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

tag.attrs

# {u'class': u'boldest'}

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

tag['class'] = 'verybold'

tag['id'] = 1

tag

#

Extremely bold

del tag['class']

del

tag['id']

tag

#

Extremely bold

tag['class']

# KeyError: 'class'

print(tag.get('class'))

# None

多值属性

HTML 4定义了一系列可以包含多个值的属性.在HTML5中移除了一些,却增加更多.最常见的多值的属性是 class (一个tag可以有多个CSS的class). 还有一些属性 rel , rev , accept-charset , headers , accesskey . 在Beautiful Soup中多值属性的返回类型是list:

css_soup = BeautifulSoup('

')

css_soup.p['class']

# ["body", "strikeout"]

css_soup = BeautifulSoup('

')

css_soup.p['class']

# ["body"]

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

id_soup = BeautifulSoup('

')

id_soup.p['id']

# 'my id'

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

rel_soup = BeautifulSoup('

Back to the

')

rel_soup.a['rel']

# ['index']

rel_soup.a['rel'] = ['index', 'contents']

print(rel_soup.p)

#

Back to the

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

xml_soup = BeautifulSoup('

', 'xml')

xml_soup.p['class']

# u'body strikeout'

可以遍历的字符串

字符串常被包含在tag内.Beautiful Soup用 NavigableString 类来包装tag中的字符串:

tag.string

# u'Extremely bold'

type(tag.string)

#

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

unicode_string = unicode(tag.string)

unicode_string

# u'Extremely bold'

type(unicode_string)

#

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

tag.string.replace_with("No longer bold")

tag

#

No longer bold

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

soup.name

# u'[document]'

注释及特殊字符串

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

markup = ""

soup = BeautifulSoup(markup)

comment = soup.b.string

type(comment)

#

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

comment

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

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

print(soup.b.prettify())

#

#

#

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

from bs4 import CData

cdata = CData("A CDATA block")

comment.replace_with(cdata)

print(soup.b.prettify())

#

#

#

遍历文档树

还拿”爱丽丝梦游仙境”的文档来做例子:

html_doc = """

The Dormouse's story

The Dormouse's story

Once upon a time there were three little sisters; and their names were

,

and

;

and they lived at the bottom of a well.

...

"""

from bs4 import BeautifulSoup

soup = BeautifulSoup(html_doc)

通过这段例子来演示怎样从文档的一段内容找到另一段内容

子节点

一个Tag可能包含多个字符串或其它的Tag,这些都是这个Tag的子节点.Beautiful Soup提供了许多操作和遍历子节点的属性.

注意: Beautiful Soup中字符串节点不支持这些属性,因为字符串没有子节点

tag的名字

操作文档树最简单的方法就是告诉它你想获取的tag的name.如果想获取

标签,只要用 soup.head :

soup.head

#

The Dormouse's story

soup.title

#

The Dormouse's story

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

标签中的第一个标签:

soup.body.b

# The Dormouse's story

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

soup.a

#

如果想要得到所有的

soup.find_all('a')

# [ ,

# ,

# ]

.contents 和 .children

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

head_tag = soup.head

head_tag

#

The Dormouse's story

head_tag.contents

[

The Dormouse's story]

title_tag = head_tag.contents[0]

title_tag

#

The Dormouse's story

title_tag.contents

# [u'The Dormouse's story']

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

len(soup.contents)

# 1

soup.contents[0].name

# u'html'

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

text = title_tag.contents[0]

text.contents

# AttributeError: 'NavigableString' object has no attribute 'contents'

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

for child in title_tag.children:

print(child)

# The Dormouse's story

.descendants

.contents 和 .children 属性仅包含tag的直接子节点.例如,

标签只有一个直接子节点<p>head_tag.contents</p><p># [</p><title>The Dormouse's story]

但是

标签也包含一个子节点:字符串 “The Dormouse’s story”,这种情况下字符串 “The Dormouse’s story”也属于标签的子孙节点. .descendants 属性可以对所有tag的子孙节点进行递归循环 [5] :<p> <div class="lazy-load-ad"></div><strong>for</strong> child <strong>in</strong> head_tag.descendants:</p><p> <strong>print</strong>(child)</p><p> # </p><title>The Dormouse's story

# The Dormouse's story

上面的例子中,

标签只有一个子节点,但是有2个子孙节点:节点和的子节点, BeautifulSoup 有一个直接子节点(节点),却有很多子孙节点:

len(list(soup.children))

# 1

len(list(soup.descendants))

# 25

.string

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

title_tag.string

# u'The Dormouse's story'

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

head_tag.contents

# [

The Dormouse's story]

head_tag.string

# u'The Dormouse's story'

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

print(soup.html.string)

# None

.strings 和 stripped_strings

如果tag中包含多个字符串 [2] ,可以使用 .strings 来循环获取:

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 可以去除多余空白内容:

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中

.parent

通过 .parent 属性来获取某个元素的父节点.在例子“爱丽丝”的文档中,

标签是标签的父节点:<p>title_tag = soup.title</p><p>title_tag</p><p># </p><title>The Dormouse's story

title_tag.parent

#

The Dormouse's story

文档title的字符串也有父节点:

标签<p>title_tag.string.parent</p><p># </p><title>The Dormouse's story

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

html_tag = soup.html

type(html_tag.parent)

#

BeautifulSoup 对象的 .parent 是None:

print(soup.parent)

# None

.parents

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

link = soup.a

link

#

for parent in link.parents:

if parent is None:

print(parent)

else:

print(parent.name)

# p

# body

# html

# [document]

# None

兄弟节点

看一段简单的例子:

sibling_soup = BeautifulSoup(" ")

print(sibling_soup.prettify())

#

#

#

#

# text1

#

#

# text2

#

#

#

#

因为标签和标签是同一层:他们是同一个元素的子节点,所以

可以被称为兄弟节点.一段文档以标准格式输出时,兄弟节点有相同的缩进级别.在代码中也可以使用这种关系.

.next_sibling 和 .previous_sibling

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

sibling_soup.b.next_sibling

# text2

sibling_soup.c.previous_sibling

# text1

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

print(sibling_soup.b.previous_sibling)

# None

print(sibling_soup.c.next_sibling)

# None

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

sibling_soup.b.string

# u'text1'

print(sibling_soup.b.string.next_sibling)

# None

实际文档中的tag的 .next_sibling 和 .previous_sibling 属性通常是字符串或空白. 看看“爱丽丝”文档:

如果以为第一个

link = soup.a

link

#

link.next_sibling

# u',\n'

第二个

link.next_sibling.next_sibling

#

.next_siblings 和 .previous_siblings

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

for sibling in soup.a.next_siblings:

print(repr(sibling))

# u',\n'

#

# u' and\n'

#

# 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'

#

# u',\n'

#

# u'Once upon a time there were three little sisters; and their names were\n'

# None

回退和前进

看一下“爱丽丝” 文档:

The Dormouse's story

The Dormouse's story

HTML解析器把这段字符串转换成一连串的事件: “打开标签”,”打开一个

标签”,”打开一个标签”,”添加一段字符串”,”关闭<title>标签”,”打开<p>标签”,等等.Beautiful Soup提供了重现解析器初始化过程的方法.</p><p>.next_element 和 .previous_element</p><p>.next_element 属性指向解析过程中下一个被解析的对象(字符串或tag),结果可能与 .next_sibling 相同,但通常是不一样的.</p><p>这是“爱丽丝”文档中最后一个</p><p>last_a_tag = soup.find("a", id="link3")</p><p>last_a_tag</p><p># </p><p>last_a_tag.next_sibling</p><p># '; and they lived at the bottom of a well.'</p><p>但这个</p><p>last_a_tag.next_element</p><p># u'Tillie'</p><p>这是因为在原始文档中,字符串“Tillie” 在分号前出现,解析器先进入 标签,然后是分号和剩余部分.分号与 <div class="lazy-load-ad"></div></p><p>.previous_element 属性刚好与 .next_element 相反,它指向当前被解析的对象的前一个解析对象:</p><p>last_a_tag.previous_element</p><p># u' and\n'</p><p>last_a_tag.previous_element.next_element</p><p># </p><p>.next_elements 和 .previous_elements</p><p>通过 .next_elements 和 .previous_elements 的迭代器就可以向前或向后访问文档的解析内容,就好像文档正在被解析一样:</p><p><strong>for</strong> element <strong>in</strong> last_a_tag.next_elements:</p><p> <strong>print</strong>(repr(element))</p><p># u'Tillie'</p><p># u';\nand they lived at the bottom of a well.'</p><p># u'\n\n'</p><p># </p><p class="story">...</p><p># u'...'</p><p># u'\n'</p><p># None</p><h1>搜索文档树</h1><p>Beautiful Soup定义了很多搜索方法,这里着重介绍2个: find() 和 find_all() .其它方法的参数和用法类似,请读者举一反三. <div class="lazy-load-ad"></div></p><p>再以“爱丽丝”文档作为例子:</p><p>html_doc = """</p><p/><title>The Dormouse's story

The Dormouse's story

Once upon a time there were three little sisters; and their names were

,

and

;

and they lived at the bottom of a well.

...

"""

from bs4 import BeautifulSoup

soup = BeautifulSoup(html_doc)

使用 find_all() 类似的方法可以查找到想要查找的文档内容

过滤器

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

字符串

最简单的过滤器是字符串.在搜索方法中传入一个字符串参数,Beautiful Soup会查找与字符串完整匹配的内容,下面的例子用于查找文档中所有的标签:

soup.find_all('b')

# [The Dormouse's story]

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

正则表达式

如果传入正则表达式作为参数,Beautiful Soup会通过正则表达式的 match() 来匹配内容.下面例子中找出所有以b开头的标签,这表示

标签都应该被找到:

import re

for tag in soup.find_all(re.compile("^b")):

print(tag.name)

# body

# b

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

for tag in soup.find_all(re.compile("t")):

print(tag.name)

# html

# title

列表

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

soup.find_all(["a", "b"])

# [The Dormouse's story,

# ,

# ,

# ]

True

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

for tag in soup.find_all(True):

print(tag.name)

# html

# head

# title

# body

# p

# b

# p

# a

# a

# a

# p

方法

如果没有合适过滤器,那么还可以定义一个方法,方法只接受一个元素参数 [4] ,如果这个方法返回 True 表示当前元素匹配并且被找到,如果不是则反回 False

下面方法校验了当前元素,如果包含 class 属性却不包含 id 属性,那么将返回 True:

def has_class_but_no_id(tag):

return tag.has_attr('class') and not tag.has_attr('id')

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

标签:

soup.find_all(has_class_but_no_id)

# [

The Dormouse's story

,

#

Once upon a time there were...

,

#

...

]

返回结果中只有

标签没有

下面代码找到所有被文字包含的节点内容:

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 , text , **kwargs )

find_all() 方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件.这里有几个例子:

soup.find_all("title")

# [

The Dormouse's story]

soup.find_all("p", "title")

# [

The Dormouse's story

]

soup.find_all("a")

# [ ,

# ,

# ]

soup.find_all(id="link2")

# [ ]

import re

soup.find(text=re.compile("sisters"))

# u'Once upon a time there were three little sisters; and their names were\n'

有几个方法很相似,还有几个方法是新的,参数中的 text 和 id 是什么含义? 为什么 find_all("p", "title") 返回的是CSS Class为”title”的

标签? 我们来仔细看一下 find_all() 的参数

name 参数

name 参数可以查找所有名字为 name 的tag,字符串对象会被自动忽略掉.

简单的用法如下:

soup.find_all("title")

# [

The Dormouse's story <div class="lazy-load-ad"></div>]

重申: 搜索 name 参数的值可以使任一类型的 过滤器 ,字符窜,正则表达式,列表,方法或是 True .

keyword 参数

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

soup.find_all(id='link2')

# [ ]

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

soup.find_all(href=re.compile("elsie"))

# [ ]

搜索指定名字的属性时可以使用的参数值包括 字符串 , 正则表达式 , 列表, True .

下面的例子在文档树中查找所有包含 id 属性的tag,无论 id 的值是什么:

soup.find_all(id=True)

# [ ,

# ,

# ]

使用多个指定名字的参数可以同时过滤tag的多个属性:

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

# [ ]

有些tag属性在搜索不能使用,比如HTML5中的 data-* 属性:

data_soup = BeautifulSoup('

foo!
')

data_soup.find_all(data-foo="value")

# SyntaxError: keyword can't be an expression

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

data_soup.find_all(attrs={"data-foo": "value"})

# [

foo!
]

按CSS搜索

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

soup.find_all("a", class_="sister")

# [ ,

# ,

# ]

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

soup.find_all(class_=re.compile("itl"))

# [

The Dormouse's story

]

def has_six_characters(css_class):

return css_class is not None and len(css_class) == 6

soup.find_all(class_=has_six_characters)

# [ ,

# ,

# ]

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

css_soup = BeautifulSoup('

')

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

# [

]

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

# [

]

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

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

# [

]

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

soup.find_all("a", attrs={"class": "sister"})

# [ ,

# ,

# ]

text 参数

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

soup.find_all(text="Elsie")

# [u'Elsie']

soup.find_all(text=["Tillie", "Elsie", "Lacie"])

# [u'Elsie', u'Lacie', u'Tillie']

soup.find_all(text=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(text=is_the_only_string_within_a_tag)

# [u"The Dormouse's story", u"The Dormouse's story", u'Elsie', u'Lacie', u'Tillie', u'...']

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

soup.find_all("a", text="Elsie")

# [ ]

limit 参数

find_all() 方法返回全部的搜索结构,如果文档树很大那么搜索会很慢.如果我们不需要全部结果,可以使用 limit 参数限制返回结果的数量.效果与SQL中的limit关键字类似,当搜索到的结果数量达到 limit 的限制时,就停止搜索返回结果.

文档树中有3个tag符合搜索条件,但结果只返回了2个,因为我们限制了返回数量:

soup.find_all("a", limit=2)

# [ ,

# ]

recursive 参数

调用tag的 find_all() 方法时,Beautiful Soup会检索当前tag的所有子孙节点,如果只想搜索tag的直接子节点,可以使用参数 recursive=False .

一段简单的文档:

<p> The Dormouse's story</p><p> </p>

...

是否使用 recursive 参数的搜索结果:

soup.html.find_all("title")

# [

The Dormouse's story]

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

# []

像调用 find_all() 一样调用tag

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

soup.find_all("a")

soup("a")

这两行代码也是等价的:

soup.title.find_all(text=True)

soup.title(text=True)

find()

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

find_all() 方法将返回文档中符合条件的所有tag,尽管有时候我们只想得到一个结果.比如文档中只有一个

标签,那么使用 find_all() 方法来查找标签就不太合适, 使用 find_all 方法并设置 limit=1 参数不如直接使用 find() 方法.下面两行代码是等价的:

soup.find_all('title', limit=1)

# [

The Dormouse's story]

soup.find('title')

#

The Dormouse's story

唯一的区别是 find_all() 方法的返回结果是值包含一个元素的列表,而 find() 方法直接返回结果.

find_all() 方法没有找到目标是返回空列表, find() 方法找不到目标时,返回 None .

print(soup.find("nosuchtag"))

# None

soup.head.title 是 tag的名字 方法的简写.这个简写的原理就是多次调用当前tag的 find() 方法:

soup.head.title

#

The Dormouse's story

soup.find("head").find("title")

#

The Dormouse's story

find_parents() 和 find_parent()

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

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

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

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

a_string = soup.find(text="Lacie")

a_string

# u'Lacie'

a_string.find_parents("a")

# [ ]

a_string.find_parent("p")

#

Once upon a time there were three little sisters; and their names were

# ,

# and

# ;

# and they lived at the bottom of a well.

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

# []

文档中的一个

find_previous_siblings() 和 find_previous_sibling()

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

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

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

last_link = soup.find("a", id="link3")

last_link

#

last_link.find_previous_siblings("a")

# [ ,

# ]

first_story_paragraph = soup.find("p", "story")

first_story_paragraph.find_previous_sibling("p")

#

The Dormouse's story

find_all_next() 和 find_next()

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

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

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

first_link = soup.a

first_link

#

first_link.find_all_next(text=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")

#

...

第一个例子中,字符串 “Elsie”也被显示出来,尽管它被包含在我们开始查找的

#

The Dormouse's story

]

first_link.find_previous("title")

#

The Dormouse's story

find_all_previous("p") 返回了文档中的第一段(class=”title”的那段),但还返回了第二段,

标签包含了我们开始查找的

通过tag标签逐层查找:

soup.select("body a")

# [ ,

# ,

# ]

soup.select("html head title")

# [

The Dormouse's story]

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

soup.select("head > title")

# [

The Dormouse's story]

soup.select("p > a")

# [ ,

# ,

# ]

soup.select("p > a:nth-of-type(2)")

# [ ]

soup.select("p > #link1")

# [ ]

soup.select("body > a")

# []

找到兄弟节点标签:

soup.select("#link1 ~ .sister")

# [ ,

# ]

soup.select("#link1 + .sister")

# [ ]

通过CSS的类名查找:

soup.select(".sister")

# [ ,

# ,

# ]

soup.select("[class~=sister]")

# [ ,

# ,

# ]

通过tag的id查找:

soup.select("#link1")

# [ ]

soup.select("a#link2")

# [ ]

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

soup.select('a[href]')

# [ ,

# ,

# ]

通过属性的值来查找:

soup.select('a[href="http://example.com/elsie"]')

# [ ]

soup.select('a[href^="http://example.com/"]')

# [ ,

# ,

# ]

soup.select('a[href$="tillie"]')

# [ ]

soup.select('a[href*=".com/el"]')

# [ ]

通过语言设置来查找:

multilingual_markup = """

Hello

Howdy, y'all

Pip-pip, old fruit

Bonjour mes amis

"""

multilingual_soup = BeautifulSoup(multilingual_markup)

multilingual_soup.select('p[lang|=en]')

# [

Hello

,

#

Howdy, y'all

,

#

Pip-pip, old fruit

]

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

修改文档树

Beautiful Soup的强项是文档树的搜索,但同时也可以方便的修改文档树

修改tag的名称和属性

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

soup = BeautifulSoup('Extremely bold')

tag = soup.b

tag.name = "blockquote"

tag['class'] = 'verybold'

tag['id'] = 1

tag

#

Extremely bold

del tag['class']

del tag['id']

tag

#

Extremely bold

修改 .string

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

markup = ' '

soup = BeautifulSoup(markup)

tag = soup.a

tag.string = "New link text."

tag

#

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

append()

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

soup = BeautifulSoup(" ")

soup.a.append("Bar")

soup

#

soup.a.contents

# [u'Foo', u'Bar']

BeautifulSoup.new_string() 和 .new_tag()

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

soup = BeautifulSoup("")

tag = soup.b

tag.append("Hello")

new_string = soup.new_string(" there")

tag.append(new_string)

tag

# Hello there.

tag.contents

# [u'Hello', u' there']

如果想要创建一段注释,或 NavigableString 的任何子类,将子类作为 new_string() 方法的第二个参数传入:

from bs4 import Comment

new_comment = soup.new_string("Nice to see you.", Comment)

tag.append(new_comment)

tag

# Hello there

tag.contents

# [u'Hello', u' there', u'Nice to see you.']

# 这是Beautiful Soup 4.2.1 中新增的方法

创建一个tag最好的方法是调用工厂方法 BeautifulSoup.new_tag() :

soup = BeautifulSoup("")

original_tag = soup.b

new_tag = soup.new_tag("a", href="http://www.example.com")

original_tag.append(new_tag)

original_tag

#

new_tag.string = "Link text."

original_tag

#

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

insert()

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

markup = ' '

soup = BeautifulSoup(markup)

tag = soup.a

tag.insert(1, "but did not endorse ")

tag

#

tag.contents

# [u'I linked to ', u'but did not endorse', example.com

]

insert_before() 和 insert_after()

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

soup = BeautifulSoup("stop")

tag = soup.new_tag("i")

tag.string = "Don't"

soup.b.string.insert_before(tag)

soup.b

# Don'tstop

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

soup.b.i.insert_after(soup.new_string(" ever "))

soup.b

# Don't ever stop

soup.b.contents

# [Don't, u' ever ', u'stop']

clear()

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

markup = ' '

soup = BeautifulSoup(markup)

tag = soup.a

tag.clear()

tag

#

extract()

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

markup = ' '

soup = BeautifulSoup(markup)

a_tag = soup.a

i_tag = soup.i.extract()

a_tag

#

i_tag

# example.com

print(i_tag.parent)

None

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

my_string = i_tag.string.extract()

my_string

# u'example.com'

print(my_string.parent)

# None

i_tag

#

decompose()

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

markup = ' '

soup = BeautifulSoup(markup)

a_tag = soup.a

soup.i.decompose()

a_tag

#

replace_with()

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

markup = ' '

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

#

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

wrap()

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

soup = BeautifulSoup("

I wish I was bold.

")

soup.p.string.wrap(soup.new_tag("b"))

# I wish I was bold.

soup.p.wrap(soup.new_tag("div"))

#

I wish I was bold.

该方法在 Beautiful Soup 4.0.5 中添加

unwrap()

Tag.unwrap() 方法与 wrap() 方法相反.将移除tag内的所有tag标签,该方法常被用来进行标记的解包:

markup = ' '

soup = BeautifulSoup(markup)

a_tag = soup.a

a_tag.i.unwrap()

a_tag

#

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

输出

格式化输出

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

markup = ' '

soup = BeautifulSoup(markup)

soup.prettify()

# '\n

\n \n \n

#

#

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

print(soup.a.prettify())

#

# I linked to

#

# example.com

#

#

压缩输出

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

str(soup)

# '

'

unicode(soup.a)

# u' '

str() 方法返回UTF-8编码的字符串,可以指定 编码 的设置.

还可以调用 encode() 方法获得字节码或调用 decode() 方法获得Unicode.

输出格式

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

soup = BeautifulSoup("“Dammit!” he said.")

unicode(soup)

# u'

\\u201cDammit!\\u201d he said.'

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

str(soup)

# '

\\xe2\\x80\\x9cDammit!\\xe2\\x80\\x9d he said.'

get_text()

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

markup = ' '

soup = BeautifulSoup(markup)

soup.get_text()

u'\nI linked to example.com\n'

soup.i.get_text()

u'example.com'

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

# soup.get_text("|")

u'\nI linked to |example.com|\n'

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

# soup.get_text("|", strip=True)

u'I linked to|example.com'

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

[text for text

in soup.stripped_strings]

# [u'I linked to', u'example.com']

指定文档解析器

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

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

  • 要解析的文档是什么类型: 目前支持, “html”, “xml”, 和 “html5”
  • 指定使用哪种解析器: 目前支持, “lxml”, “html5lib”, 和 “html.parser”

安装解析器 章节介绍了可以使用哪种解析器,以及如何安装.

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

解析器之间的区别

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

BeautifulSoup(" ")

#

因为空标签不符合HTML标准,所以解析器把它解析成

同样的文档使用XML解析如下(解析XML需要安装lxml库).注意,空标签依然被保留,并且文档前添加了XML头,而不是被包含在标签内:

BeautifulSoup(" ", "xml")

#

#

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

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

标签被直接忽略掉了:

BeautifulSoup("

", "lxml")

#

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

BeautifulSoup("

", "html5lib")

#

html5lib库没有忽略掉

标签,而是自动补全了标签,还给文档树添加了标签.

使用pyhton内置库解析结果如下:

BeautifulSoup("

", "html.parser")

#

与lxml [7] 库类似的,Python内置库忽略掉了

标签,与html5lib库不同的是标准库没有尝试创建符合标准的文档格式或将文档片段包含在标签内,与lxml不同的是标准库甚至连标签都没有尝试去添加.

因为文档片段“

”是错误格式,所以以上解析方式都能算作”正确”,html5lib库使用的是HTML5的部分标准,所以最接近”正确”.不过所有解析器的结构都能够被认为是”正常”的.

不同的解析器可能影响代码执行结果,如果在分发给别人的代码中使用了 BeautifulSoup ,那么最好注明使用了哪种解析器,以减少不必要的麻烦.

编码

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

markup = "

Sacr\\xc3\\xa9 bleu!

"

soup = BeautifulSoup(markup)

soup.h1

#

Sacré bleu!

soup.h1.string

# u'Sacr\\xe9 bleu!'

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

soup.original_encoding

'utf-8'

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

这是我见过最牛逼,最全面的Beautiful Soup 4.2 教程!没有之一


分享到:


相關文章: