本篇從實際出發,展示如何用網頁爬蟲。並介紹一個流行的爬蟲框架~
1. 網頁爬蟲的過程
所謂網頁爬蟲,就是模擬瀏覽器的行為訪問網站,從而獲得網頁信息的程序。正因為是程序,所以獲得網頁的速度可以輕易超過單身多年的手速:)。通常適用於需要大量網頁信息的場合。
爬取網頁的流程為:訪問初始url -> 獲得返回的網頁,從這個網頁中得到新的url並放入待爬隊列 -> 訪問新的url -> ...依次循環。整體上來看就是一個廣度優先的過程,當然,新的url也不一定非要從返回的網頁中獲得。
一個簡單的網頁爬蟲應該包括以下部分:
- 一個url隊列。我們的爬蟲從這個隊列中讀取url,並將新的url放入這個隊列。這里最重要的是判重。簡單的哈希就能達到判重的目的,但是為了節約空間(url的數量往往很多),一般使用bloomfilter的思想。bloomfilter與普通的哈希算法最大的不同就是bloomfilter只需要一個bit來表示某個元素是否存在,所以能節約空間。bloomfilter有一個小缺點,即准確率並不是百分百:判斷一個元素是不是已經存在時,已有的有很小的可能會判斷為不存在,但是沒有的元素一定會判斷為不存在。
- 網頁爬取模塊。需要能模擬瀏覽器發送請求。
- 網頁分析模塊。爬下來的是網頁源碼,可以用正則或者其他方法提取我們需要的信息。
- 新的url生成模塊。生成新的url,放入隊列。
那么,最簡單的爬蟲就可以這么寫:
import Queue
start_url = "http://www.cnblogs.com/rubinorth"
url_queue = Queue.Queue() # url隊列
url_queue.put(start_url)
bloomfilter.put(start_url)
#### 一直循環到隊列為空 ####
while(True):
if url_queue.size() > 0:
current_url = url_queue.get() # 隊首的url
page = crawl(current_url) # crawl為網頁爬取模塊,page是爬到的網頁源代碼
next_urls = deal_page(page) # deal_page為網頁分析模塊,next_urls是新的一些url
for next_url in next_urls:
if not bloomfilter.has(next_url): # 判重
bloomfilter.put(next_url)
url_queue.put(next_url)
else:
break
2. 為什么選用scrapy
scrapy是目前一個比較流行的爬蟲框架,其基本原理與上面的爬蟲是一樣的,但是它提供了很多便利的功能。
首先,先簡要介紹一下scrapy各個模塊之間的關系和整個框架運行的流程。是時候祭出那張scrapy的經典圖了:
從這張圖上看,scrapy包含了以下模塊:
- scrapy engine,主引擎。在處理數據流時負責管理整個系統,同時各種事件的觸發也由其負責。
- spider,即我們的爬蟲。主要的爬蟲代碼都在這部分中,包括發起請求,處理返回的網頁等等。
- spider middleware,spider中間件。中間件的一種,主要工作對spider發送的request做一些處理。
- scheduler,調度器。上面說到的url隊列就是調度器在管理,一方面接收spider發送的request請求,放入隊列中;另一方面會從隊首取出request交由downloader去下載網頁。
- downloader,下載器。將網頁的html源碼下載下來供之后的網頁分析和信息提取。
- downloader middleware,下載器中間件。中間件的一種,在下在網頁之前和之后都會運行,可以用來設置發送請求的header,cookies,代理ip以及處理一些錯誤返回的情況。
- item pipeline, 管道。一個網頁被爬取下來並解析之后,其后續的信息存儲之類的工作在pipeline中進行。當然也可以直接在spider中完成這些工作,但在pipeline中完成則顯得整個項目結構清晰。
上面列出的里面spider,pipeline需要自己寫,兩種middleware需要的話可以自己添加自己寫的。
光介紹給人感覺比較空洞,那下面就讓我們來使用scrapy實現一個簡單的爬蟲吧。
3. scrapy實現爬蟲
scrapy createproject cnblog_project
使用上面的命令創建一個scrapy工程之后,首先我們要寫的是spider。
class CnblogSpider(Spider):
name = 'cnblog_spider' # 爬蟲名字
allowed_domain = ['cnblogs.com'] # 允許的domain
def __init__(self):
self.start_urls = ['http://www.cnblogs.com/rubinorth']
def start_requests(self):
return [Request(url, callback=self.parse_page) for url in self.start_urls]
# 分析爬取的頁面並構造下一個頁面的請求
def parse_page(self, response):
logging.info("parse : " + response.url)
sel = Selector(response)
item = CnblogItem()
# 提取頁面內容
item['name'] = sel.xpath("//a[@id='Header1_HeaderTitle']/text()").extract()[0]
yield item
# 下一個頁面的請求
new_url = get_new_url(response.body) # 根據源碼分析出新的鏈接,需自己實現
yield Request(new_url, callback=self.parse_page)
上面是一個簡單的爬蟲,start_urls是初始的url集合(上面只有一個),start_requests則根據start_urls構造Request,並交給調度器。parse_page中,response是返回的頁面的源碼;CnblogItem是scrapy提供的item組件,方便結構化地提取源碼中的數據,而yield item則會將這個item交給管道;yield Request(new_url, callback=self.parse_page)則會發送一個新的Request,發起下一輪的爬取。
items.py中只要這么寫:
class CnblogItem(scrapy.Item):
name = scrapy.Field()
接着,我們需要寫pipelines.py
class CnblogPipeline(object):
def process_item(self, item, spider):
print item['name']
return item
每個pipeline都必須有process_item這個方法。上面我們只是簡單地打印出了name。return item是考慮到可能有多個pipeline(return了之后可以讓其他pipeline處理)。
最后,只需要修改settings.py即可:
...
ITEM_PIPELINES = {
'yelp_project.pipelines.CnblogPipeline': 304,
}
...
需要在setting中打開自己的pipeline。
好了,一個簡單的爬蟲就這么寫完了。注意我們並沒有用到中間件,也不需要寫自己的中間件。
最后, 命令行運行:
scrapy crawl cnblog_spider