scrapy進階,組合多請求抓取Item利器ItemCollector詳解!

ItemCollector是我編寫的二次開發框架struture-spider的核心組件,之前寫過幾篇文章介紹過struture-spider的使用方法。

往期回顧:

夏洛之楓:個性化爬蟲一鍵生成,想抓哪裡點哪裡!zhuanlan.zhihu.com圖標夏洛之楓:[structure_spider每周一練]:一鍵下載百度mp3!zhuanlan.zhihu.com圖標夏洛之楓:使用structure_spider多請求組合抓取結構化數據zhuanlan.zhihu.com圖標

顧明思義,structure-spider即為結構化爬蟲。而之所以稱之為結構,是因為它是用來解決一個網頁中多個不同請求如何按特定層次結構組合的問題。

例如,把豆瓣中的一部電影作為一個項目,我們可以直接從網頁中獲取標題,電影規格,評分,簡介等。而如果要更進一步抓取全部影人和圖片時。我們需要點擊全部再次發送請求來獲取。如果到此而止的話,可能事情還不會變的太複雜。正如scrapy中所建議的那樣:

通過meta傳遞item,例如:

def page_parser(self, response): sites = hxs.select(//div[@class="row"]) items = [] request = Request("http://www.example.com/lin1.cpp", callback=self.parseDescription1) request.meta[item] = item yield request request = Request("http://www.example.com/lin1.cpp", callback=self.parseDescription2, meta={item: item}) yield request yield Request("http://www.example.com/lin1.cpp", callback=self.parseDescription3, meta={item: item})def parseDescription1(self,response): item = response.meta[item] item[desc1] = "test" return itemdef parseDescription2(self,response): item = response.meta[item] item[desc2] = "test2" return itemdef parseDescription3(self,response): item = response.meta[item] item[desc3] = "test3" return item

但上述代碼會產生3個item,只有最後一個item會擁有完整信息。

或者這樣:

def page_parser(self, response): sites = hxs.select(//div[@class="row"]) items = [] request = Request("http://www.example.com/lin1.cpp", callback=self.parseDescription1) request.meta[item] = Item() return [request] def parseDescription1(self,response): item = response.meta[item] item[desc1] = "test" return [Request("http://www.example.com/lin2.cpp", callback=self.parseDescription2, meta={item: item})] def parseDescription2(self,response): item = response.meta[item] item[desc2] = "test2" return [Request("http://www.example.com/lin3.cpp", callback=self.parseDescription3, meta={item: item})] def parseDescription3(self,response): item = response.meta[item] item[desc3] = "test3" return [item]

這樣的確產生了一個Item,但是鏈接都被硬編寫到每個回調函數,真實的生產環境中,鏈接肯定是分散在各個請求中的,你肯定不會寫這樣的代碼。

上述例子來源:How can i use multiple requests and pass items in between them in scrapy python

更極端一點的情況可能還會這樣:

豆瓣電影主頁面下面擁有全部問題的鏈接,全部問題鏈接頁面擁有每個問題的鏈接,每個問題的鏈接裡面擁有提問者的問題詳情以及回答者們無私的幫助,如果回答者太熱情可能還需要翻頁,同時回答者有可能還有其回復者。這還沒有考慮與問題平行的短評,影評。天哪,組合這麼多請求,最後返回一個電影全部的結構層次,管理這麼多請求及其Item,會是多麼糟糕的一種體驗!

但是,現在不同了,我們有了item_collector。

item_collector講解

item_collector是一個模塊,包含了ItemCollector及Node類。代碼如下:

import copyfrom scrapy import Itemfrom .custom_request import Requestclass Node(object): """ ItemCollector樹的節點 """ def __init__(self, prop_name, item_loader, req_meta, enricher=None, parent=None, spider=None): self.prop_name = prop_name self.item_loader = item_loader self.req_meta = req_meta if not enricher: enricher = "enrich_" + prop_name self.enricher = enricher if isinstance(enricher, str) else enricher.__name__ self.parent = parent self.children = list() self.enriched = False # 與父節點共用item_loader的節點不會在完成時生成item。 if self.parent and self.parent.item_loader == item_loader: self.do_not_load = True else: self.do_not_load = False def run(self, response, spider): """ 除根節點以外,每個節點會經過兩次run的調用。 第一次run會首先調用enrich方法。豐富其item_loader,併產生子節點。 兩次run調用都會調用dispatch方法。 :param response: :param spider: :return: """ if not self.enriched: children = getattr(spider, self.enricher)(self.item_loader, response) self.enriched = True if children: self.children.extend(Node(*child, parent=self, spider=spider) for child in children) return self.dispatch(response) def dispatch(self, response): """ dispatch會調度產生請求/item/None 噹噹前節點存在req_meta時,組建請求。 噹噹前節點不存在req_meta時,遍歷其子節點,查找req_meta,並調用子節點dispatch,返回其結果 刪除不存在req_meta的子節點 :param response: :return: """ if self.req_meta: meta = response.request.meta.copy() self.req_meta.pop("callback", None) self.req_meta.pop("errback", None) custom_meta = self.req_meta.pop("meta", {}) meta["priority"] += 1 meta.update(custom_meta) kw = copy.deepcopy(self.req_meta) self.req_meta.clear() return Request(meta=meta, callback="parse_next", errback="errback", **kw), self else: for child in self.children[:]: if child.req_meta: return child.dispatch(response) else: self.children.remove(child) return None if self.do_not_load else self.item_loader.load_item(), self def __str__(self): return "<Node prop_name: {}, item_loader: {}, enricher: {} >" .format( self.prop_name, self.item_loader, self.enricher) __repr__ = __str__class ItemCollector(object): """ ItemCollector: """ def __init__(self, root): self.root = root self.current_node = root def collect(self, response, spider): """ item_collector收集的開始,每次收集返回一個請求或者item。 調用當前節點的run方法,返回一個請求/item/None及其被產生的節點(哪個節點產生了這個請求/item/None)。 當返回為Item時,表示該節點及其子節點已經完成所有操作。將其賦值父節點的item_loader。 當返回為Request時,表示該節點產生了一個新請求,返回該請求交付scrapy調度。 當返回為None時,表示該節點已完成,但與其父節點共用item_loader,此時item_loader不會生成item。將當前節點指針指向其父節點。 :param response: :param spider: :return: """ req_or_item = None while not req_or_item: req_or_item, self.current_node = self.current_node.run(response, spider) if isinstance(req_or_item, Item): # 存在parent,置req_or_item為None,循環直到遇見一個request,否則跳出循環返回Item。 if self.current_node.parent: self.current_node.parent.item_loader.add_value(self.current_node.prop_name, req_or_item) req_or_item = None self.current_node = self.current_node.parent elif req_or_item is None: self.current_node = self.current_node.parent return req_or_item

除去注釋不到60行,非常簡單。

先給出一些定義:

節點:每次請求對應一個節點,如豆瓣電影的主頁面,對應的根節點,每個節點由一個屬性名(根節點為None,一些列表頁請求節點會使用一個屬性名來做為標識),Item_loader,enricher,req_meta(根節點由於是在請求結束之後產生的,所以此屬性值為空)。節點可以有子節點,也可以有父節點。

  • 屬性名:每個結點最終有可能會做為一個Item添加到父節點item_loader中,此屬性為父Item中定義的屬性。同時屬性名還可以用來組成enricher的名字。
  • item_loader:scrapy中的Item_loader,對應一個item。
  • enricher:enricher是一個回調方法,定義在spider中,每次請求結束後,傳入(item_loader, response)調用,用來豐富item。
  • req_meta:當前節點即將發出的請求元屬性。Request的參數集。

全部方法除去雙下方法只有三個:

ItemCollector.collect:

  • item_collector收集的開始,每次收集返回一個請求或者item。調用當前節點的run方法,返回一個請求/item/None及其被產生的節點(哪個節點產生了這個請求/item/None)。
  1. 當返回為Item時,表示該節點及其子節點已經完成所有操作。將其賦值給父節點的item_loader。
  2. 當返回為Request時,表示該節點產生了一個新請求,返回該請求交付scrapy調度。
  3. 當返回為None時,表示該節點已完成,但與其父節點共用item_loader,此時item_loader不會生成item。
  4. 1,3兩種情況將當前節點指針指向其父節點。

Node.run:

  • 除根節點以外,每個節點會經過兩次run的調用。
  • 第一次run會首先調用enrich方法。豐富其item_loader,併產生子節點。
  • 兩次run調用都會調用dispatch方法。

Node.dispatch:

  • dispatch會調度產生請求/item/None。
  • 噹噹前節點存在req_meta時,組建請求。
  • 噹噹前節點不存在req_meta時,遍歷其子節點,查找req_meta,並調用子節點dispatch,返回其結果及當前節點
  • 刪除不存在req_meta的子節點

item_collector的使用方法

首先我們需要定義兩個parse方法,parse方法為實際的Request.callback。

第一個parse方法為入口函數,產生item_collector。方法名稱隨意。

一個最簡版大概是這樣的:

def parse_item(self, response): base_loader = ItemLoader(item=Myitem()) response.request.meta["item_collector"] = ItemCollector(Node(None, base_loader, None, self.enrich_data)) yield item_collector.collect(response, self)

其中self.enrich_data為最初的enricher,大概會做以下操作

@enrich_wrapper def enrich_data(self, item_loader, response): self.logger.debug("Start to enrich_data. ") item_loader.add_value("id", response.url, re=r"subject/(d+)/") item_loader.add_xpath("title", //h1/span/text()) item_loader.add_xpath("info", //div[@id="info"]) item_loader.add_xpath("score", //strong[@class="ll rating_num"]/text()) item_loader.add_xpath("recommendations", //div[@class="recommendations-bd"]/dl/dd/a/text()) item_loader.add_xpath("description", //div[@id="link-report"]/span/text()) nodes = list() celebrities_url = xpath_exchange(response.xpath(//div[@id="celebrities"]/h2/span/a/@href)) if celebrities_url: nodes.append(("celebrities", item_loader, {"url": response.urljoin(celebrities_url)})) related_pic_urls = response.xpath(//div[@id="related-pic"]/h2/span[@class="pl"]/a/@href).extract() if len(related_pic_urls) >= 2: related_pic_url = related_pic_urls[-2] else: related_pic_url = "" if related_pic_url: nodes.append(("related_pics", item_loader, {"url": related_pic_url})) comments_url = xpath_exchange(response.xpath(//div[@id="comments-section"]//h2/span/a/@href)) if comments_url: nodes.append(("comments", item_loader, {"url": comments_url})) question_list_url = xpath_exchange(response.xpath(//div[@id="askmatrix"]/div/h2/span/a/@href)) if question_list_url: nodes.append(("question_list", item_loader, {"url": question_list_url})) review_list_url = xpath_exchange(response.xpath(//section[@class="reviews mod movie-content"]//h2/span/a/@href)) if review_list_url: nodes.append(("review_list", item_loader, {"url": response.url.split("?")[0] + review_list_url})) return nodes

為item_loader進行了豐富操作,並最終返回了一個三元組列表。

次級請求統一的parse方法大概是這個樣子

def parse_next(self, response): yield response.meta["item_collector"].collect(response, self)

是的,非常簡單。就一行,甚至你可以將collect方法參數對調後直接使用。

而相應的enricher會是這樣的

@enrich_wrapper def enrich_related_pics(self, item_loader, response): self.logger.debug("Start to enrich_related_pics. ") types = response.xpath(//div[@class="article"]/div[@class="mod"]) related_pics = dict() for type in types: related_pics[re_search(r"(w+)", xpath_exchange(type.xpath(div[@class="hd"]/h2/text())))] = type.xpath(div[@class="bd"]/ul/li/a/img/@src).extract() item_loader.add_value("related_pics", related_pics)

僅僅豐富了item_loader

還有這樣的

@enrich_wrapper def enrich_question_list(self, item_loader, response): self.logger.debug("Start to enrich_question_list. ") nodes = list() next_url = xpath_exchange(response.xpath(//span[@class="next"]/a/@href)) if next_url: nodes.append(("question_list", item_loader, {"url": response.urljoin(next_url)})) qustions = response.xpath(//div[@class="questions"]/div[@class="item"]) for question in qustions: nodes.append(("questions", CustomLoader(item=QuestionItem()), {"url": xpath_exchange(question.xpath(h3/a/@href))})) return nodes

沒有對item_loader進行操作,但是返回了三元組列表。

當然,豐富item_loader的同時返回三元組表也可行的。

@enrich_wrapper def enrich_questions(self, item_loader, response): self.logger.debug("Start to enrich_questions. ") item_loader.add_xpath("title", //div[@class="article"]/h1/text()) item_loader.add_xpath("content", //div[@id="question-content"]/p/text()) item_loader.add_xpath("author", //div[@class="article"]/p[@class="meta"]/a/text()) item_loader.add_xpath("datetime", //div[@class="article"]/p[@class="meta"]/text(), lambda values: values[-1]) answer_url = response.url.split("?")[0] + "answers/?start=0&limit=20" return [("answers", item_loader, {"url": answer_url})]

等所有node執行完畢,一個完成的item就會被返回了。

很簡單,不是嗎?

struture-spider就是在其基礎之上進行了封裝。

使用流程如下:

  • 創建spider繼承自StructureSpider
  • 提供get_base_loader方法,返回最頂層item_loader
  • 提供enrich_data函數,進行item_loader豐富操作及返回三元組列表
  • 若有返回三元組代表,繼續提供enrich_`prop_name`方法。
  • 運行spider.

今天的內容基本就這些了,講了item_collector的實現以及用法。structure-spider的用例我之前寫了很多了在此不再贅述。請關注github。

ShichaoMa/structure_spidergithub.com圖標
推薦閱讀:

Python入門
高德API+Python解決租房問題
有哪些值得推薦的 Python 開發工具?
學會python對你的生活產生什麼樣的改變?

TAG:架構 | scrapy | Python |