Python 爬蟲之網(wǎng)頁(yè)解析庫(kù) BeautifulSoup

image

BeautifulSoup 是一個(gè)使用靈活方便、執(zhí)行速度快、支持多種解析器的網(wǎng)頁(yè)解析庫(kù),可以讓你無(wú)需編寫正則表達(dá)式也能從 html 和 xml 中提取數(shù)據(jù)。BeautifulSoup 不僅支持 Python 內(nèi)置的 Html 解析器,還支持 lxml、html5lib 等第三方解析器。

以下是對(duì)幾個(gè)主要解析器的對(duì)比:

解析器 使用方法 優(yōu)勢(shì) 劣勢(shì)
Python 標(biāo)準(zhǔn)庫(kù) BeautifulSoup(markup, "html.parser") Python的內(nèi)置標(biāo)準(zhǔn)庫(kù)
執(zhí)行速度適中
文檔容錯(cuò)能力強(qiáng)
Python 2.7.3 or 3.2.2)前的版本中文檔容錯(cuò)能力差
lxml HTML 解析器 BeautifulSoup(markup, "lxml") 速度快
文檔容錯(cuò)能力強(qiáng)
需要安裝C語(yǔ)言庫(kù)
lxml XML 解析器 BeautifulSoup(markup, ["lxml", "xml"])
BeautifulSoup(markup, "xml")
速度快
唯一支持XML的解析器
需要安裝C語(yǔ)言庫(kù)
html5lib BeautifulSoup(markup, "html5lib") 最好的容錯(cuò)性
以瀏覽器的方式解析文檔
生成HTML5格式的文檔
速度慢
不依賴外部擴(kuò)展

安裝

BeautifulSoup 安裝

我們可以通過 pip 來安裝 BeautifulSoup4。

pip install BeautifulSoup4

PyPi 中還有一個(gè)名字是 BeautifulSoup,它是 BeautifulSoup3 的發(fā)布版本,目前已停止維護(hù),不建議使用該版本。

解析器安裝

雖然 BeautifulSoup 支持多種解釋器,但是綜合來考慮的話還是推薦使用 lxml 解釋器,因?yàn)?lxml 解釋器的效率更高且支持所有的 python 版本,我們可以通過 pip 來安裝 lxml 解釋器。

pip install lxml

使用

BeautifulSoup 將 HTML 文檔轉(zhuǎn)化為一個(gè)樹形結(jié)構(gòu),樹形結(jié)構(gòu)的每個(gè)節(jié)點(diǎn)都是一個(gè) python 對(duì)象,節(jié)點(diǎn)的類型可以分為 Tag、NavigableString、BeautifulSoup 和 Comment 四類。

將 html 文本傳入 BeautifulSoup 的構(gòu)造方法即可得到一個(gè)文檔對(duì)象,通過該對(duì)象下每一個(gè)節(jié)點(diǎn)的數(shù)據(jù)。

from bs4 import BeautifulSoup

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

節(jié)點(diǎn)的訪問

Tag

HTML 中的標(biāo)簽在 BeautifulSoup 中我們稱之為 Tag,在 Tag 眾多屬性中最常用也最重要的屬性即 name 和 attribute。

name 顧名思義他是 Tag 的名稱,比如 <p class='title'></p> 這段 HTML 中 Tag 的 name 即為 p。

attribute 是 tag 的屬性,比如 <p class='title'></p> 這段 HTML 中 Tag 的 class 屬性的值即為 title。

attribute 的操作方法與字典相同,我們可以正常對(duì) tag 的屬性進(jìn)行刪除、修改等操作。

以下代碼展示了 name 和 attribute 的使用方法。

# -*- coding:utf-8 -*-
from bs4 import BeautifulSoup

html = """
<html>
    <a class="sister"  id="link2"></a>
    <p class="story"></p>
</html>
"""

soup = BeautifulSoup(html,features="lxml")
tag1 = soup.a
tag2 = soup.p

print (type(tag1))
print (type(tag2))

print (tag1.name)
print (tag2.name)

