Python爬蟲之三種網頁抓取方法性能比較

 下面咱們將介紹三種抓取網頁數據的方法,首先是正則表達式,而後是流行的 BeautifulSoup 模塊,最後是強大的 lxml 模塊。css

1. 正則表達式html

  若是你對正則表達式還不熟悉,或是須要一些提示時,能夠查閱Regular Expression HOWTO 得到完整介紹。python

  當咱們使用正則表達式抓取國家面積數據時,首先要嘗試匹配元素中的內容,以下所示:css3

>>> import re >>> import urllib2 >>> url = 'http://example.webscraping.com/view/United-Kingdom-239' >>> html = urllib2.urlopen(url).read() >>> re.findall('<td class="w2p_fw">(.*?)</td>', html) ['<img src="/places/static/images/flags/gb.png" />', '244,820 square kilometres', '62,348,447', 'GB', 'United Kingdom', 'London', '<a href="/continent/EU">EU</a>', '.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', '<div><a href="/iso/IE">IE </a></div>'] >>> 

 

 

   從上述結果看出,多個國家眷性都使用了< td class=」w2p_fw」 >標籤。要想分離出面積屬性,咱們能夠只選擇其中的第二個元素,以下所示:web

>>> re.findall('<td class="w2p_fw">(.*?)</td>', html)[1] '244,820 square kilometres'

 

   雖然如今可使用這個方案,可是若是網頁發生變化,該方案極可能就會失效。好比表格發生了變化,去除了第二行中的國土面積數據。若是咱們只在如今抓取數據,就能夠忽略這種將來可能發生的變化。可是,若是咱們但願將來還能再次抓取該數據,就須要給出更加健壯的解決方案,從而儘量避免這種佈局變化所帶來的影響。想要該正則表達式更加健壯,咱們能夠將其父元素< tr >也加入進來。因爲該元素具備ID屬性,因此應該是惟一的。正則表達式

>>> re.findall('<tr id="places_area__row"><td class="w2p_fl"><label for="places_area" id="places_area__label">Area: </label></td><td class="w2p_fw">(.*?)</td>', html) ['244,820 square kilometres']

 

 

  這個迭代版本看起來更好一些,可是網頁更新還有不少其餘方式,一樣可讓該正則表達式沒法知足。好比,將雙引號變爲單引號,< td >標籤之間添加多餘的空格,或是變動area_label等。下面是嘗試支持這些可能性的改進版本。express

>>> re.findall('<tr id="places_area__row">.*?<td\s*class=["\']w2p_fw["\']>(.*?)</td>',html)['244,820 square kilometres']

 

  雖然該正則表達式更容易適應將來變化,但又存在難以構造、可讀性差的問題。此外,還有一些微小的佈局變化也會使該正則表達式沒法知足,好比在< td >標籤裏添加title屬性。 
  從本例中能夠看出,正則表達式爲咱們提供了抓取數據的快捷方式,可是,該方法過於脆弱,容易在網頁更新後出現問題。幸虧還有一些更好的解決方案,後期將會介紹。api

2. Beautiful Soup緩存

  Beautiful Soup是一個很是流行的 Python 模塊。該模塊能夠解析網頁,並提供定位內容的便捷接口。若是你尚未安裝該模塊,可使用下面的命令安裝其最新版本(須要先安裝 pip,請自行百度):app

pip install beautifulsoup4

 

  使用 Beautiful Soup 的第一步是將已下載的 HTML 內容解析爲 soup 文檔。因爲大多數網頁都不具有良好的 HTML格式,所以 Beautiful Soup 須要對其實際格式進行肯定。例如,在下面這個簡單網頁的列表中,存在屬性值兩側引號缺失和標籤未閉合的問題。

<ul class=country> <li>Area <li>Population </ul>

 

 

  若是 Population 列表項被解析爲 Area 列表項的子元素,而不是並列的兩個列表項的話,咱們在抓取時就會獲得錯誤的結果。下面讓咱們看一下 Beautiful Soup 是如何處理的。

