(點擊上方藍字,可快速關注我們)
提示:在歷史推送中,可查看昨天(04月24日)的上一篇。
在上一篇中,我們實現(xiàn)了一個基本網(wǎng)絡爬蟲,它可以從StackOverflow上下載最新的問題,并將它們存儲在MongoDB數(shù)據(jù)庫中。在本文中,我們將對其擴展,使它能夠爬取每個網(wǎng)頁底部的分頁鏈接,并從每一頁中下載問題(包含問題標題和URL)。
" 在你開始任何爬取工作之前,檢查目標網(wǎng)站的使用條款并遵守robots.txt文件。同時,做爬取練習時遵守道德,不要在短時間內(nèi)向某個網(wǎng)站發(fā)起大量請求。像對待自己的網(wǎng)站一樣對待任何你將爬取的網(wǎng)站。"
開始
有兩種可能的方法來接著從上次我們停下的地方繼續(xù)進行。
第一個方法是,擴展我們現(xiàn)有的網(wǎng)絡爬蟲,通過利用一個xpath表達式從”parse_item”方法里的響應中提取每個下一頁鏈接,并通過回調(diào)同一個parse_item方法產(chǎn)生一個請求對象。利用這種方法,爬蟲會自動生成針對我們指定的鏈接的新請求,你可以在Scrapy文檔這里找到更多有關該方法的信息。
另一個更簡單的方法是,使用一個不同類型的爬蟲—CrawlSpider(鏈接)。這是基本Spider的一個擴展版本,它剛好滿足我們的要求。
CrawlSpider
我們將使用與上一篇教程中相同的爬蟲項目,所以如果你需要的話可以從repo上獲取這些代碼。
創(chuàng)建樣板
在“stack”目錄中,首先由crawl模板生成爬蟲樣板。
$ scrapy genspider stack_crawler -t crawl
Created spider 'stack_crawler' using template 'crawl' in module:
stack.spiders.stack_crawler
Scrapy項目現(xiàn)在看起來應該像這樣:
├── scrapy.cfg
└── stack
├── __init__.py
├── items.py
├── pipelines.py
├── settings.py
└── spiders
├── __init__.py
├── stack_crawler.py
└── stack_spider.py
stack_crawler.py文件內(nèi)容如下:
# -*- coding: utf-8 -*-
import scrapy
from scrapy.contrib.linkextractors import LinkExtractor
from scrapy.contrib.spiders import CrawlSpider, Rule
from stack.items import StackItem
class StackCrawlerSpider(CrawlSpider):
name = 'stack_crawler'
allowed_domains = ['']
start_urls = ['http://www./']
rules = (
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
)
def parse_item(self, response):
i = StackItem()
#i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()
#i['name'] = response.xpath('//div[@id="name"]').extract()
#i['description'] = response.xpath('//div[@id="description"]').extract()
return i
我們只需要對這個樣板做一些更新。
更新“start_urls”列表
首先,添加問題的第一個頁面鏈接到start_urls列表:
start_urls = [
'http:///questions?pagesize=50&sort=newest'
]
更新“rules”列表
接下來,我們需要添加一個正則表達式到“rules”屬性中,以此告訴爬蟲在哪里可以找到下一個頁面鏈接:
rules = [
Rule(LinkExtractor(allow=r'questions?page=[0-9]&sort=newest'),
callback='parse_item', follow=True)
]
現(xiàn)在爬蟲能根據(jù)那些鏈接自動請求新的頁面,并將響應傳遞給“parse_item”方法,以此來提取問題和對應的標題。
如果你仔細查看的話,可以發(fā)現(xiàn)這個正則表達式限制了它只能爬取前9個網(wǎng)頁,因為在這個demo中,我們不想爬取所有的176234個網(wǎng)頁。
更新“parse_item”方法
現(xiàn)在我們只需編寫如何使用xpath解析網(wǎng)頁,這一點我們已經(jīng)在上一篇教程中實現(xiàn)過了,所以直接復制過來。
def parse_item(self, response):
questions = response.xpath('//div[@class="summary"]/h3')
for question in questions:
item = StackItem()
item['url'] = question.xpath(
'a[@class="question-hyperlink"]/@href').extract()[0]
item['title'] = question.xpath(
'a[@class="question-hyperlink"]/text()').extract()[0]
yield item
這就是為爬蟲提供的解析代碼,但是現(xiàn)在先不要啟動它。
添加一個下載延遲
我們需要通過在settings.py文件中設定一個下載延遲來善待StackOverflow(和任何其他網(wǎng)站)。
DOWNLOAD_DELAY = 5
這告訴爬蟲需要在每兩個發(fā)出的新請求之間等待5秒鐘。你也很有必要做這樣的限制,因為如果你不這么做的話,StackOverflow將會限制你的訪問流量,如果你繼續(xù)不加限制地爬取該網(wǎng)站,那么你的IP將會被禁止。所有,友好點—要像對待自己的網(wǎng)站一樣對待任何你爬取的網(wǎng)站。
現(xiàn)在只剩下一件事要考慮—存儲數(shù)據(jù)。
MongoDB
上次我們僅僅下載了50個問題,但是因為這次我們要爬取更多的數(shù)據(jù),所有我們希望避免向數(shù)據(jù)庫中添加重復的問題。為了實現(xiàn)這一點,我們可以使用一個MongoDB的 upsert方法,它意味著如果一個問題已經(jīng)存在數(shù)據(jù)庫中,我們將更新它的標題;否則我們將新問題插入數(shù)據(jù)庫中。
修改我們前面定義的MongoDBPipeline:
class MongoDBPipeline(object):
def __init__(self):
connection = pymongo.Connection(
settings['MONGODB_SERVER'],
settings['MONGODB_PORT']
)
db = connection[settings['MONGODB_DB']]
self.collection = db[settings['MONGODB_COLLECTION']]
def process_item(self, item, spider):
for data in item:
if not data:
raise DropItem("Missing data!")
self.collection.update({'url': item['url']}, dict(item), upsert=True)
log.msg("Question added to MongoDB database!",
level=log.DEBUG, spider=spider)
return item
為簡單起見,我們沒有優(yōu)化查詢,也沒有處理索引值,因為這不是一個生產(chǎn)環(huán)境。
|