print (tag1.attrs)
print (tag1['class'])
tag1['class'] = "brothers"
print (tag1.attrs)

print (tag2.attrs)
print (tag2['class'])

在運(yùn)行以上代碼之前,請(qǐng)先確認(rèn)安裝了 BeautifulSoup 和 lxml 庫(kù)。以上代碼在 python 3.7.0 版本測(cè)試,若要在 python 2.7 版本使用請(qǐng)修改 print 部分。

NavigableString

我們可以通過 name 和 attrs 來獲取標(biāo)簽的屬性等內(nèi)容,但是在很多情況下我們想要獲取的是標(biāo)簽所包含的內(nèi)容,此時(shí)我們就需要使用 string 屬性。先來看下以下代碼

# -*- coding:utf-8 -*-
from bs4 import BeautifulSoup

html = """<p name="dromouse"><b>The Dormouse's story</b></p>"""

soup = BeautifulSoup(html, features='lxml')

print (soup.p.name)
print (soup.p.string)
print (soup.b.string)

以上代碼執(zhí)行結(jié)果如下

p
The Dormouse's story
The Dormouse's story

在這個(gè)示例中僅僅通過一行代碼 ==soup.p.string== 就獲取了標(biāo)簽所包含的字符串,在 Python 爬蟲第一篇(urllib+regex) 中使用的正則表達(dá)式來獲取標(biāo)簽所包含的內(nèi)容,有興趣的話可以去看一下。

標(biāo)簽中所包含的字符串無(wú)法進(jìn)行編輯,但是可以使用 replace_with 方法進(jìn)行替換。

BeautifulSoup

BeautifulSoup 對(duì)象表示的是一個(gè)文檔的全部?jī)?nèi)容.大部分時(shí)候,可以把它當(dāng)作 Tag 對(duì)象,是一個(gè)特殊的 Tag,我們可以分別獲取它的類型,名稱等屬性。

Comment

Comment 是一個(gè)特殊的 NavigableString。在 html 文件中不可避免的會(huì)出現(xiàn)大量的注釋部分,由于使用 string 屬性會(huì)將注釋部分作為正常內(nèi)容輸出,而我們往往不需要注釋部分的內(nèi)容,此時(shí)就引入了 Comment 對(duì)象,BeautifulSoup 將 html 文檔中的注釋部分自動(dòng)設(shè)置為 Comment 對(duì)象,在使用過程中通過判斷 string 的類型是否為 Comment 就可以過濾注釋部分的內(nèi)容。

# -*- coding:utf-8 -*-
from bs4 import BeautifulSoup, element

html = """
<p name="dromouse">The Dormouse's story</p>
<b><!--Hey, buddy. Want to buy a used parser?--></b>
"""

soup = BeautifulSoup(html, features='lxml')

print (soup.p.string)
print (soup.b.string)

print (type(soup.p.string))
print (type(soup.b.string))

if type(soup.p.string) != element.Comment:
    print (soup.p.string)
if type(soup.b.string) != element.Comment:
    print (soup.b.string)

以上代碼執(zhí)行結(jié)果如下

The Dormouse's story
Hey, buddy. Want to buy a used parser?
<class 'bs4.element.NavigableString'>
<class 'bs4.element.Comment'>
The Dormouse's story

文檔的遍歷

BeautifulSoup 提供了子孫節(jié)點(diǎn)、內(nèi)容屬性「.string 屬性」、父節(jié)點(diǎn)、兄弟節(jié)點(diǎn)、前后節(jié)點(diǎn)等多種方式來遍歷整個(gè)文檔。

子孫節(jié)點(diǎn)

BeautifulSoup 提供了 contents、children 和 descendants 三種屬性來操作子孫節(jié)點(diǎn)。通過 contents 和 children 可以獲取一個(gè) Tag 的直接節(jié)點(diǎn),contents 返回的是一個(gè) list,children 返回的是一個(gè) list 的生成器,可以通過遍歷來獲取所有內(nèi)容。descendants 將獲取一個(gè) Tag 的說有子節(jié)點(diǎn),以及子節(jié)點(diǎn)的子節(jié)點(diǎn)「孫節(jié)點(diǎn)」。它也是一個(gè)生成器,需要通過遍歷來獲取內(nèi)容。