>>> from bs4 import BeautifulSoup
>>> broken_html = '<ul class=country><li>Area<li>Population</ul>' >>> # parse the HTML >>> soup = BeautifulSoup(broken_html, 'html.parser') >>> fixed_html = soup.prettify() >>> print fixed_html <ul class="country"> <li> Area <li> Population </li> </li> </ul>

  從上面的執行結果中能夠看出,Beautiful Soup 可以正確解析缺失的引號並閉合標籤。如今可使用 find() 和 find_all() 方法來定位咱們須要的元素了。

>>> ul = soup.find('ul', attrs={'class':'country'})
>>> ul.find('li') # return just the first match
<li>Area<li>Population</li></li> >>> ul.find_all('li') # return all matches [<li>Area<li>Population</li></li>, <li>Population</li>]

 

Note: 因爲不一樣版本的Python內置庫的容錯能力有所區別,可能處理結果和上述有所不一樣,具體請參考: https://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser。想了解所有方法和參數,能夠查閱 Beautiful Soup 的 官方文檔

  下面是使用該方法抽取示例國家面積數據的完整代碼。

>>> from bs4 import BeautifulSoup >>> import urllib2 >>> url = 'http://example.webscraping.com/view/United-Kingdom-239' >>> html = urllib2.urlopen(url).read() >>> # locate the area row >>> tr = soup.find(attrs={'id':'places_area__row'}) >>> # locate the area tag >>> td = tr.find(attrs={'class':'w2p_fw'}) >>> area = td.text # extract the text from this tag >>> print area 244,820 square kilometres

 

  這段代碼雖然比正則表達式的代碼更加複雜,但更容易構造和理解。並且,像多餘的空格和標籤屬性這種佈局上的小變化,咱們也無需再擔憂了。

3. Lxml

  Lxml 是基於 libxml2 這一 XML 解析庫的 Python 封裝。該模塊使用 C語言 編寫,解析速度比 Beautiful Soup 更快,不過安裝過程也更爲複雜。最新的安裝說明能夠參考 http://lxml.de/installation.html .**

  和 Beautiful Soup 同樣,使用 lxml 模塊的第一步也是將有可能不合法的 HTML 解析爲統一格式。下面是使用該模塊解析一個不完整 HTML 的例子:

>>> import lxml.html
>>> broken_html = '<ul class=country><li>Area<li>Population</ul>' >>> # parse the HTML >>> tree = lxml.html.fromstring(broken_html) >>> fixed_html = lxml.html.tostring(tree, pretty_print=True) >>> print fixed_html <ul class="country"> <li>Area</li> <li>Population</li> </ul>

 

 

  一樣地,lxml 也能夠正確解析屬性兩側缺失的引號,並閉合標籤,不過該模塊沒有額外添加 < html > 和 < body > 標籤。

  解析完輸入內容以後,進入選擇元素的步驟,此時 lxml 有幾種不一樣的方法,好比 XPath 選擇器和相似 Beautiful Soup 的 find() 方法。不過,後續咱們將使用 CSS 選擇器,由於它更加簡潔,而且可以在解析動態內容時得以複用。此外,一些擁有 jQuery 選擇器相關經驗的讀者會對其更加熟悉。

  下面是使用 lxml 的 CSS 選擇器抽取面積數據的示例代碼:

>>> import urllib2 >>> import lxml.html >>> url = 'http://example.webscraping.com/view/United-Kingdom-239' >>> html = urllib2.urlopen(url).read() >>> 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

 

   *行代碼首先會找到 ID 爲 places_area__row 的表格行元素,而後選擇 class 爲 w2p_fw 的表格數據子標籤。

   CSS 選擇器表示選擇元素所使用的模式,下面是一些經常使用的選擇器示例:

選擇全部標籤: *
選擇 <a> 標籤: a 選擇全部 class="link" 的元素: .link 選擇 class="link" 的 <a> 標籤: a.link 選擇 id="home" 的 <a> 標籤: a#home 選擇父元素爲 <a> 標籤的全部 <span> 子標籤: a > span 選擇 <a> 標籤內部的全部 <span> 標籤: a span 選擇 title 屬性爲"Home"的全部 <a> 標籤: a[title=Home]

   W3C 已提出 CSS3 規範,其網址爲 https://www.w3.org/TR/2011/REC-css3-selectors-20110929/

  Lxml 已經實現了大部分 CSS3 屬性,其不支持的功能能夠參見: https://cssselect.readthedocs.io/en/latest/ .

