剖析树
到目前为止,我们只是载入文档,然后再输出它。 现在看看更让我们感兴趣的剖析树: Beautiful Soup剖析一个文档后生成的数据结构。
剖析对象 (BeautifulSoup或 BeautifulStoneSoup的实例)是深层嵌套(deeply-nested), 精心构思的(well-connected)的数据结构,可以与XML和HTML结构相互协调。 剖析对象包括2个其他类型的对象,Tag对象, 用于操纵像<TITLE> ,<B>这样的标签;NavigableString对象, 用于操纵字符串,如"Page title"和"This is paragraph"。
NavigableString的一些子类 (CData, Comment, Declaration, and ProcessingInstruction), 也处理特殊XML结构。 它们就像NavigableString一样, 除了但他们被输出时, 他们会被添加一些额外的数据。下面是一个包含有注释(comment)的文档:
from BeautifulSoup import BeautifulSoup
import re
hello = "Hello! <!--I've got to be nice to get what I want.-->"
commentSoup = BeautifulSoup(hello)
comment = commentSoup.find(text=re.compile("nice"))
comment.__class__
# <class 'BeautifulSoup.Comment'>
comment
# u"I've got to be nice to get what I want."
comment.previousSibling
# u'Hello! '
str(comment)
# "<!--I've got to be nice to get what I want.-->"
print commentSoup
# Hello! <!--I've got to be nice to get what I want.-->
现在,我们深入研究一下我们开头使用的那个文档:
from BeautifulSoup import BeautifulSoup
doc = ['<html><head><title>Page title</title></head>',
'<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
'<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
'</html>']
soup = BeautifulSoup(''.join(doc))
print soup.prettify()
# <html>
# <head>
# <title>
# Page title
# </title>
# </head>
# <body>
# <p id="firstpara" align="center">
# This is paragraph
# <b>
# one
# </b>
# .
# </p>
# <p id="secondpara" align="blah">
# This is paragraph
# <b>
# two
# </b>
# .
# </p>
# </body>
# </html>
Tag的属性
Tag和NavigableString对象有很多有用的成员,在 Navigating剖析树和 Searching剖析树中我们会更详细的介绍。 现在,我们先看看这里使用的Tag成员:属性
SGML标签有属性:.例如,在上面那个HTML 中每个<P>标签都有"id"属性和"align"属性。 你可以将Tag看成字典来访问标签的属性:
firstPTag, secondPTag = soup.findAll('p')
firstPTag['id']
# u'firstPara'
secondPTag['id']
# u'secondPara'
NavigableString对象没有属性;只有Tag 对象有属性。
Navigating剖析树
Tag 对象都有如下含有所有的成员的列表(尽管,某些实际的成员值可能为None). NavigableString对象也有下面这些成员,除了contents和 string成员。
parent
上面那个 例子中, <HEAD> Tag的parent是<HTML> Tag. <HTML> Tag 的parent是BeautifulSoup 剖析对象自己。 剖析对象的parent是None. 利用parent,你可以向前遍历剖析树。
soup.head.parent.name
# u'html'
soup.head.parent.parent.__class__.__name__
# 'BeautifulSoup'
soup.parent == None
# True
contents
使用parent向前遍历树。使用contents向后遍历树。 contents是Tag的有序列表, NavigableString 对象包含在一个页面元素内。只有最高层的剖析对象和 Tag 对象有contents。 NavigableString 只有strings,不能包含子元素,因此他们也没有contents.
在上面的例子中, contents 的第一个<P> Tag是个列表,包含一个 NavigableString ("This is paragraph "), 一个<B> Tag, 和其它的 NavigableString (".")。而contents 的<B> Tag: 包含一个NavigableString ("one")的列表。
pTag = soup.p
pTag.contents
# [u'This is paragraph ', <b>one</b>, u'.']
pTag.contents[1].contents
# [u'one']
pTag.contents[0].contents
# AttributeError: 'NavigableString' object has no attribute 'contents'
string
为了方便,如果一个标签只有一个子节点且是字符串类型,这个自己可以这样访问 tag.string,等同于tag.contents[0]的形式。 在上面的例子中, soup.b.string是个NavigableString对象,它的值是Unicode字符串"one". 这是剖析树中<B>Tag 的第一个string。
soup.b.string
# u'one'
soup.b.contents[0]
# u'one'
但是soup.p.string是None, 剖析中的第一个<P> Tag 拥有多个子元素。soup.head.string也为None, 虽然<HEAD> Tag只有一个子节点,但是这个子节点是Tag类型 (<TITLE> Tag), 不是NavigableString。
soup.p.string == None
# True
soup.head.string == None
# True
nextSibling和previousSibling
使用它们你可以跳往在剖析树中同等层次的下一个元素。 在上面的文档中, <HEAD> Tag的nextSibling 是<BODY> Tag, 因为<BODY> Tag是在<html> Tag的下一层。 <BODY>标签的nextSibling为None, 因为<HTML>下一层没有标签是直接的在它之后。
soup.head.nextSibling.name
# u'body'
soup.html.nextSibling == None
# True
相应的<BODY> Tag的previousSibling是<HEAD>标签, <HEAD> Tag的previousSibling为None:
soup.body.previousSibling.name
# u'head'
soup.head.previousSibling == None
# True
更多例子:<P> Tag的第一个nextSibling是第二个 <P> Tag。 第二个<P>Tag里的<B>Tag的previousSibling是 NavigableString"This is paragraph"。 这个NavigableString的previousSibling是None, 不会是第一个<P> Tag里面的任何元素。
soup.p.nextSibling
# <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>
secondBTag = soup.findAlll('b')[1]
secondBTag.previousSibling
# u'This is paragraph'
secondBTag.previousSibling.previousSibling == None
# True
next和previous
使用它们可以按照soup处理文档的次序遍历整个文档,而不是它们在剖析树中看到那种次序。 例如<HEAD> Tag的next是<TITLE>Tag, 而不是<BODY> Tag。 这是因为在原始文档中,<TITLE> tag 直接在<HEAD>标签之后。
soup.head.next
# u'title'
soup.head.nextSibling.name
# u'body'
soup.head.previous.name
# u'html'
Where next and previous are concerned, a Tag's contents come before its nextSibling. 通常不会用到这些成员,但有时使用它们能够非常方便地从剖析树获得不易找到的信息。
你可以像遍历list一样遍历一个标签(Tag)的contents 。 这非常有用。类似的,一个Tag的有多少child可以直接使用len(tag)而不必使用len(tag.contents)来获得。 以上面那个文档中的为例:
for i in soup.body:
print i
# <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>
# <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>
len(soup.body)
# 2
len(soup.body.contents)
# 2
使用标签(tag)名作为成员
像剖析对象或Tag对象的成员一样使用Tag名可以很方便的操作剖析树。 前面一些例子我们已经用到了这种方式。以上述文档为例, soup.head获得文档第一个<HEAD>标签:
soup.head
# <head><title>Page title</title></head>
通常,调用mytag.foo获得的是mytag的第一个child,同时必须是一个<FOO> 标签。 如果在mytag中没有<FOO> 标签,mytag.foo返回一个None。 你可以使用这中方法快速的读取剖析树:
soup.head.title
# <title>Page title</title>
soup.body.p.b.string
# u'one'
你也可以使用这种方法快速的跳到剖析树的某个特定位置。例如,如果你担心<TITLE> tags会离奇的在<HEAD> tag之外, 你可以使用soup.title去获得一个HTML文档的标题(title),而不必使用soup.head.title:
soup.title.string
# u'Page title'
soup.p跳到文档中的第一个 <P> tag,不论它在哪里。 soup.table.tr.td 跳到文档总第一个table的第一列第一行。
这些成员实际上是下面first 方法的别名,这里更多介绍。 这里提到是因为别名使得一个定位(zoom)一个结构良好剖析树变得异常容易。
获得第一个<FOO> 标签另一种方式是使用.fooTag 而不是 .foo。 例如,soup.table.tr.td可以表示为soup.tableTag.trTag.tdTag,甚至为soup.tableTag.tr.tdTag。 如果你喜欢更明确的知道表示的意义,或者你在剖析一个标签与Beautiful Soup的方法或成员有冲突的XML文档是,使用这种方式非常有用。
from BeautifulSoup import BeautifulStoneSoup
xml = '<person name="Bob"><parent rel="mother" name="Alice">'
xmlSoup = BeautifulStoneSoup(xml)
xmlSoup.person.parent # A Beautiful Soup member
# <person name="Bob"><parent rel="mother" name="Alice"></parent></person>
xmlSoup.person.parentTag # A tag name
# <parent rel="mother" name="Alice"></parent>
如果你要找的标签名不是有效的Python标识符,(例如
hyphenated-name),你就需要使用
first方法了。