# -*- coding:utf-8 -*-

from bs4 import BeautifulSoup

html = """
<html><head><title>The Dormouse's story</title></head>
 <body>
  <p class="title"><b>The Dormouse's story</b></p>
  <p class="story">Once upon a time there were three little sisters; and their names were
   <a class="sister"  id="link1">Elsie</a>,
   <a class="sister"  id="link2">Lacie</a>and
   <a class="sister"  id="link2">Tillie</a>;
   and they lived at the bottom of a well.
  </p>
 </body>
</html>
"""

soup = BeautifulSoup(html, features='lxml')

print ('---------- contents ----------')
print (soup.contents)
print ('---------- children ----------')
for index, value in enumerate(soup.children):
    print ("%4d : %s" %(index, value))
print ('---------- descendants ----------')
for index, value in enumerate(soup.descendants):
    print ("%4d : %s" %(index, value))

以上代碼演示了 contents、children 和 descendants 屬性的使用,執(zhí)行結(jié)果這里就不再貼出來了,有興趣的或可以自己運(yùn)行一下獲取結(jié)果并驗(yàn)證它。

ps: 以上代碼均在 python 3.7.0 測(cè)試通過。

內(nèi)容屬性

BeautifulSoup 提供了 string、strings 和 stripped_strings 三個(gè)屬性來獲取 Tag 的內(nèi)容。如果一個(gè) Tag 僅有一個(gè)子節(jié)點(diǎn)有內(nèi)容「NavigableString 類型子節(jié)點(diǎn)」或其只有一個(gè)子節(jié)點(diǎn)可以使用 string 屬性來獲取節(jié)點(diǎn)內(nèi)容。若 Tag 包含多個(gè)子節(jié)點(diǎn),且不止一個(gè)子節(jié)點(diǎn)含有內(nèi)容,此時(shí)需要用到 strings 和 stripped_strings 屬性,使用 strings 獲取的內(nèi)容會(huì)包含很多的空格和換行,使用 stripped_strings 可以過濾這些空格和換行。

# -*- coding:utf-8 -*-

from bs4 import BeautifulSoup

html = """
<html><head><title>The Dormouse's story</title></head>
 <body>
  <p class="title"><b>The Dormouse's story</b></p>
  <p class="story">Once upon a time there were three little sisters; and their names were
   <a class="sister"  id="link1">Elsie</a>,
   <a class="sister"  id="link2">Lacie</a>and
   <a class="sister"  id="link2">Tillie</a>;
   and they lived at the bottom of a well.
  </p>
 </body>
</html>
"""

soup = BeautifulSoup(html, features='lxml')

print ('---------- single navigablestring string attributes ----------')
print (soup.title.string)
print (soup.head.string)
print ('---------- multiple navigablestring string attributes ----------')
print (soup.body.string)
print ('---------- multiple navigablestring strings attributes ----------')
for string in soup.body.strings:
    print (string)
print ('---------- multiple navigablestring stripped_strings attributes ----------')
for string in soup.body.stripped_strings:
    print (string)

以上代碼展示了 string、strings 和 stripped_strings 屬性的應(yīng)用,需要注意的是當(dāng) Tag 不止一個(gè)子節(jié)點(diǎn)含有內(nèi)容時(shí),使用 strings 屬性將返回 None。strings 和 stripped_strings 返回的是生成器,需要通過迭代獲取內(nèi)容。

父節(jié)點(diǎn)

BeautifulSoup 通過 parent 和 parents 來獲取 Tag 的父節(jié)點(diǎn)。使用 parent 得到的是 Tag 的直接父節(jié)點(diǎn),而 parents 將得到 Tag 的所有父節(jié)點(diǎn),包括
父節(jié)點(diǎn)的父節(jié)點(diǎn)。