Note: lxml在內部的實現中,其實是將 CSS 選擇器轉換爲等價的 XPath 選擇器。

4. 性能對比

   在如下這段代碼中,每一個爬蟲都會執行 1000 次,每次執行都會檢查抓取結果是否正確,而後打印總用時。

# -*- coding: utf-8 -*- import csv import time import urllib2 import re import timeit from bs4 import BeautifulSoup import lxml.html FIELDS = ('area', 'population', 'iso', 'country', 'capital', 'continent', 'tld', 'currency_code', 'currency_name', 'phone', 'postal_code_format', 'postal_code_regex', 'languages', 'neighbours') def regex_scraper(html): results = {} for field in FIELDS: results[field] = re.search('<tr id="places_{}__row">.*?<td class="w2p_fw">(.*?)</td>'.format(field), html).groups()[0] return results def beautiful_soup_scraper(html): soup = BeautifulSoup(html, 'html.parser') results = {} for field in FIELDS: results[field] = soup.find('table').find('tr', id='places_{}__row'.format(field)).find('td', class_='w2p_fw').text return results def lxml_scraper(html): tree = lxml.html.fromstring(html) results = {} for field in FIELDS: results[field] = tree.cssselect('table > tr#places_{}__row > td.w2p_fw'.format(field))[0].text_content() return results def main(): times = {} html = urllib2.urlopen('http://example.webscraping.com/view/United-Kingdom-239').read() NUM_ITERATIONS = 1000 # number of times to test each scraper for name, scraper in ('Regular expressions', regex_scraper), ('Beautiful Soup', beautiful_soup_scraper), ('Lxml', lxml_scraper): times[name] = [] # record start time of scrape start = time.time() for i in range(NUM_ITERATIONS): if scraper == regex_scraper: # the regular expression module will cache results # so need to purge this cache for meaningful timings re.purge() # *行代碼 result = scraper(html) # check scraped result is as expected assert(result['area'] == '244,820 square kilometres') times[name].append(time.time() - start) # record end time of scrape and output the total end = time.time() print '{}: {:.2f} seconds'.format(name, end - start) writer = csv.writer(open('times.csv', 'w')) header = sorted(times.keys()) writer.writerow(header) for row in zip(*[times[scraper] for scraper in header]): writer.writerow(row) if __name__ == '__main__': main()

 

 

   注意,咱們在 *行代碼 中調用了 re.purge() 方法。默認狀況下,正則表達式會緩存搜索結果,爲了公平起見,咱們須要使用該方法清除緩存。

下面是個人電腦運行該腳本的結果:

這裏寫圖片描述


   因爲硬件條件的區別,不一樣電腦的執行結果也會存在必定差別。不過,每種方法之間的相對差別應當是至關的。從結果中能夠看出,在抓取咱們的示例網頁時,Beautiful Soup 比其餘兩種方法慢了超過 7 倍之多。實際上這一結果是符合預期的,由於 lxml 和正則表達式模塊都是 C 語言編寫的,而 Beautiful Soup 則是純 Python 編寫的。一個有趣的事實是,lxml 表現的和正則表達式差很少好。因爲 lxml 在搜索元素以前,必須將輸入解析爲內部格式,所以會產生額外的開銷。而當抓取同一網頁的多個特徵時,這種初始化解析產生的開銷就會下降,lxml 也就更具競爭力,因此說,lxml 是一個強大的模塊。

5. 總結

三種網頁抓取方法優缺點:

       抓取方法     性能       使用難度       安裝難度
正則表達式 困難 簡單(內置模塊)
Beautiful Soup 簡單 簡單(純Python)
Lxml 簡單 相對困難



   若是你的爬蟲瓶頸是下載網頁,而不是抽取數據的話,那麼使用較慢的方法(如 Beautiful Soup)也不成問題。正則表達式在一次性抽取中很是有用,此外還能夠避免解析整個網頁帶來的開銷,若是隻需抓取少許數據,而且想要避免額外依賴的話,那麼正則表達式可能更加適合。不過,一般狀況下,lxml 是抓取數據的最好選擇,這是由於它不只速度快,功能也更加豐富,而正則表達式和 Beautiful Soup只在某些特定場景下有用。

 

 

 

 

轉自:https://blog.csdn.net/oscer2016/article/details/70209144

相關文章
相關標籤/搜索