在上一篇中 , 我們構(gòu)建了一個(gè)爬蟲(chóng), 可以通過(guò)跟蹤鏈接的方式下載我們所
需的網(wǎng)頁(yè)。 但是爬蟲(chóng)在下載網(wǎng)頁(yè)之后又將 結(jié)果丟棄掉了 。 現(xiàn)在, 我們需要讓這個(gè)爬蟲(chóng)從每個(gè)網(wǎng)頁(yè)中抽取一些數(shù)據(jù),然后實(shí)現(xiàn)某些事情, 這種做法也被稱(chēng)為抓取(scraping) 。
1.分析網(wǎng)頁(yè)
在抓取之前我們首先應(yīng)該了解網(wǎng)頁(yè)的結(jié)構(gòu)如何,可以用瀏覽器打開(kāi)你要抓取的網(wǎng)頁(yè)然后有點(diǎn)單機(jī)選擇查看頁(yè)面源代碼,
推薦使用火狐瀏覽器 可以使用firebug工具進(jìn)行查看 安裝firebug http://jingyan.baidu.com/article/fdffd1f832b032f3e98ca1b2.html
可以右鍵單機(jī)我們?cè)谧ト≈懈信d趣的網(wǎng)頁(yè)部分如圖所示
2,三種網(wǎng)頁(yè)的抓取方法
- 正則表達(dá)式
- BeautifulSoup模塊
- lxml
2.1正則表達(dá)式
import requests
import re
def download(url, user_agent='jians', num_retries=2):
headers = {'User-agent': user_agent}
response = requests.get(url, headers=headers)
if num_retries > 0:
if 500 <= response.status_code < 600:
return download(url, num_retries - 1)
return response.text
def get_page():
url = 'http://example.webscraping.com/view/Unitled-Kingdom-239'
html = download(url)
# 通過(guò)嘗試匹配<td>元素中的內(nèi)容
html_str = re.findall('<td class="w2p_fw">(.*?)</td>', html)
print html_str
# 分離出面積熟悉,拿到第二個(gè)元素(area)的信息
area_str = re.findall('<td class="w2p_fw">(.*?)</td>', html)[1]
print area_str
# 通過(guò)指定tr的id去查詢(xún)area,可以有效的防止網(wǎng)頁(yè)發(fā)生變化
html_places = re.findall('<tr id="places_area__row">.*?'
'<td class="w2p_fw">(.*?)</td>', html)
print html_places
get_page()
正則表達(dá)式為我們提供了抓取數(shù)據(jù)的快捷方式, 但是該方法過(guò)于脆弱 , 容易在網(wǎng)頁(yè)更新后出現(xiàn)問(wèn)題
2.2 Beautiful Soup
Beautiful Soup 是一個(gè)非常流行的 Python 模塊。 該模塊可以解析網(wǎng)頁(yè), 并
提供定位 內(nèi) 容的便捷接 口
安裝beautifulsoup
pip install beautifulsoup4
beanuifulsoup4文檔
https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html
簡(jiǎn)單使用:
from bs4 import BeautifulSoup #導(dǎo)入beautifulsoup需要這樣導(dǎo)入
import requests
def get_area():
'''使用該方法抽取示例 國(guó)家面積數(shù)據(jù)的完整代碼。'''
url = 'http://example.webscraping.com/view/Unitled-Kingdom-239'
html = requests.get(url).text
soup = BeautifulSoup(html,'html.parser')
tr = soup.find(attrs={'id': 'places_area__row'})
td = tr.find(attrs={'class': 'w2p_fw'})
area = td.text
print area
get_area()
2.3Lxml
Lxml 是基于 libxml2 這一 XML 解析庫(kù)的 Python 封裝。 該模塊使用 C
語(yǔ)言編寫(xiě) , 解析速度 比 Beautiful Soup 更快.
安裝模塊
pip install lxml
ping install cssselect #需要css選擇器模塊
簡(jiǎn)單使用:
import lxml.html
import requests
def get_area():
'''將有可能不合法的HTML 解析為統(tǒng)一格式'''
broken_html = '<ul class = country> <li>Area<li>Population</ul>'
tree = lxml.html.fromstring(broken_html)
fixed_html= lxml.html.tostring(tree, pretty_print=True)
print fixed_html
def get_area1():
'''使用CSS選擇器抽取面積數(shù)據(jù)的代碼'''
url = 'http://example.webscraping.com/view/Unitled-Kingdom-239'
html = requests.get(url).text
tree = lxml.html.fromstring(html)
td = tree.cssselect('tr#places_area__row > td.w2p_fw')[0]
area = td.text_content()
print area
get_area()
get_area1()
2.4三種抓取方法的對(duì)比
用三種方式分別抓取1000次http://example.webscraping.com/view/Unitled-Kingdom-239 網(wǎng)頁(yè)中的國(guó)家數(shù)據(jù),并打印時(shí)間
代碼:
#!/usr/bin/env python
# -*-coding:utf-8 -*-
import re
from bs4 import BeautifulSoup
import lxml.html
from lxml.cssselect import CSSSelector
import requests
import time
FIELDS = ('area', 'population', 'iso', 'country', 'capital', 'continent', 'tld', 'currency_code',
'currency_name', 'phone', 'postal_code_format', 'postal_code_regex', 'languages', 'neighbours' )
def download(url, user_agent='jians', num_retries=2):
headers = {'User-agent': user_agent}
response = requests.get(url, headers=headers)
if num_retries > 0:
if 500 <= response.status_code < 600:
return download(url, num_retries - 1)
return response.text
def re_scarper(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 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
def lx_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
def get_counter():
NUM_ITERATIONS = 1000
html = download('http://example.webscraping.com/view/Unitled-Kingdom-239')
for name, scrapter in [('REGULAR expression', re_scarper),
('beautifulsoup', bs_scraper),
('lxml', lx_scraper)]:
start = time.time()
for i in range(NUM_ITERATIONS):
if scrapter == re_scarper:
re.purge()
result = scrapter(html)
assert(result['area'] == '244,820 square kilometres')
end = time.time()
print '%s:%.2f seconds' % (name, end-start)
注意代碼格式,方法之間空兩行
種方法之間 的相對(duì)差異應(yīng)當(dāng)是相 當(dāng) 的 。 從結(jié)果中可以看出 , 在抓取我們的示
例 網(wǎng)頁(yè)時(shí), Beautiful Soup 比其他兩種方法慢了超過(guò) 6 倍之多 。 實(shí)際上這一結(jié)
果是符合預(yù)期的 , 因 為 lxml 和正則表達(dá)式模塊都是 C 語(yǔ)言編寫(xiě) 的 , 而
BeautifulSoup 則是純 Python 編寫(xiě)的 。 一個(gè)有趣的事實(shí)是, lxml 表現(xiàn)得
和正則表達(dá)式差不多好。 由于 lxml 在搜索元素之前, 必須將輸入解析為 內(nèi)
部格式, 因此會(huì)產(chǎn)生額外的開(kāi)銷(xiāo) 。 而當(dāng)抓取同一網(wǎng)頁(yè)的多個(gè)特征時(shí), 這種初
始化解析產(chǎn)生的開(kāi)銷(xiāo)就會(huì)降低, lxml 也就更具競(jìng)爭(zhēng)力 。
結(jié)論
法 ( 如 Beautiful Soup) 也不成問(wèn)題。 如果只需抓取少量數(shù)據(jù) , 并且想要避免
額外依賴(lài)的話(huà), 那么正則表達(dá)式可能更加適合。 不過(guò), 通常情況下, lxml 是
抓取數(shù)據(jù) 的最好選擇 , 這是 因 為 該方法既快速又健壯 , 而正則表達(dá)式和
Beautiful Soup 只在某些特定場(chǎng)景下有用。
為鏈接爬蟲(chóng)添加抓取回調(diào)
前面我們已經(jīng)了解了 如何抓取國(guó)家數(shù)據(jù),接下來(lái)我們需要將其集成到上
一篇的鏈接爬蟲(chóng)當(dāng)中
獲取該版本鏈接爬蟲(chóng)的完整代碼, 可以訪問(wèn)
https : //bitbucket .org/wswp/code/src/tip/chapter02 /link crawler . py。
對(duì)傳入的 scrape callback 函數(shù)定制化處理, 就能使
用該爬蟲(chóng)抓取其他網(wǎng)站了
import re
import lxml.html
from link_craw import link_crawler
FIELDS = ('area', 'population', 'iso', 'country', 'capital', 'continent', 'tld', 'currency_code', 'currency_name', 'phone', 'postal_code_format', 'postal_code_regex', 'languages', 'neighbours')
def scrape_callback(url, html):
'''使用lxml抓取'''
if re.search('/view/', url):
tree = lxml.html.fromstring(html)
row = [tree.cssselect('table > tr#places_{}__row > td.w2p_fw'.format(field))[0].text_content() for field in FIELDS]
print url, row
if __name__ == '__main__':
link_crawler('http://example.webscraping.com/', '/(index|view)', scrape_callback=scrape_callback)
在抓取網(wǎng)站時(shí), 我們更希望能夠復(fù)用這些數(shù)據(jù) , 因此下面我們
對(duì)其功能進(jìn)行擴(kuò)展, 把得到的結(jié)果數(shù)據(jù)保存到 csv 表格中 , 其代碼如下所示。
import csv
import re
import lxml.html
from link_craw import link_crawler
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', 'neighbours')
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)
if __name__ == '__main__':
link_crawler('http://example.webscraping.com/', '/(index|view)', scrape_callback=ScrapeCallback())
程序就會(huì)將結(jié)果寫(xiě)入一個(gè) csv 文件中 , 我們可以使用類(lèi)似 Excel 或者 LibreOffice 的應(yīng)用查看該文件,
我們完成了第一個(gè)可以工作的數(shù)據(jù)抓取爬蟲(chóng)!