摘要:介紹了使用Scrapy進行雙向爬取(對付分類信息網站)的方法。
所謂的雙向爬取是指以下這種情況,我要對某個生活分類信息的網站進行數據爬取,譬如要爬取租房信息欄目,我在該欄目的索引頁看到如下頁面,此時我要爬取該索引頁中的每個條目的詳細信息(縱向爬取),然后在分頁器里跳轉到下一頁(橫向爬取),再爬取第二頁中的每個條目的詳細信息,如此循環,直至最后一個條目。
這樣來定義雙向爬取:
-
水平方向 – 從一個索引頁到另一個索引頁
-
純直方向 – 從一個索引頁到條目詳情頁
在本節中,
提取索引頁到下一個索引頁的xpath為:'//*[contains(@class,"next")]//@href'
提取索引頁到條目詳情頁的xpath為:'//*[@itemprop="url"]/@href'
manual.py文件的源代碼地址:
把之前的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)
- 修改parse_item函數如下
#有幾點變化: #1、xpath表達式里全部用了一個點號開頭,因為這是在selector里面提取信息,所以這個一個相對路徑的xpath表達式,這個點號代表了selector #2、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個頁面),效率提高了很多