c爬蟲(chóng)抓取網(wǎng)頁(yè)數據(本節書(shū)摘來(lái)自異步社區《用Python寫(xiě)網(wǎng)絡(luò )爬蟲(chóng)》第2章)
優(yōu)采云 發(fā)布時(shí)間: 2022-04-19 15:08c爬蟲(chóng)抓取網(wǎng)頁(yè)數據(本節書(shū)摘來(lái)自異步社區《用Python寫(xiě)網(wǎng)絡(luò )爬蟲(chóng)》第2章)
本節節選自異步社區作者【澳大利亞】理查德·勞森(Richard Lawson)所著(zhù)的《用Python編寫(xiě)Web爬蟲(chóng)》一書(shū)第2章第2.2節,李斌翻譯,更多章節可訪(fǎng)問(wèn)云棲社區“異步社區”公眾號查看。
2.2 三種網(wǎng)頁(yè)抓取方法
現在我們已經(jīng)了解了這個(gè)網(wǎng)頁(yè)的結構,有三種方法可以從中獲取數據。首先是正則表達式,然后是流行的 BeautifulSoup 模塊,最后是強大的 lxml 模塊。
2.2.1 正則表達式
如果您不熟悉正則表達式,或者需要一些提示,請查看完整介紹。
當我們使用正則表達式抓取區域數據時(shí),首先需要嘗試匹配
里面的內容
元素,如下圖。
>>> import re
>>> url = 'http://example.webscraping.com/view/United
-Kingdom-239'
>>> html = download(url)
>>> re.findall('(.*?)', html)
['/places/static/images/flags/gb.png',
'244,820 square kilometres',
'62,348,447',
'GB',
'United Kingdom',
'London',
'EU',
'.uk',
'GBP',
'Pound',
'44',
'@# #@@|@## #@@|@@# #@@|@@## #@@|@#@ #@@|@@#@ #@@|GIR0AA',
'^(([A-Z]\d{2}[A-Z]{2})|([A-Z]\d{3}[A-Z]{2})|([A-Z]{2}\d{2}
[A-Z]{2})|([A-Z]{2}\d{3}[A-Z]{2})|([A-Z]\d[A-Z]\d[A-Z]{2})
|([A-Z]{2}\d[A-Z]\d[A-Z]{2})|(GIR0AA))$',
'en-GB,cy-GB,gd',
'IE ']
從上面的結果可以看出,標簽被用于幾個(gè)國家屬性。為了隔離area屬性,我們可以只選擇其中的第二個(gè)元素,如下圖。
>>> re.findall('(.*?)', html)[1]
'244,820 square kilometres'
雖然此方案目前可用,但如果頁(yè)面更改,它可能會(huì )失敗。例如,該表已更改為刪除第二行中的土地面積數據。如果我們現在只抓取數據,我們可以忽略這種未來(lái)可能發(fā)生的變化。但是,如果我們以后想再次獲取這些數據,我們需要一個(gè)更健壯的解決方案,盡可能避免這種布局更改的影響。為了使這個(gè)正則表達式更健壯,我們可以將它作為父對象
>>> re.findall('Area: (.*?)', html)
['244,820 square kilometres']
這個(gè)迭代版本看起來(lái)更好,但是網(wǎng)頁(yè)更新還有很多其他方式也可以使這個(gè)正則表達式不滿(mǎn)足。例如,要將雙引號改為單引號,
在之間添加額外的空格
標簽,或更改 area_label 等。下面是嘗試支持這些可能性的改進(jìn)版本。
>>> re.findall('.*?>> from bs4 import BeautifulSoup
>>> broken_html = 'AreaPopulation'
>>> # parse the HTML
>>> soup = BeautifulSoup(broken_html, 'html.parser')
>>> fixed_html = soup.prettify()
>>> print fixed_html
Area
Population
從上面的執行結果可以看出,Beautiful Soup 能夠正確解析缺失的引號并關(guān)閉標簽,除了添加和
標記使它成為一個(gè)完整的 HTML 文檔?,F在我們可以使用 find() 和 find_all() 方法來(lái)定位我們需要的元素。
>>> ul = soup.find('ul', attrs={'class':'country'})
>>> ul.find('li') # returns just the first match
Area
>>> ul.find_all('li') # returns all matches
[Area, Population]
要了解所有的方法和參數,可以參考 BeautifulSoup 的官方文檔:.
以下是使用該方法提取樣本國家地區數據的完整代碼。
>>> from bs4 import BeautifulSoup
>>> url = 'http://example.webscraping.com/places/view/
United-Kingdom-239'
>>> html = download(url)
>>> soup = BeautifulSoup(html)
>>> # locate the area row
>>> tr = soup.find(attrs={'id':'places_area__row'})
>>> td = tr.find(attrs={'class':'w2p_fw'}) # locate the area tag
>>> area = td.text # extract the text from this tag
>>> print area
244,820 square kilometres
此代碼雖然比正則表達式代碼更復雜,但更易于構建和理解。此外,布局中的小變化,例如額外的空白和選項卡屬性,我們不必再擔心了。
2.2.3 Lxml
Lxml 是基于 XML 解析庫 libxml2 的 Python 包裝器。這個(gè)模塊是用C語(yǔ)言編寫(xiě)的,解析速度比Beautiful Soup快,但是安裝過(guò)程比較復雜。最新的安裝說(shuō)明可供參考。
與 Beautiful Soup 一樣,使用 lxml 模塊的第一步是將可能無(wú)效的 HTML 解析為統一格式。以下是使用此模塊解析相同的不完整 HTML 的示例。
>>> import lxml.html
>>> broken_html = 'AreaPopulation'
>>> tree = lxml.html.fromstring(broken_html) # parse the HTML
>>> fixed_html = lxml.html.tostring(tree, pretty_print=True)
>>> print fixed_html
Area
Population
同樣,lxml可以正確解析屬性和關(guān)閉標簽周?chē)鄙俚囊?,但是模塊不加和
標簽。
解析輸入內容后,進(jìn)入選擇元素的步驟。此時(shí),lxml 有幾種不同的方法,例如類(lèi)似于 Beautiful Soup 的 XPath 選擇器和 find() 方法。但是,在本例和后續示例中,我們將使用 CSS 選擇器,因為它們更簡(jiǎn)潔,可以在第 5 章解析動(dòng)態(tài)內容時(shí)重復使用。另外,一些有 jQuery 選擇器經(jīng)驗的讀者會(huì )更熟悉它。
以下是使用 lxml 的 CSS 選擇器提取區域數據的示例代碼。
>>> tree = lxml.html.fromstring(html)
>>> td = tree.cssselect('tr#places_area__row > td.w2p_fw')[0]
>>> area = td.text_content()
>>> print area
244,820 square kilometres
CSS 選擇器的關(guān)鍵行已加粗。這行代碼會(huì )先找到ID為places_area__row的表格行元素,然后選擇w2p_fw類(lèi)的表格數據子標簽。
CSS 選擇器
CSS 選擇器表示用于選擇元素的模式。下面是一些常用選擇器的示例。
選擇所有標簽:*
選擇<a>標簽:a
選擇所有class="link"的元素:.link
選擇class="link"的<a>標簽:a.link
選擇id="home"的<a>標簽:a#home
選擇父元素為<a>標簽的所有子標簽:a > span
選擇<a>標簽內部的所有標簽:a span
選擇title屬性為"Home"的所有<a>標簽:a[title=Home]
CSS3 規范已由 W3C 在 `/2011/REC-css3-selectors-20110929/ 提出。
Lxml 已經(jīng)實(shí)現了大部分 CSS3 屬性,不支持的功能可以在這里找到。
需要注意的是,lxml 的內部實(shí)現實(shí)際上將 CSS 選擇器轉換為等效的 XPath 選擇器。
2.2.4 性能對比
為了更好地評估本章描述的三種抓取方法之間的權衡,我們需要比較它們的相對效率。通常,爬蟲(chóng)從網(wǎng)頁(yè)中提取多個(gè)字段。因此,為了使比較更加真實(shí),我們將在本章中實(shí)現每個(gè)爬蟲(chóng)的擴展版本,從國家頁(yè)面中提取每個(gè)可用數據。首先,我們需要回到Firebug,檢查國家頁(yè)面其他功能的格式,如圖2.4.
您可以從 Firebug 的顯示中看到,表格中的每一行都有一個(gè)以 places_ 開(kāi)頭并以 __row 結尾的 ID。這些行中收錄的國家/地區數據的格式與上面示例中的格式相同。下面是使用上述信息提取所有可用國家/地區數據的實(shí)現代碼。
FIELDS = ('area', 'population', 'iso', 'country', 'capital',
'continent', 'tld', 'currency_code', 'currency_name', 'phone',
'postal_code_format', 'postal_code_regex', 'languages',
'nei*敏*感*詞*ours')
import re
def re_scraper(html):
results = {}
for field in FIELDS:
results[field] = re.search('.*?(.*?)' % field, html).groups()[0]
return results
from bs4 import BeautifulSoup
def bs_scraper(html):
soup = BeautifulSoup(html, 'html.parser')
results = {}
for field in FIELDS:
results[field] = soup.find('table').find('tr',
id='places_%s__row' % field).find('td',
class_='w2p_fw').text
return results
import lxml.html
def lxml_scraper(html):
tree = lxml.html.fromstring(html)
results = {}
for field in FIELDS:
results[field] = tree.cssselect('table > tr#places_%s__row
> td.w2p_fw' % field)[0].text_content()
return results
獲取結果
現在我們已經(jīng)完成了所有爬蟲(chóng)的代碼實(shí)現,接下來(lái)通過(guò)下面的代碼片段來(lái)測試這三種方法的相對性能。
import time
NUM_ITERATIONS = 1000 # number of times to test each scraper
html = download('http://example.webscraping.com/places/view/
United-Kingdom-239')
for name, scraper in [('Regular expressions', re_scraper),
('BeautifulSoup', bs_scraper),
('Lxml', lxml_scraper)]:
# record start time of scrape
start = time.time()
for i in range(NUM_ITERATIONS):
if scraper == re_scraper:
re.purge()
result = scraper(html)
# check scraped result is as expected
assert(result['area'] == '244,820 square kilometres')
# record end time of scrape and output the total
end = time.time()
print '%s: %.2f seconds' % (name, end – start)
在這段代碼中,每個(gè)爬蟲(chóng)會(huì )被執行1000次,每次執行都會(huì )檢查爬取結果是否正確,然后打印總時(shí)間。這里使用的下載函數還是上一章定義的。請注意,我們在粗體代碼行中調用了 re.purge() 方法。默認情況下,正則表達式模塊會(huì )緩存搜索結果,為了與其他爬蟲(chóng)比較公平,我們需要使用這種方法來(lái)清除緩存。
下面是在我的電腦上運行腳本的結果。
$ python performance.py
Regular expressions: 5.50 seconds
BeautifulSoup: 42.84 seconds
Lxml: 7.06 seconds
由于硬件條件的不同,不同計算機的執行結果也會(huì )有一定差異。但是,每種方法之間的相對差異應該具有可比性。從結果可以看出,Beautiful Soup 在爬取我們的示例網(wǎng)頁(yè)時(shí)比其他兩種方法慢 6 倍以上。事實(shí)上,這個(gè)結果是意料之中的,因為 lxml 和 regex 模塊是用 C 編寫(xiě)的,而 BeautifulSoup 是用純 Python 編寫(xiě)的。一個(gè)有趣的事實(shí)是 lxml 的行為與正則表達式一樣。由于 lxml 必須在搜索元素之前將輸入解析為內部格式,因此會(huì )產(chǎn)生額外的開(kāi)銷(xiāo)。當爬取同一個(gè)網(wǎng)頁(yè)的多個(gè)特征時(shí),這種初始解析的開(kāi)銷(xiāo)會(huì )減少,lxml會(huì )更有競爭力。多么神奇的模塊!
2.2.5 個(gè)結論
表2.1總結了每種爬取方式的優(yōu)缺點(diǎn)。
如果您的爬蟲(chóng)的瓶頸是下載頁(yè)面,而不是提取數據,那么使用較慢的方法(如 Beautiful Soup)不是問(wèn)題。如果你只需要抓取少量數據并想避免額外的依賴(lài),那么正則表達式可能更合適。通常,lxml 是抓取數據的最佳選擇,因為它快速且健壯,而正則表達式和 Beautiful Soup 僅在某些場(chǎng)景下有用。
2.2.6 添加鏈接爬蟲(chóng)的爬取回調
我們已經(jīng)看到了如何抓取國家數據,我們需要將其集成到上一章的鏈接爬蟲(chóng)中。為了重用這個(gè)爬蟲(chóng)代碼去抓取其他網(wǎng)站,我們需要添加一個(gè)回調參數來(lái)處理抓取行為?;卣{是在某個(gè)事件發(fā)生后調用的函數(在這種情況下,在網(wǎng)頁(yè)下載完成后)。爬取回調函數收錄url和html兩個(gè)參數,可以返回要爬取的url列表。下面是它的實(shí)現代碼??梢?jiàn),用Python實(shí)現這個(gè)功能非常簡(jiǎn)單。
def link_crawler(..., scrape_callback=None):
...
links = []
if scrape_callback:
links.extend(scrape_callback(url, html) or [])
...
在上面的代碼片段中,我們將新添加的抓取回調函數代碼加粗。如果想獲取該版本鏈接爬蟲(chóng)的完整代碼,可以訪(fǎng)問(wèn)org/wswp/code/src/tip/chapter02/link_crawler.py。
現在,我們只需要自定義傳入的scrape_callback函數,就可以使用爬蟲(chóng)抓取其他網(wǎng)站了。下面修改lxml抓取示例的代碼,以便在回調函數中使用。
def scrape_callback(url, html):
if re.search('/view/', url):
tree = lxml.html.fromstring(html)
row = [tree.cssselect('table > tr#places_%s__row >
td.w2p_fw' % field)[0].text_content() for field in
FIELDS]
print url, row
上面的回調函數會(huì )抓取國家數據并顯示出來(lái)。不過(guò)一般情況下,在爬取網(wǎng)站的時(shí)候,我們更希望能夠重用這些數據,所以讓我們擴展一下它的功能,把得到的數據保存在一個(gè)CSV表中。代碼如下。
import csv
class ScrapeCallback:
def __init__(self):
self.writer = csv.writer(open('countries.csv', 'w'))
self.fields = ('area', 'population', 'iso', 'country',
'capital', 'continent', 'tld', 'currency_code',
'currency_name', 'phone', 'postal_code_format',
'postal_code_regex', 'languages',
'nei*敏*感*詞*ours')
self.writer.writerow(self.fields)
def __call__(self, url, html):
if re.search('/view/', url):
tree = lxml.html.fromstring(html)
row = []
for field in self.fields:
row.append(tree.cssselect('table >
tr#places_{}__row >
td.w2p_fw'.format(field))
[0].text_content())
self.writer.writerow(row)
為了實(shí)現這個(gè)回調,我們使用回調類(lèi)而不是回調函數,以維護 csv 中 writer 屬性的狀態(tài)。在構造函數中實(shí)例化csv的writer屬性,然后在__call__方法中進(jìn)行多次寫(xiě)入。注意__call__是一個(gè)特殊的方法,當一個(gè)對象作為函數調用時(shí)會(huì )調用,這也是鏈接爬蟲(chóng)中cache_callback的調用方式。也就是說(shuō),scrape_callback(url, html) 等價(jià)于調用 scrape_callback.__call__(url, html)。如果想詳細了解Python的特殊類(lèi)方法,可以參考一下。
以下是將回調傳遞給鏈接爬蟲(chóng)的代碼。
link_crawler('http://example.webscraping.com/', '/(index|view)',
max_depth=-1, scrape_callback=ScrapeCallback())
現在,當我們使用回調運行這個(gè)爬蟲(chóng)時(shí),程序會(huì )將結果寫(xiě)入 CSV 文件,我們可以使用 Excel 或 LibreOffice 等應用程序查看該文件,如圖 2.5 所示。
成功!我們完成了我們的第一個(gè)工作數據抓取爬蟲(chóng)。





