上一篇分享了正則表達式的使用,相信大家對正則也已經有了一定的了解。它可以針對任意字符串做任何的匹配并提取所需信息。
但是我們爬蟲基本上解析的都是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的搜索文檔樹