# -*- coding:utf-8 -*-

from bs4 import BeautifulSoup

html = """
<html><head><title>The Dormouse's story</title></head>
 <body>
  <p class="title"><b>The Dormouse's story</b></p>
  <p class="story">Once upon a time there were three little sisters; and their names were
   <a class="sister"  id="link1">Elsie</a>,
   <a class="sister"  id="link2">Lacie</a>and
   <a class="sister"  id="link2">Tillie</a>;
   and they lived at the bottom of a well.
  </p>
 </body>
</html>
"""

soup = BeautifulSoup(html, features='lxml')

print ('---------- parent attributes ----------')
print (soup.title.parent.name)
print ('---------- parents attributes ----------')
for parent in soup.title.parents:
    print(parent.name)

以上代碼的執(zhí)行結(jié)果如下:

---------- parent attributes ----------
head
---------- parents attributes ----------
head
html
[document]

兄弟節(jié)點(diǎn)

兄弟節(jié)點(diǎn)即和當(dāng)前節(jié)點(diǎn)處在同一級(jí)上的節(jié)點(diǎn),BeautifulSoup 通過 next_sibling、previous_sibling、next_siblings 和 previous_siblings 四個(gè)屬性類獲取兄弟節(jié)點(diǎn),next_sibling 和 previous_sibling 屬性用來獲取上一個(gè)兄弟節(jié)點(diǎn)和下一個(gè)兄弟節(jié)點(diǎn),若節(jié)點(diǎn)不存在則返回 None。next_siblings 和 previous_siblings 屬性用于對(duì)當(dāng)前節(jié)點(diǎn)的兄弟節(jié)點(diǎn)機(jī)型迭代,通過這兩個(gè)屬性可以獲取當(dāng)前節(jié)點(diǎn)的所有兄弟節(jié)點(diǎn)。

.next_sibling 和 .previous_sibling 屬性通常是字符串或空白,因?yàn)榭瞻谆蛘邠Q行也可以被視作一個(gè)節(jié)點(diǎn),所以得到的結(jié)果可能是空白或者換行。

# -*- coding: utf-8 -*-

from bs4 import BeautifulSoup

html = """
<html><head><title>The Dormouse's story</title></head>
 <body>
  <p class="title"><b>The Dormouse's story</b></p>
  <p class="story">Once upon a time there were three little sisters; and their names were
   <a class="sister"  id="link1">Elsie</a>,
   <a class="sister"  id="link2">Lacie</a>and
   <a class="sister"  id="link2">Tillie</a>;
   and they lived at the bottom of a well.
  </p>
 </body>
</html>
"""

soup = BeautifulSoup(html, features='lxml')

print ('---------- next_sibling and previous_sibling ----------')
print (repr(soup.p.next_sibling))
print (repr(soup.p.previous_sibling))
print ('---------- next_siblings and previous_siblings ----------')
for sibling in soup.a.next_siblings:
    print (repr(sibling))
if soup.a.previous_siblings is not None:
    for sibling in soup.a.previous_siblings:
        print (repr(sibling))
else:
    print ('None')

運(yùn)行結(jié)果如下

---------- next_sibling and previous_sibling ----------
'\n'
'\n'
---------- next_siblings and previous_siblings ----------
',\n   '
<a class="sister"  id="link2">Lacie</a>
'and\n   '
<a class="sister"  id="link2">Tillie</a>
';\n   and they lived at the bottom of a well.\n  '
'Once upon a time there were three little sisters; and their names were\n   '

前后節(jié)點(diǎn)

共有 next_element、previous_element、next_elements 和 next_elements 四個(gè)屬性來操作前后節(jié)點(diǎn),和兄弟節(jié)點(diǎn)不同的是并不是針對(duì)同一層級(jí)的節(jié)點(diǎn),而是所有節(jié)點(diǎn)不分層級(jí)。使用方法和兄弟節(jié)點(diǎn)類似,這里不再單獨(dú)舉例說明了。

內(nèi)容的搜索

