上一篇分享了正则表达式的使用,相信大家对正则也已经有了一定的了解。它可以针对任意字符串做任何的匹配并提取所需信息。
但是我们爬虫基本上解析的都是html或者xml结构的内容,而非任意字符串。正则表达式虽然很强大灵活,但是对于html这样结构复杂的来说,写pattern的工作量会大大增加,并且有任意一处出错都得不到匹配结果,比较麻烦。
本篇将介绍一款针对html和xml结构,操作简单并容易上手的解析利器—BeautifulSoup。
BeautifulSoup的介绍
第一次使用BeautifulSoup的时候就在想:这个名字有什么含义吗?美味的汤?于是好信也在网上查了一下。
来看,官方文档是这么解释的:
“
BeautifulSoup: We called him Tortoise because he taught us”
意思是我们叫他乌龟因为他教了我们,当然这里Tortoise是Taught us的谐音。BeautifulSoup这个词来自于《爱丽丝漫游仙境》,意思是“甲鱼汤”。上面那个官方配图也是来自于《爱丽丝漫游仙境》,看来是没跑了,估计是作者可能很喜欢这部小说吧,因而由此起了这个名字。
好,让我们看看真正的BeautifulSoup是什么?
BeautifulSoup是Python语言中的模块,专门用于解析html/xml,非常适合像爬虫这样的项目。它有如下几个使其强大的特点:
它提供了几个超级简单的方法和Pythonic的语句来实现强大的导航、搜索、修改解析树的功能。
它会自动把将要处理的文档转化为Unicode编码,并输出为utf-8的编码,不需要你再考虑编码的问题。
支持Python标准库中的HTML解析器,还支持第三方的模块,如 lxml解析器 。
BeautifulSoup的安装
目前BeautifulSoup的最新发型版本是BeautifulSoup4,在Python中以bs4模块引入。
博主使用的Python3.x,可以使用 pip3 install bs4 来进行安装,也可以通过官方网站下载来安装,链接:https://www.crummy.com/software/BeautifulSoup/,具体安装步骤不在此叙述了。
以为安装完了吗?还没有呢。
上面介绍BeautifulSoup的特点时说到了,BeautifulSoup支持Python标准库的解析器html5lib,纯Python实现的。除此之外,BeautifulSoup还支持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")
lxml HTML解析器BeautifulSoup(markup, "lxml")
lxml HTML解析器BeautifulSoup(markup, ["lxml", "xml"])
BeautifulSoup(markup, "xml")
html5libBeautifulSoup(markup, "html5lib")
推荐使用lxml作为解析器,lxml是用C语言库来实现的,因此效率更高。在Python2.7.3之前的版本和Python3中3.2.2之前的版本,必须安装lxml或html5lib, 因为那些Python版本的标准库中内置的HTML解析方法不够稳定。
BeautifulSoup的文档对象创建
首先引入bs4库,也就是BeautifulSoup在Python中的模块。
from bs4 import BeautifulSoup
好了,我们来看一下官方提供的例子,这段例子引自《爱丽丝漫游记》。
html_doc ="""
The Dormouse's storyThe Dormouse's story
Once upon a time there were three little sisters; and their names were
Elsie,
Lacie
andTillie;
and they lived at the bottom of a well.
...
"""
假设以上html_doc就是我们已经下载的网页,我们需要从中解析并获取感兴趣的内容。
首先的首先,我们需要创建一个BeautifulSoup的文档对象,依据不同需要可以传入“字符串”或者“一个文件句柄”。
传入“字符串”
soup = BeautifulSoup(html_doc)
传入“文件句柄”,打开一个本地文件
soup = BeautifulSoup(open("index.html"))
文档首先被转换为Unicode,如果是解析html文档,直接创建对象就可以了(像上面操作那样),这时候BeautifulSoup会选择一个最合适的解析器对文档进行解析。
但同时,BeautifulSoup也支持手动选择解析器,根据指定解析器进行解析(也就是我们安装上面html5lib和lxml的原因)。
手动指定解析器如下:
soup = BeautifulSoup(html_doc, "lxml")
如果仅是想要解析HTML文档,只要用文档创建 BeautifulSoup 对象就可以了。Beautiful Soup会自动选择一个解析器来解析文档。但是还可以通过参数指定使用那种解析器来解析当前文档。
BeautifulSoup 第一个参数应该是要被解析的文档字符串或是文件句柄,第二个参数用来标识怎样解析文档。如果第二个参数为空,那么Beautiful Soup根据当前系统安装的库自动选择解析器,解析器的优先数序: lxml, html5lib, Python标准库。在下面两种条件下解析器优先顺序会变化:
要解析的文档是什么类型: 目前支持, “html”, “xml”, 和 “html5”
指定使用哪种解析器: 目前支持, “lxml”, “html5lib”, 和 “html.parser”
BeautifulSoup的对象种类
Beautiful Soup将复杂HTML文档转换成一个复杂的树形结构,每个节点都是Python对象,所有对象可以归纳为4种:Tag
NavigableString
BeautifulSoup
Comment
Tag就是html或者xml中的标签,BeautifulSoup会通过一定的方法自动寻找你想要的指定标签。查找标签这部分会在后面“遍历查找树”和“搜索查找树”中介绍,这里仅介绍对象。
soup = BeautifulSoup('Extremely bold')
tag=soup.b
type(tag)
>>>
Tag标签下也有对象,有两个重要的属性对象:name和attributes。
Name
Name就是标签tag的名字,以下简单操作便可获取。
tag.name
>>> u'b'
Attributes
我们都知道一个标签下可能有很多属性,比如上面那个标签b有class属性,属性值为boldest,那么我们如何获取这个属性值呢?
其实标签的属性操作和Python中的字典操作一样的,如下:
tag['class']
>>> u'boldest'
也可以通过“点”来获取,比如:
tag.attrs
>>> {u'class': u'boldest'}
NavigableString是可遍历字符串的意思,其实就是标签内包括的字符串,在爬虫里也是我们主要爬取的对象之一。
在BeautifulSoup中可以非常简单的获取标签内这个字符串。
tag.string
>>> u'Extremely bold'
就这么简单的完成了信息的提取,简单吧。要说明一点,tag中包含的字符串是不能编辑的,但是可以替换。
tag.string.replace_with("No longer bold")
tag
>>>
No longer bold
BeautifulSoup对象表示的是一个文档的全部内容。大部分时候,可以把它当作Tag对象。
soup.name
>>> u'[document]'
BeautifulSoup对象不是一个真正的tag,没有name和attributes,但是却可以查看它的name属性。如上所示,“[document]”为BeautifulSoup文档对象的特殊属性名字。
还有一些对象也是我们需要特殊注意的,就是注释。其实comment对象是一个特殊类型的NavigableString对象,请看下面。
markup = ""
soup = BeautifulSoup(markup)
comment = soup.b.string
type(comment)
>>>
comment
>>> u'Hey, buddy. Want to buy a used parser'
这和NavigableString的使用是一样,同样使用 .string 对标签内字符串进行提取。但是,请看上面comment这个例子,里面字符串是一个comment,有这样的格式,一样使用了 .string对其进行提取,得到的结果是去掉了comment标志的里面的字符串。这样的话,当我们并不知道它是否是comment,如果得到以上的结果很有可能不知道它是个comment。
因此,这可能会让我们得到我们不想要的comment,扰乱我们的解析结果。
为了避免这种问题的发生,可以在使用之前首先通过以下代码进行一个简单的判断,然后再进行其它操作。
iftype(soup.b.string)==bs4.element.Comment:
print(soup.b.string)
BeautifulSoup的遍历文档树
仍然用最开始的《爱丽丝》中的一段话作为例子。
子节点
子节点有 .contents 和 .children 两种用法。
contents
content属性可以将标签所有子节点以列表形式返回。
#
The Dormouse's storyprint(soup.head.contents)
>>> [title>The Dormouse's story]
这样就可以返回一个子节点标签了。当然你也可以通过soup.title来实现,但是当文档结构复杂的时候,比如有不止一个title的话,那这样就不如contents使用来的快了。
head下只有一个标签title,那么如果我们查看一下body下的子标签。
print(soup.body.contents)
>>>
['\n',
The Dormouse's story
, '\n',Once upon a time there were three little sisters; and their names were
Elsie,
Lacie
andTillie;
and they lived at the bottom of a well.
, '\n',...
, '\n']你会发现这些子节点列表中有很多“\n”,这是因为它把空格包括进去了,所以这里需要注意一下。
children
也可以通过 .chidren 得到相同的结果,只不过返回的children是一个生成器(generator),而不是一个列表。
print(soup.body.children)
>>>
看到这是一个生成器,因此我们可以for..in..进行遍历,当然也可以得到以上同样的结果。
forchildinsoup.body.children: print(child)
子孙节点
子孙节点使用.descendants属性。如果子节点可以直接获取标签的直接子节点,那么子孙节点则可以获取所有子孙节点,注意说的是所有,也就是说孙子的孙子都得给我找出来,下用面开一个例子。
for child in head_tag.descendants: print(child)
>>>
The Dormouse's story>>> The Dormouse's stor
title是head的子节点,而title中的字符串是title的子节点,title和title所包含的字符串都是head的子孙节点,因此被循环递归的查找出来。.descendants 的用法和 .children 是一样的,会返回一个生成器,需要for..in..进行遍历。
父节点
父节点使用 .parents 属性实现,可以得到父辈的标签。
title_tag = soup.title
title_tag
>>>
The Dormouse's storytitle_tag.parent
>>>
The Dormouse's storytitle_tag.parent.name
>>> head
获得全部父节点则使用.parents属性实现,可以循环得到所有的父辈的节点。
link = soup.a
for parent in link.parents: if parent is None: print(parent) else: print(parent.name)
>>>
p
body
html
[document]
None
可以看到a节点的所有父辈标签都被遍历了,包括BeautifulSoup对象本身的[document]。
兄弟节点
兄弟节点使用.next_sibling和.previous_sibling属性。
兄弟嘛,不难理解自然就是同等地位的节点了,其中next_sibling 获取下一个兄弟节点,而previous_sibling 获取前一个兄弟节点。
a_tag = soup.find("a", id="link1")
a_tag.next_sibling
>>> ,
a_tag.previous_element
>>>
Once upon a time there were three little sisters; and their names were
兄弟节点可以通过 .next_siblings 和 .previous.sibling 获取所有前后兄弟节点,同样需要遍历获取每个元素。
回退和前进
当然还有一些其它用法,如回退和前进.next_element和.previous_element,它是针对所有节点的回退和前进,不分辈分。
a_tag = soup.find("a", id="link1")
a_tag
>>>
Elsie,
a_tag.next_element
>>> Elsie
a_tag.previous_element
>>>
Once upon a time there were three little sisters; and their names were
因为使用了回退,将会寻找下一个节点对象而不分辈分,那么这个标签的下一个节点就是它的子节点Elsie,而上一个节点就是上一个标签的字符串对象。find用法会在后续搜索文档树里面详细介绍。
回退和前进也可以寻找所有的前后节点,使用.next_elements和.previous_elements。
for elem in last_a_tag.next_elements:
if elem.nameisNone:continue
print(elem.name)
>>>
a
a
p
返回对象同样是生成器,需要遍历获得元素。其中使用了if判断去掉了不需要的None。
节点内容
前面提到过NavigableString对象的 .string 用法,这里在文档遍历再次体会一下其用法。
如果tag只有一个NavigableString 类型子节点,那么这个tag可以使用.string得到子节点,就像之前提到的一样。而如果一个tag里面仅有一个子节点(比如tag里tag的字符串节点),那么这个tag也可以使用.string方法,输出结果与当前唯一子节点的.string结果相同(如上所示)。
title_tag.string
>>> u'The Dormouse's story'
head_tag.contents
>>> [
The Dormouse's story]head_tag.string
>>> u'The Dormouse's story'
但是如果这个tag里面有多个节点,那就不灵了。因为tag无法确定该调用哪个节点,如下面这种。
print(soup.html.string)
>>> None
如果tag中包含多个字符串,可以使用 .strings 来循环获取,输出的字符串中可能包含了很多空格或空行,使用.stripped_strings可以去除多余空白内容。
上面提介绍的都是如何遍历各个节点,下面我们看看如何搜索我们我们真正想获取的内容,如标签属性等。
BeautifulSoup的搜索文档树
搜索文档树有很多种用法,但使用方法都基本一致。这里只选择介绍一种.find_all。
find_all()
find_all(name, attrs , recursive , text , **kwargs)
find_all() 方法可以搜索当前标签下的子节点,并会经过过滤条件判断是否符合标准,先随便看个例子。
soup.find_all("a")
>>>
[Elsie,
Lacie,
Tillie]
soup.find_all(id="link2")
>>>
[Lacie]
通过以上例子,可以发现,我们只要设定好我们的过滤条件,便可轻松的解析我们想要的内容。这些条件如何设定呢?
就是通过find_all()的这些参数来设置的,让我们来看看。
Name参数
name参数就是标签的名字,如上面的例子寻找所有标签,name参数可以是字符串、True、正则表达式、列表、甚至具体方法。
下面举个正则表达式的例子。
importre
soup =BeautifulSoup(html_doc, 'lxml')fortag insoup.find_all(re.compile("^t")):print(tag.name)
>>> title
可以看到正则表达式的意思是匹配任何以“t”开头的标签名称,就只有title一个。
使用“True”会匹配任何值,使用“列表”会匹配列表中所有的标签项,如果没有合适的过滤条件,还可以自定义一个“方法”。
Keyword参数
就如同Python中的关键字参数一样,我们可以搜索指定的标签属性来定位标签。
soup.find_all(id='link2')
>>>
[Lacie]
找到了id属性为link2的标签。
soup.find_all(href=re.compile("elsie"))
>>>
[Elsie]
找到了href属性里含有“elsie”字样的标签。
也可以同时定义多个关键字条件来过滤匹配结果。
soup.find_all(href=re.compile("elsie"), id='link1')
>>>
[three]
text参数
通过text参数可以搜索匹配的字符串内容,与name的用法相似,也可以使用字符串、True、正则表达式、列表、或者具体方法。
soup.find_all(text="Elsie")>>> [u'Elsie']
soup.find_all(text=re.compile("Dormouse")) >>>
[u"The Dormouse's story", u"The Dormouse's story"]
limit参数
limit参数可以限制返回匹配结果的数量,看下面这个例子。
soup.find_all("a", limit=2)
>>>
[Elsie,
Lacie]
文档中本来有三个标签,但是通过限制只得到了两个。
recursive参数
find_all()会寻找符合匹配条件的所有子孙节点,如果我们只想找直接的子节点,就可以设置recursive参数来进行限制,recursive=False。
soup.html.find_all("title")
>>> [
The Dormouse's story]soup.html.find_all("title", recursive=False)
>>> [ ]
上面是两种使用recursive和没有使用recursive的情况,可以发现它的作用。
以上就是find_all()所有参数的介绍,其它方法如find(),find_parents()等更多方法与find_all()基本一致,可以举一反三。
总结
以上就是BeautifulSoup的使用方法介绍,主要记住三个部分内容:
BeautifulSoup对象种类
BeautifulSoup的遍历文档树
BeautifulSoup的搜索文档树