Python爬蟲:我這有美味的湯,你喝嗎
使用Beautiful Soup
在前面的文章中已經講過了正則表達式的使用方法了,但是如果正則表達式出現(xiàn)問題,那么得到的結果就不是我們想要的內容。熟悉前端的朋友肯定知道,對于一個網(wǎng)頁來說,都有一定的特殊結構和層級關系,而且很多節(jié)點都用id和class來區(qū)分。所以可以借助網(wǎng)頁的結構和屬性來提取數(shù)據(jù)。
Beautiful Soup是一個可以從HTML或XML中提取數(shù)據(jù)的Python庫。它可以通過你喜歡的轉換器快速幫你解析并查找整個HTML文檔。
★Beautiful Soup自動將輸入文檔轉為Unicode編碼,輸出文檔轉為UTF-8編碼。因此你不需要考慮編碼方式。
除非文檔沒有指定一個編碼方式,這時你只要說明一下原始的編碼方式就可以了。
”
準備工作
在開始之前,確保已經安裝好Beautiful Soup和lxml。如果沒有安裝,請參考下面的安裝教程。
pip?install?bs4
pip?install?lxml
解析器
Beautiful在解析時依賴解析器,它除了支持Python標準庫中的HTML解析器外,還支持一些第三方庫(比如lxml)。
下面簡單的介紹Beautiful Soup 支持的解析器。
| 解析器 | 使用方法 | 優(yōu)勢 | 劣勢 |
|---|---|---|---|
| Python標準庫 | BeautifulSoup(markup, 'html.parser') | python內置的標準庫,執(zhí)行速度適中 | Python3.2.2之前的版本容錯能力差 |
| lxml HTML解析器 | BeautifulSoup(markup, 'lxml') | 速度快、文檔容錯能力強 | 需要安裝C語言庫 |
| lxml XML解析器 | BeautifulSoup(markup 'xml') | 速度快,唯一支持XML的解析器 | 需要安裝C語言庫 |
| html5lib | BeautifulSoup(markup, 'html5lib') | 最好的容錯性、以瀏覽器的方式解析文檔、生成HTML5格式的文檔 | 速度慢,不依賴外部拓展 |
從上面的表格可以看出,lxml解析器可以解析HTML和XML文檔,并且速度快,容錯能力強,所有推薦使用它。
如果使用lxml,那么在初始化的BeautifulSoup時候,可以把第二個參數(shù)設為lxml即可。
具體代碼如下所示:
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup('Hello?world
',?'lxml')
print(soup.p)
基本用法
下面先用示例來看看Beautiful Soup的基本用法
html_doc?=?"""
The?Dormouse's?story
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsie,
Lacie?and
Tillie;
and?they?lived?at?the?bottom?of?a?well.
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.prettify())
print(soup.title.string)
首先對上面的代碼做簡單的說明。
眼尖的小伙伴會發(fā)現(xiàn),在聲明的 html_doc 變量中是一串HTML代碼,但是html標簽和body標簽并沒有閉合。
接著,將html_doc傳入BeautifulSoup并指定'lxml'為解析器。這樣就成功創(chuàng)建了BeautifulSoup對象,將這個對象賦值給soup。
接下來就可以調用soup的各個方法和屬性來解析這串HTML代碼了。
首先,調用prettify( )方法。這個方法可以把要解析的字符串以標準的縮進格式輸出。這里需要注意的是,輸出結果里面包含body、html節(jié)點,也就是說對于不標準的HTML字符串,BeautifulSoup可以自動更正格式。
這一步不是由prettify( )方法做成的,而是在創(chuàng)建BeautifulSoup時就完成。
然后調用soup.title.string,這實際上是輸出HTML中title節(jié)點的文本內容。
節(jié)點選擇器
選擇元素
下面再用一個例子詳細說明選擇元素的方法:
html_doc?=?"""
The?Dormouse's?story
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsie,
Lacie?and
Tillie;
and?they?lived?at?the?bottom?of?a?well.
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.title)
print(type(soup.title))
print(soup.title.string)
print(soup.head)
print(soup.p)
print(type(soup.p))
運行結果:
The?Dormouse's?story
bs4.element.Tag'>
The?Dormouse's?story
The?Dormouse's?story
The?Dormouse's?story
'bs4.element.Tag'>
下面就對上面的代碼做簡要的描述,這里依然選用之前的HTML代碼,首先打印輸出title節(jié)點的選擇結果,你會發(fā)現(xiàn)選擇結果是Tag類型,該類型有很多方法和屬性,比如string屬性,輸出title節(jié)點的文本內容。
其他代碼都是選擇節(jié)點,并打印節(jié)點及其內部的所有內容。
最后要注意的是當有多個節(jié)點時,這種選擇方式只會匹配到第一個節(jié)點,例如:p節(jié)點。
提取節(jié)點信息
從上面的代碼我們知道可以使用string屬性獲取文本的內容。但是有些時候我需要獲取節(jié)點屬性的值,或者節(jié)點名。
(1)獲取名稱
可以利用name屬性獲取節(jié)點的名稱。
具體代碼如下所示:
soup?=?BeautifulSoup('Extremely?bold')
tag?=?soup.b
print(tag.name)
通過運行上面的代碼,你會發(fā)現(xiàn)成功獲取到了b節(jié)點的名稱。
(2)獲取屬性
每個節(jié)點可能有多個屬性,比如id和class等,選擇這個節(jié)點元素之后,可以調用attrs獲取所有的屬性。
具體代碼示例如下所示:
html_doc?=?"""
The?Dormouse's?story
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsie,
Lacie?and
Tillie;
and?they?lived?at?the?bottom?of?a?well.
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.p.attrs)
print(soup.p.attrs['name'])
運行結果
{'class':?['title'],?'name':?'Dormouse'}
Dormouse
從上面的運行結果你會發(fā)現(xiàn)屬性值返回的是字典類型。
class屬性使用列表保存,這是為什么呢?
原因是:class這個屬性可以有多個值,所以將其保存在列表中
(4)獲取內容
可以利用string屬性獲取節(jié)點元素包含的文本內容,比如要獲取第一個p節(jié)點的文本。
print(soup.p.string)
獲取子節(jié)點
獲取子節(jié)點也可以理解為嵌套選擇,我們知道在一個節(jié)點中可能包含其他的節(jié)點,BeautifulSoup提供了許多操作和遍歷子節(jié)點的屬性。
比如我們可以獲取HTML中的head元素還可以繼續(xù)獲得head元素內部的節(jié)點元素。
html_doc?=?"""
The?Dormouse's?story
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsie,
Lacie?and
Tillie;
and?they?lived?at?the?bottom?of?a?well.
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.head.title)
print(soup.head.title.string)
關聯(lián)選擇
在做選擇的時候,有時候不能做到一步就獲取到我想要的節(jié)點元素,需要選取某一個節(jié)點元素,然后以這個節(jié)點為基準再選取它的子節(jié)點、父節(jié)點、兄弟節(jié)點等。
(1)選取子節(jié)點和子孫節(jié)點
選取節(jié)點元素之后,想要獲取它的直接子節(jié)點可以調用contents屬性。
具體代碼示例如下:
html_doc?=?"""
The?Dormouse's?story
The?Dormouse's?story
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.p.contents)
html_doc?=?"""
The?Dormouse's?story
The?Dormouse's?story
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.p.contents)
相信眼尖的小伙伴看上面兩段代碼的很容易就看出區(qū)別了吧。
第一段代碼的p節(jié)點沒有換行,但是第二段代碼的p節(jié)點是存在換行符的。所以當你嘗試運行上面代碼的時候會發(fā)現(xiàn),直接子節(jié)點保存在列表中,并且第二段代碼存在換行符。
相同的功能還可以通過調用children屬性來獲取。
html_doc?=?"""
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsie,
Lacie?and
Tillie;
and?they?lived?at?the?bottom?of?a?well.
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.p.children)
print(list(soup.p.children))
for?i?in?soup.p.children:
????print(i)
上面的代碼通過調用children屬性來獲取選擇結果,返回的類型是生成器類型,可以轉為list類型或者是for循環(huán)將其輸出。
如果想要獲取子孫的節(jié)點的話,可以調用descendants屬性來獲取輸出內容。
具體代碼示例如下所示:
html_doc?=?"""
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
ElsieElsie,
Lacie?and
Tillie;
and?they?lived?at?the?bottom?of?a?well.
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.p.descendants)
for?child?in?soup.p.descendants:
????print(child)
此時返回的結果依然還是生成器類型,遍歷輸出之后,你會發(fā)現(xiàn)可以單獨輸出人名,若子節(jié)點內還有子節(jié)點也會單獨輸出。
(2)父節(jié)點和祖先節(jié)點
如果想要獲取某個節(jié)點的父節(jié)點可以直接調用parent屬性。
具體代碼示例如下所示:
html_doc?=?"""
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsie
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.a.parent)
試著運行上面的代碼,你會發(fā)現(xiàn),獲取的父節(jié)點是第一個a節(jié)點的直接父節(jié)點。而且也不會去訪問祖先節(jié)點。
如果想要獲取所有的祖先節(jié)點可以調用parents屬性。
具體代碼示例如下所示:
html_doc?=?"""
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsie
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.a.parents)
for?i,?parent?in?enumerate(soup.a.parents):
????print(i,?parent)
獲取祖先節(jié)點,依然返回的類型仍然是生成器類型。所以通過循環(huán)可以遍歷出每一個內容。
試著運行上面的代碼,你會發(fā)現(xiàn),輸出結果包含了body節(jié)點和html節(jié)點。
(3) 兄弟節(jié)點
上面的兩個了例子說明了父節(jié)點與子節(jié)點的獲取方法。那假如我需要獲取同級節(jié)點該怎么辦呢?可以使用next_sibling、previous_sibling、next_siblings、previous_siblings這四個屬性來獲取。
具體代碼示例如下:
html_doc?=?"""
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsiehello
Lacie?and
Tillie;
and?they?lived?at?the?bottom?of?a?well.
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.a.next_sibling)
print(list(soup.a.next_siblings))
print(soup.a.previous_sibling)
print(list(soup.a.previous_siblings))
從上面的代碼可以發(fā)現(xiàn),這里調用了4個屬性,分別是next_sibling和previous_sibling,這個兩個屬性分別獲取節(jié)點的上一個兄弟元素和下一個兄弟元素。
而next_siblings和previous_siblings是獲取前面和后面的兄弟節(jié)點,返回的類型依然是生成器類型。
方法選擇器
前面所講的內容都是通過屬性來選擇的,這種方法非???,但是如果是較為復雜的選擇,那上面的選擇方法就可能顯得繁瑣。因此,Beautiful Soup為我們提供了查詢方法,比如:find_all()和find()等。調用它們,傳入相應的參數(shù)。
find_all()
它的API如下:
find_all(name, attrs, recursive, text, **kwargs)
(1)name
可以根據(jù)節(jié)點名稱來選擇參數(shù)
具體代碼示例如下:
html_doc?=?"""
The?Dormouse's?story
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsie,
Lacie?and
Tillie;
and?they?lived?at?the?bottom?of?a?well.
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.find_all('a'))
print(len(soup.find_all('a')))
上面的代碼調用了find_all( )方法,傳入了name參數(shù),參數(shù)值為a,
試著運行上面的代碼,我們想要獲取的所有a節(jié)點,返回結果是列表類型,長度為3。
html_doc?=?"""
The?Dormouse's?story
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsie,
Lacie?and
Tillie;
and?they?lived?at?the?bottom?of?a?well.
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.find_all('a'))
for?a?in?soup.find_all('a'):
????print(a.find_all('span'))
????print(a.string)
將上面的代碼做些許修改。
試著運行上面的代碼,你會發(fā)現(xiàn)可以通過a節(jié)點去獲取span節(jié)點,同樣的也可以獲取a節(jié)點的文本內容。
(2)attrs
除了根據(jù)節(jié)點名查詢的話,同樣的也可以通過屬性來查詢。
具體代碼示例如下所示:
html_doc?=?"""
The?Dormouse's?story
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsie,
Lacie?and
Tillie;
and?they?lived?at?the?bottom?of?a?well.
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.find_all(attrs={'id':?'link1'}))
print(soup.find_all(attrs={'name':?'Dormouse'}))
這里查詢的時候要傳入的參數(shù)是attrs參數(shù),參數(shù)的類型是字典類型。
對于常用的屬性比如class,我們可以直接傳入class這個參數(shù),還是上面的文本,具體代碼示例如下:
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.find_all(class_?=?'sister'))
在這里需要注意的是class是Python的保留字,所以在class的后面加上下劃線。
同樣的,其實id屬性也可以這樣操作,還是上面的文本,具體代碼示例如下:
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.find_all(id?=?'link2'))
find( )
除了find_all( )方法,還有find( )方法,前者返回的是多個元素,以列表形式返回,后綴是返回一個元素。
具體代碼示例如下:
html_doc?=?"""
The?Dormouse's?story
The?Dormouse's?story
Once?upon?a?time?there?were?three?little?sisters;?and?their?names?were
Elsie,
Lacie?and
Tillie;
and?they?lived?at?the?bottom?of?a?well.
...
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.find(name='a'))
print(type(soup.find(name='a')))
試著運行上面的代碼,你會發(fā)現(xiàn),find ( )方法返回第一個a節(jié)點的元素,類型是Tag類型。
find( )與find_all( )的使用方法相同。
還有其他方法選擇器,在這里做一下簡單的介紹。
find_parents() 和find_parent():前者返回所有祖先節(jié)點,后者返回直接父節(jié)點。
find_next_siblings()和find_next_sibling():前者返回后面的所有兄弟節(jié)點,后者返回后面第一個兄弟節(jié)點。
find_previous_siblings和find_previous_sibling():前者返回前面的所有兄弟節(jié)點,后者返回前面第一個兄弟節(jié)點。
CSS選擇器
Beautiful Soup還為我們提供了另一種選擇器,就是CSS選擇器。熟悉前端開發(fā)的小伙伴來說,CSS選擇器肯定也不陌生。
使用CSS選擇器的時候,需要調用select( ) 方法,將屬性值或者是節(jié)點名稱傳入選擇器即可。
具體代碼示例如下:
html_doc?=?"""
????
????????Hello?World
???
????
????
????
????????
???????????- Foo
???????????- Bar
???????????- Jay
????????
????????
????????
???????????- Foo
???????????- Bar
???????????- Jay
????????
????
????
"""
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
print(soup.select('.panel?.panel-heading'))?#?獲取class為panel-heading的節(jié)點
print(soup.select('ul?li'))?#?獲取ul下的li節(jié)點
print(soup.select('#list-2?li'))?#?獲取id為list-2下的li節(jié)點
print(soup.select('ul'))?#?獲取所有的ul節(jié)點
print(type(soup.select('ul')[0]))
試著運行上面的代碼,查看運行結果之后,很多內容你就明白了。
最后一句輸出列表中元素的類型,你會發(fā)現(xiàn)依然還是Tag類型。
嵌套選擇
select( )方法同樣支持嵌套選擇,例如,會選擇所有的ul節(jié)點,在對ul節(jié)點進行遍歷,選擇li節(jié)點。
與上面的html文本相同,具體代碼如下所示:
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
for?ul?in?soup.select('ul'):
????print(ul.select('li'))
試著運行上面的結果,輸出所有ul節(jié)點下的所有l(wèi)i節(jié)點組成的列表。
獲取屬性
從上面的幾個例子中相信大家應該明白了,所有的節(jié)點類型都是Tag類型,所以獲取屬性依然可以使用以前的方法,仍然是上面的HTML文本,這里嘗試獲取每個ul節(jié)點下的id屬性。
具體代碼示例如下所示:
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
for?ul?in?soup.select('ul'):
????print(ul['id'])
????print(ul.attrs['id'])
從上面的代碼可以看出,可以直接向中括號傳入屬性名,或者通過attrs屬性獲取屬性值。
獲取文本
要獲取文本除了之前所說的string屬性,另外,還可以調用get_text()方法。
依然還是前面的html文本具體代碼示例如下所示:
from?bs4?import?BeautifulSoup
soup?=?BeautifulSoup(html_doc,?'lxml')
for?li?in?soup.select('li'):
????print('String:',?li.string)
????print('get?text:',?li.get_text())
小結
Beautiful Soup到這里基本上就結束了。
在編寫爬蟲的時候一般使用find_all( )和find( )方法獲取指定節(jié)點。
如果對css選擇器熟悉的話也可以使用select( )方法。
實戰(zhàn)
前言
如果你看到了這里,那么恭喜你完成了很多人不能做到的堅持,因為很少人能夠看完上面雜而多的知識。
這次的實戰(zhàn)內容,我?guī)淼氖桥廊站視頻彈幕。
為什么是這個實戰(zhàn)內容呢?很簡單就是為了迎合我們剛剛學完的Beautiful Soup。
準備工作
工欲善其事,必先利其器,寫爬蟲也是同樣的道理。
首先,安裝好兩個必要的庫:requests, bs4
pip?install?requests
pip?install?bs4
關于B站彈幕限制
以前B站的彈幕很快可以通過抓包獲取到,但是現(xiàn)在B站有了限制,就獲取不到了,不過不用擔心,我拿到以前的API接口依然是可以獲取到B站彈幕的。
爬取內容
在2020年的最后一天,郭敬明和于正在早期由于抄襲分別向莊羽和瓊瑤道歉。當時看了一下還上了微博的熱搜。
所以,我今天就默默的打開B站,看看UP主們是怎么樣分析這次道歉的以及觀眾對這次分析發(fā)表的言論,所有就來爬取視頻的彈幕。
視頻鏈接如下:
https://www.bilibili.com/video/BV1XK411M7ka?from=search&seid=17596321343783034307
需求分析
獲取彈幕API接口

通過抓包,我們需要的獲取內容就是oid信息。
我拿了以前的API接口,進行獲取彈幕,現(xiàn)在我也將這個接口分享給大家。
https://api.bilibili.com/x/v1/dm/list.so?oid=276746872
每一個視頻的彈幕都可以通過修改oid的值去獲取。
將上面的鏈接輸入到瀏覽器就會可以看到彈幕信息了。

爬取彈幕
既然我們在上面所講的內容是Beautiful Soup,那肯定是通過Beautiful Soup進行數(shù)據(jù)解析,文本內容保存下來。獲取彈幕的寫法肯定會有很多種,我在下面就先列出一種。
功能實現(xiàn)
同樣的,我們需要對上面的鏈接發(fā)起請求。再通過Beautiful Soup獲取文本內容,保存至txt文檔。
具體代碼
import?requests
from?bs4?import?BeautifulSoup
class?DanMu(object):
????def?__init__(self):
????????self.headers?=?{
????????????'user-agent':?'Mozilla/5.0?(Windows?NT?10.0;?Win64;?x64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/87.0.4280.66?Safari/537.36'
????????}
????????self.url?=?'https://api.bilibili.com/x/v1/dm/list.so?oid=276746872'
????#?獲取網(wǎng)頁信息
????def?get_html(self):
????????response?=?requests.get(self.url,?headers=self.headers)
????????html?=?response.content.decode('utf-8')
????????return?html
????#?保存彈幕
????def?get_info(self):
????????html?=?self.get_html()
????????soup?=?BeautifulSoup(html,?'lxml')
????????file?=?open('彈幕.txt',?'w',?encoding='utf-8')
????????for?d?in?soup.find_all(name='d'):
????????????danmu?=?d.string
????????????file.write(danmu)
????????????file.write('\n')
if?__name__?==?'__main__':
????danmu?=?DanMu()
????danmu.get_info()

通過上面的代碼只獲取到了3000條彈幕,但是彈幕有6000多條,這也算是一種反爬措施。當我寫到反爬的時候,會給大家做分析。
最后
本次分享到這里就結束了,如果你讀到了這里,那么說明本篇文章對你還是有所幫助的,這也是我分享知識的初衷。
沒有什么是可以一蹴而就的,生活如此,學習亦是如此!
路漫漫其修遠兮, 吾將上下而求索。
我是啃書君,一個專注于學習的人。你懂的越多,你不懂的越多,更多精彩內容我們下期再見!
為了大家更快速的學習知識,掌握技術,隨時溝通交流問題,特組建了技術交流群,大家在群里可以分享自己的技術棧,拋出日常問題,群里會有很多大佬及時解答的,這樣我們就會結識很多志同道合的人,長按下圖可加我微信,備注:Python即可進群。
??????????????
?掃碼加群??