BeautifulSoup 提供一下方法用于文檔內(nèi)容的搜索:

  1. find 和 find_all:搜索當(dāng)前 Tag 及其所有子節(jié)點(diǎn),判斷其是否符合過濾條件。
  2. find_parent 和 find_parents:用來搜索當(dāng)前 Tag 的父節(jié)點(diǎn),判斷其是否符合過濾條件。
  3. find_next_siblings 和 find_next_sibling:用來搜索當(dāng)前 Tag 前面的兄弟節(jié)點(diǎn),判斷其是否符合過濾條件。
  4. find_previous_siblings 和 find_previous_sibling:用來搜索當(dāng)前 Tag 后面的兄弟節(jié)點(diǎn),判斷其是否符合過濾條件。
  5. find_all_next 和 find_next:通過 next_elements 屬性對(duì)當(dāng)前 Tag 的之后的節(jié)點(diǎn)和字符串進(jìn)行迭代,并判斷其是否符合過濾條件。
  6. find_all_previous 和 find_previous:通過previous_elements 屬性對(duì)當(dāng)前節(jié)點(diǎn)前面的節(jié)點(diǎn)和字符串進(jìn)行迭代,并判斷其是否符合過濾條件。

以上方法的參數(shù)及用法均相同,原理類似,這里以 find_all 方法為例進(jìn)行介紹,其他方法不再一一舉例說明。find_all 方法的定義如下:

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

name參數(shù)
name 參數(shù)用于查找所有名字為 name 的 Tag,它會(huì)自動(dòng)忽略掉字符串對(duì)象。name 參數(shù)不僅僅可以傳入字符串,也可以傳入正則表達(dá)式、列表、True「當(dāng)需要匹配任何值時(shí)可以出入 True」、或者方法。

當(dāng) name 參數(shù)傳入方法時(shí),此方法僅接受一個(gè)參數(shù)「HTML 文檔中的一個(gè)節(jié)點(diǎn)」,當(dāng)該方法返回 True 時(shí)表示當(dāng)前元素被找到,反之則返回 False。

以下代碼簡(jiǎn)單介紹 name 參數(shù)的使用

# -*- coding:utf-8 -*-
from bs4 import BeautifulSoup
import re

html = """
<html><head><title>The Dormouse's story</title></head>
 <body>
  <p class="title"><b>The Dormouse's story</b></p>
  <p class="story">Once upon a time there were three little sisters; and their names were
   <a class="sister"  id="link1">Elsie</a>,
   <a class="sister"  id="link2">Lacie</a>and
   <a class="sister"  id="link2">Tillie</a>;
   and they lived at the bottom of a well.
  </p>
 </body>
</html>
"""

soup = BeautifulSoup(html, features='lxml')

print ('---------- string ----------')
print (soup.find_all('title'))
print ('---------- regex ----------')
print (soup.find_all(re.compile('^b')))
print ('---------- list ----------')
print (soup.find_all(['b', 'a']))
print ('---------- True ----------')
print (soup.find_all(True))
print ('---------- function ----------')
def has_class_but_no_href(tag):
    return tag.has_attr('class') and not tag.has_attr('href')
print (soup.find_all(has_class_but_no_href))

keyword 參數(shù)

如果一個(gè)指定名字的參數(shù)不是搜索內(nèi)置的參數(shù)名,搜索時(shí)會(huì)把該參數(shù)當(dāng)作指定名字tag的屬性來搜索,如果包含一個(gè)名字為 id 的參數(shù),Beautiful Soup會(huì)搜索每個(gè)tag的”id”屬性.

我們可以使用 keyword 參數(shù)來搜索指定名字的屬性,可使用的參數(shù)值包括字符串、正則表達(dá)式、列表和 True。

# -*- coding:utf-8 -*-
from bs4 import BeautifulSoup
import re

