摘要:介紹了使用Scrapy進行雙向爬取(對付分類信息網站)的方法。html
所謂的雙向爬取是指如下這種狀況,我要對某個生活分類信息的網站進行數據爬取,譬如要爬取租房信息欄目,我在該欄目的索引頁看到以下頁面,此時我要爬取該索引頁中的每一個條目的詳細信息(縱向爬取),而後在分頁器裏跳轉到下一頁(橫向爬取),再爬取第二頁中的每一個條目的詳細信息,如此循環,直至最後一個條目。git
這樣來定義雙向爬取:github
水平方向 – 從一個索引頁到另外一個索引頁web
純直方向 – 從一個索引頁到條目詳情頁shell
在本節中,express
提取索引頁到下一個索引頁的xpath爲:'//*[contains(@class,"next")]//@href'app
提取索引頁到條目詳情頁的xpath爲:'//*[@itemprop="url"]/@href'dom
manual.py文件的源代碼地址:scrapy
把以前的basic.py文件複製爲manual.py文件,並作如下修改:
導入Request:from scrapy.http import Request
修改spider的名字爲manual
更改starturls爲'http://web:9312/properties/index00000.html'
將原料的parse函數更名爲parse_item,並新建一個parse函數,代碼以下:
#本函數用於提取索引頁中每一個條目詳情頁的超連接,以及下一個索引頁的超連接 def parse(self, response): # Get the next index URLs and yield Requests next_selector = response.xpath('//*[contains(@class,"next")]//@href') for url in next_selector.extract(): yield Request(urlparse.urljoin(response.url, url))#Request()函數沒有賦值給callback,就會默認回調函數就是parse函數,因此這個語句等價於 yield Request(urlparse.urljoin(response.url, url), callback=parse) # Get item URLs and yield Requests item_selector = response.xpath('//*[@itemprop="url"]/@href') for url in item_selector.extract(): yield Request(urlparse.urljoin(response.url, url), callback=self.parse_item)
若是直接運行manual,就會爬取所有的頁面,而如今只是測試階段,能夠告訴spider在爬取一個特定數量的item以後就中止,經過參數:-s CLOSESPIDER_ITEMCOUNT=10
運行命令:$ scrapy crawl manual -s CLOSESPIDER_ITEMCOUNT=10
它的輸出以下:
spider的運行流程是這樣的:首先對start_url中的url發起一個request,而後下載器返回一個response(該response包含了網頁的源代碼和其餘信息),接着spider自動將response做爲parse函數的參數並調用。
parse函數的運行流程是這樣的:
1. 首先從該response中提取class屬性中包含有next字符的標籤(就是分頁器裏的「下一頁」)的超連接,在第一次運行時是:'index_00001.html'。
2. 在第一個for循環裏首先構建一個完整的url地址(’http://web:9312/scrapybook/properties/index_00001.html'),把該url做爲參數構建一個Request對象,並把該對象放入到一個隊列中(此時該對象是隊列的第一個元素)。
3. 繼續在該respone中提取屬性itemprop等於url字符的標籤(每個條目對應的詳情頁)的超連接(譬如:'property_000000.html')。
4. 在第二個for循環裏對提取到的url逐個構建完整的url地址(譬如:’http://web:9312/scrapybook/properties/ property_000000.html’),並使用該url做爲參數構建一個Request對象,按順序將對象放入到以前的隊列中。
5. 此時的隊列是這樣的
Request(http://…index_00001.html) |
Request(http://…property_000000.html) |
… |
Request(http://…property_000029.html) |
6. 當把最後一個條目詳情頁的超連接(property_000029.html)放入隊列後,調度器就開始處理這個隊列,由後到前把隊列的最後一個元素提取出來放入下載器中下載並把response傳入到回調函數(parse_item)中處理,直至到了第一個元素(index_00001.html),由於沒有指定回調函數,默認的回調函數是parse函數自己,此時就進入了步驟1,此次提取到的超連接是:'index_00002.html',而後就這樣循環下去。
這個parse函數的執行過程相似於這樣:
next_requests = [] for url in... next_requests.append(Request(...)) for url in... next_requests.append(Request(...)) return next_requests
能夠看到使用後進先出隊列的最大好處是在處理一個索引頁時立刻就開始處理該索引頁裏的條目列表,而不用維持一個超長的隊列,這樣能夠節省內存,有沒有以爲上面的parse函數寫得有些讓人難以理解呢,其實能夠換一種更加簡單的方式,對付這種雙向爬取的狀況,可使用crawl的模板。
首先在命令行裏按照crawl的模板生成一個名爲easy的spider
$ scrapy genspider -t crawl easy web
打開該文件
... class EasySpider(CrawlSpider): name = 'easy' allowed_domains = ['web'] start_urls = ['http://www.web/'] rules = ( Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True), ) def parse_item(self, response): ...
能夠看到自動生成了上面的那些代碼,注意這個spider是繼承了CrawlSpider類,而CrawlSpider類已經默認提供了parse函數的實現,因此咱們並不須要再寫parse函數,只須要配置rules變量便可
rules = ( Rule(LinkExtractor(restrict_xpaths='//*[contains(@class,"next")]')), Rule(LinkExtractor(restrict_xpaths='//*[@itemprop="url"]'), callback='parse_item') )
運行命令:$ scrapy crawl easy -s CLOSESPIDER_ITEMCOUNT=90
這個方法有如下不一樣之處:
這兩個xpath與以前使用的不一樣之處在於沒有了a和href這兩個約束字符,由於LinkExtrator是專門用來提取超連接的,因此會自動地提取標籤中的a和href的值,固然能夠經過修改LinkExtrator函數裏的參數tags和attrs來提取其餘標籤或屬性裏的超連接。
還要注意的是這裏的callback的值是字符串,而不是函數的引用。
Rule()函數裏設置了callback的值,spider就默認不會跟蹤目標頁裏的其餘超連接(意思是說,不會對這個已經爬取過的網頁使用xpaths來提取信息,爬蟲到這個頁面就終止了)。若是設置了callback的值,也能夠經過設置參數follow的值爲True來進行跟蹤,也能夠在callback指定的函數裏return/yield這些超連接。
在上面的縱向爬取過程當中,在索引頁的每個條目的詳情頁都分別發送了一個請求,若是你對爬取效率要求很高的話,那就得換一個思路了。不少時候在索引頁中對每個條目都作了簡介,雖然信息並無詳情頁那麼全,但若是你追求很高的爬取效率,那麼就不能逐個訪問條目的詳情頁,而是直接從索引頁中獲取條目的信息。因此,你要平衡好效率與信息質量之間的矛盾。
再次觀察索引頁,其實能夠發現每一個條目的節點都使用了itemptype=」http://schema.org/Product」來標記,因而直接從這些節點中獲取條目信息。
使用scrapy shell工具來再次分析索引頁:
scrapy shell http://web:9312/properties/index_00000.html
上圖中的每個Selector都指向了一個條目,這些Selector也是能夠用xpath來解析的,如今就要循環解析着30個Selector,從中提取條目的各類信息
fast.py源文件地址:
將manual.py文件複製並重命名爲fast.py,作如下修改:
將spider名稱修改成fast
修改parse函數,以下
def parse(self, response): # Get the next index URLs and yield Requests,這部分並沒改變 next_sel = response.xpath('//*[contains(@class,"next")]//@href') for url in next_sel.extract(): yield Request(urlparse.urljoin(response.url, url)) # Iterate through products and create PropertiesItems,改變的是這裏 selectors = response.xpath( '//*[@itemtype="http://schema.org/Product"]') for selector in selectors: # 對selector進行循環 yield self.parse_item(selector, response)
#有幾點變化: #一、xpath表達式裏所有用了一個點號開頭,由於這是在selector裏面提取信息,因此這個一個相對路徑的xpath表達式,這個點號表明了selector #二、ItemLoader函數裏用了selector變量,而不是response變量 def parse_item(self, selector, response): # Create the loader using the selector l = ItemLoader(item=PropertiesItem(), selector=selector) # Load fields using XPath expressions l.add_xpath('title', './/*[@itemprop="name"][1]/text()', MapCompose(unicode.strip, unicode.title)) l.add_xpath('price', './/*[@itemprop="price"][1]/text()', MapCompose(lambda i: i.replace(',', ''), float), re='[,.0-9]+') l.add_xpath('description', './/*[@itemprop="description"][1]/text()', MapCompose(unicode.strip), Join()) l.add_xpath('address', './/*[@itemtype="http://schema.org/Place"]' '[1]/*/text()', MapCompose(unicode.strip)) make_url = lambda i: urlparse.urljoin(response.url, i) l.add_xpath('image_urls', './/*[@itemprop="image"][1]/@src', MapCompose(make_url)) # Housekeeping fields l.add_xpath('url', './/*[@itemprop="url"][1]/@href', MapCompose(make_url)) l.add_value('project', self.settings.get('BOT_NAME')) l.add_value('spider', self.name) l.add_value('server', socket.gethostname()) l.add_value('date', datetime.datetime.now()) return l.load_item()
運行spider:scrapy crawl fast –s CLOSESPIDER_PAGECOUNT=10
能夠看到,爬取了300個item,卻只發送了10個request(由於在命令裏指定了只爬取10個頁面),效率提升了不少