BS4“元素”到底是什么?如何计算元素,由哪个解析器决定?显然很困惑

时间:2019-05-07 16:30:15

标签: beautifulsoup html-parsing lxml html5lib

我现在对我以为我理解的东西感到困惑,但事实证明我一直认为是理所当然的。

一个人经常遇到这种for循环:

from bs4 import BeautifulSoup as bs
mystring = 'some string'
soup = bs(mystring,'html.parser')
for elem in soup.find_all():
    [do something with elem]

直到我遇到此简化字符串的版本时,elem才真正引起我的注意:

mystring = 'opening text<p>text one<BR> text two.<br></p>\
<p align="right">text three<br/> text four.</p><p class="myclass">text five. </p>\
<p>text six <span style="some style">text seven</span></p>\
<p>text 8. <span style="some other style">text nine</span></p>closing text'

我现在不确定我期望的输出是什么,但是当我运行这段代码时:

counter = 1 #using 'normal' counting for simplification
for elem in soup.find_all():
    print('elem ',counter,elem)
    counter +=1

输出为:

elem  1 <p>text one<br/> text two.<br/></p>
elem  2 <br/>
elem  3 <br/>
elem  4 <p align="right">text three<br> text four.</br></p>
elem  5 <br> text four.</br>
elem  6 <p class="myclass">text five. </p>
elem  7 <p>text six <span style="some style">text seven</span></p>
elem  8 <span style="some style">text seven</span>
elem  9 <p>text 8. <span style="some other style">text nine</span></p>
elem  10 <span style="some other style">text nine</span>

因此bs4 + html.parser在字符串中找到10个元素。他们的选择和演示对我来说似乎并不直观(例如,跳过opening textclosing text)。不仅如此,print(len(soup))的输出竟然是7

为了确保,我将html.parserlxml都换成了html5lib。在这两种情况下,print(len(soup))不仅是1,而且elem的数量跃升到13个!而且,自然地,额外的元素是不同的。从第四个elem到最后,两个库都与html.parser相同。但是,对于前三个...

通过html5lib,您将获得:

elem  1 <html><head></head><body>opening text<p>text one<br/> text two.<br/></p><p align="right">text three<br/> text four.</p><p class="myclass">text five. </p><p>text six <span style="some style">text seven</span></p><p>text 8. <span style="some other style">text nine</span></p>closing text</body></html>
elem  2 <head></head>
elem  3 <body>opening text<p>text one<br/> text two.<br/></p><p align="right">text three<br/> text four.</p><p class="myclass">text five. </p><p>text six <span style="some style">text seven</span></p><p>text 8. <span style="some other style">text nine</span></p>closing text</body>

另一方面,使用lxml,您将得到:

elem  1 <html><body><p>opening text</p><p>text one<br/> text two.<br/></p><p align="right">text three<br/> text four.</p><p class="myclass">text five. </p><p>text six <span style="some style">text seven</span></p><p>text 8. <span style="some other style">text nine</span></p>closing text</body></html>
elem  2 <body><p>opening text</p><p>text one<br/> text two.<br/></p><p align="right">text three<br/> text four.</p><p class="myclass">text five. </p><p>text six <span style="some style">text seven</span></p><p>text 8. <span style="some other style">text nine</span></p>closing text</body>
elem  3 <p>opening text</p>

那么这一切背后的哲学是什么?这是谁的“过失”?是否有“正确”或“错误”的答案?而且,实际上,我应该虔诚地跟随一个解析器,还是每个解析器都有时间和地点?

对问题的长度表示歉意。

1 个答案:

答案 0 :(得分:2)

首先,根对象(在您的情况下为soup变量)是BeautifulSoup对象。您可以将其视为浏览器中的document对象。在BeautifulSoup中,BeautifulSoup对象是从Element对象派生的,但它本身并不是真正的“元素”,它更像是文档。

在元素(或BeautifulSoup对象)上调用len时,将获得该对象的contents成员中的节点数。其中可以包含注释,文档处理语句,文本节点,元素节点等。

格式正确的文档应该具有一个根元素,但是注释和文档处理语句也可以在根级别使用。在您的情况下,没有注释,也没有处理语句,通常我希望长度为1。

lxmlhtml5lib试图确保您有一个格式正确的文档,如果看到您有多个根元素,他们会将其包装在htmlbody中标签,并给您一个根元素。但是,如前所述,如果文档已经具有适当的根html元素,并且在根级别也具有注释或处理语句,则长度可能大于1。根据解析器的不同,当提供奇怪的格式错误的HTML时,他们可能会操纵其他内容,以遵守他们也强制执行的任何规则。

另一方面。 html.parser非常宽容。它不会尝试纠正您在做什么,而只是按原样解析事物。在您的情况下,它将返回一个奇怪的文档,该文档在根级别具有多个文本节点,在根级别具有多个<p>元素。因此,当您在soup上调用length时,您得到的值远大于1。

通常。 BeautifulSoup返回的初始元素是BeautifulSoup对象。它可能包含Element个节点或NaviagableString个节点(文本),如果它们是注释,文档整理,CDATA或其他处理语句,则它们可以是各种子类型。 NaviagableStrings(及相关子类型)不是Element节点,而是通常包含在ElementBeautifulSoup对象的内容中。

取决于您是否喜欢宽大处理,速度,HTML5正确性,XML支持等,它可能会影响您希望使用哪个解析器。此外,有时您可能希望针对非常特定的用例使用其他解析器。