html = """
<html><head><title>The Dormouse's story</title></head>
 <body>
  <p class="title"><b>The Dormouse's story</b></p>
  <p class="story">Once upon a time there were three little sisters; and their names were
   <a class="sister"  id="link1">Elsie</a>,
   <a class="sister"  id="link2">Lacie</a>and
   <a class="sister"  id="link2">Tillie</a>;
   and they lived at the bottom of a well.
  </p>
 </body>
</html>
"""

soup = BeautifulSoup(html, features='lxml')

print ('------------------------------')
print (soup.find_all(id='link1'))
print ('------------------------------')
print (soup.find_all(href=re.compile('tillie')))
print ('------------------------------')
print (soup.find_all(id=True))
print ('------------------------------')
print (soup.find_all(id='link2', href=re.compile('tillie')))

運(yùn)行結(jié)果如下

------------------------------
[<a class="sister"  id="link1">Elsie</a>]
------------------------------
[<a class="sister"  id="link2">Tillie</a>]
------------------------------
[<a class="sister"  id="link1">Elsie</a>, <a class="sister"  id="link2">Lacie</a>, <a class="sister"  id="link2">Tillie</a>]
------------------------------
[<a class="sister"  id="link2">Tillie</a>]

部分屬性在搜索不能使用,比如 HTML5 中的 data-* 屬性,此時(shí)可以通過 find_all 方法的 attrs 參數(shù)定義一個(gè)字典參數(shù)來搜索包含特殊屬性的 Tag。

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

CSS 選擇器

我們?cè)趯?CSS 時(shí),標(biāo)簽名不加任何修飾,類名前加點(diǎn),id名前加 #,在這里我們也可以利用類似的方法來篩選元素,用到的方法是 soup.select(),返回類型是 list,BeautifulSoup 支持了大部分的 CSS 選擇器。

# 通過標(biāo)簽名查找
print (soup.select('title')) 
# 通過類名查找
print (soup.select('.sister'))
# 通過 id 名查找
print (soup.select('#link1'))
# 組合查找
print (soup.select('p #link1'))
# 屬性查找
print (soup.select('a[class="sister"]'))

內(nèi)容的修改

通過 BeautifulSoup 我們可以對(duì) html 文檔內(nèi)容進(jìn)行插入、刪除、修改等等操作。

Tag 的名稱和屬性的修改

修改 Tag 的名稱直接對(duì) name 屬性重新賦值即可,修改屬性的使用字典的方式進(jìn)行重新賦值。

# 修改 Tag 的名稱
tag.name = block
# 修改 Tag 的 class 屬性值
tag['class'] = 'verybold'

Tag 內(nèi)容的修改

對(duì) Tag 內(nèi)容進(jìn)行修改可以直接對(duì) string 屬性進(jìn)行賦值「此時(shí)會(huì)覆蓋掉原有的內(nèi)容」,若要在當(dāng)前內(nèi)容后追加內(nèi)容可以使用 append 方法,若需要在指定位置增加內(nèi)容可以使 insert 方法。

新增節(jié)點(diǎn)

在 html 文檔中新增節(jié)點(diǎn)使用 new_tag 方法,

new_tag = soup.new_tag("a", )
tag.append(new_tag)

其他

若要清除 Tag 內(nèi)容可以使用 clear 方法。使用 extract 方法 和 decompose 方法可以將當(dāng)前節(jié)點(diǎn)從 html 文檔中移除。replace_with 方法用來移除內(nèi)容并使用新的節(jié)點(diǎn)替換被移除的內(nèi)容。wrap 方法 和 unwrap 方法是一對(duì)相反的方法,wrap 對(duì)指定的節(jié)點(diǎn)進(jìn)行包裝,而 unwrap 對(duì)指定的節(jié)點(diǎn)進(jìn)行解包。

BeautifulSoup 的功能較多,在本文中對(duì)大部分常用內(nèi)容進(jìn)行了解釋并提供了示例,不過這仍然不算完全,希望可以幫到大家一點(diǎn)。BeautifulSoup 是一個(gè)非常優(yōu)秀的網(wǎng)頁(yè)解析庫(kù),使用 BeautifulSoup 可以大大節(jié)省編程的效率。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容