Python爬虫之Beautiful Soup


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


0.安装

​pip install beautifulsoup4​

1.解析器

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语言库

html5lib

​BeautifulSoup(markup, "html5lib")​

最好的容错性以浏览器的方式解析文档生成HTML5格式的文档

速度慢不依赖外部扩展

2.使用

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

from bs4 import BeautifulSoup

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

soup = BeautifulSoup("<html>data</html>")

3.对象种类

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

Name

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

tag.name
# u'b'

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

tag.name = "blockquote"
tag
# <blockquote class="boldest">Extremely bold</blockquote>

Attributes

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

tag['class']
# u'boldest'

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

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

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

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

.contents 和 .children(直接子结点)

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

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​​ 对象的子节点:

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的直接子节点.例如,标签只有一个直接子节点

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

但是

标签也包含一个子节点:字符串 “The Dormouse’s story”,这种情况下字符串 “The Dormouse’s story”也属于标签的子孙节点. <code>.descendants</code> 属性可以对所有tag的子孙节点进行递归循环 [<a href="https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/#id92">5]</a> :

for child in head_tag.descendants:
print(child)
# <title>The Dormouse's story</title>
# The Dormouse's story

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

len(list(soup.children))
# 1
len(list(soup.descendants))
# 25

4.搜索文档树

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

5.过滤器

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

字符串

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

soup.find_all('b')
# [<b>The Dormouse's story</b>]

如果传入字节码参数,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"])
# [<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>]

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)
# [<p class="title"><b>The Dormouse's story</b></p>,
# <p class="story">Once upon a time there were...</p>,
# <p class="story">...</p>]

返回结果中只有

标签没有标签,因为标签还定义了”id”,没有返回和,因为和中没有定义”class”属性.

通过一个方法来过滤一类标签属性的时候, 这个方法的参数是要被过滤的属性的值, 而不是这个标签. 下面的例子是找出 ​​href​​​ 属性不符合指定正则的 ​​a​​ 标签.

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>]

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

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

现在来了解一下搜索方法的细节

keyword 参数

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

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

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

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

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

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

soup.find_all(id=True)
# [<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的多个属性:

soup.find_all(href=re.compile("elsie"), id='link1')
# [<a class="sister" href="http://example.com/elsie" id="link1">three</a>]

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

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:

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