爬蟲篇 | Python學習之Scrapy-Redis實戰京東圖書

  • 2019 年 11 月 1 日
  • 筆記

什麼是scrapy-redis?

redis-based components for scrapy

scrapy-Redis就是結合了分佈式數據庫redis,重寫了scrapy一些比較關鍵的代碼,將scrapy變成一個可以在多個主機上同時運行的分佈式爬蟲。

scrapy-redis是github上的一個開源項目,可以直接下載到他的源代碼:

https://github.com/rolando/scrapy-redis

scrapy-redis 詳解

scrapy-redis流程圖

redis的使用

參考前文寫的redis交互使用:Python | Python學習之Redis交互詳解

scrapy-redis example-project

scrapy-redis的源碼中提供了scrapy-redis的示例項目,我們下載下來學習一下。

https://github.com/rmax/scrapy-redis/tree/master/example-project

example-project中有三個demo,分別是dmoz,myspider_redis,以及mycrawler_redis

本次主要是對dmoz這個demo進行學習和實戰練習。

dmoz spider文件解析
from scrapy.linkextractors import LinkExtractor  from scrapy.spiders import CrawlSpider, Rule      class DmozSpider(CrawlSpider):      """Follow categories and extract links."""      name = 'dmoz'      allowed_domains = ['dmoztools.net']      start_urls = ['http://dmoztools.net/']        rules = [          Rule(LinkExtractor(              restrict_css=('.top-cat', '.sub-cat', '.cat-item')          ), callback='parse_directory', follow=True),      ]        def parse_directory(self, response):          for div in response.css('.title-and-desc'):              yield {                  'name': div.css('.site-title::text').extract_first(),                  'description': div.css('.site-descr::text').extract_first().strip(),                  'link': div.css('a::attr(href)').extract_first(),              }

可以看到,dmoz項目和我們平時創建的scrapy項目並沒有太大的區別,之所以能夠實現持久化爬蟲主要的不同之處在setting中設置了去重類和scheduler隊列。

dmoz setting文件解析

上面提到的setting中設置了去重類和scheduler隊列的操作主要就是在setting文件中添加下面這些代碼。

# 去重類--指定哪個去重方法給request對象去重  DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"  # 隊列--指定scheduler隊列,調度器內存的是待爬取鏈接和已爬取對象指紋。  SCHEDULER = "scrapy_redis.scheduler.Scheduler"  # 隊列內容是否持久化保存--為False的時候,關閉redis的時候清空redis  SCHEDULER_PERSIST = True  REDIS_URL="redis://127.0.0.1:6379"

dmoz redis 數據庫存取項

我們運行一下這個示例項目,並打開redis數據庫,查看爬取到的結果。redis數據庫中出現以下三個鍵,分別是:

dmoz request:待爬取項

(先把爬取對象序列化存入數據庫,再反序列化成爬取對,Scheduler隊列,存放的待請求的request對象,獲取的過程是pop操作,即獲取一個會去除一個)

dmoz items:爬取的內容

(通過scrapy_redis.pipelines.RedisPipeline保存,屏蔽之後可以實現自定義對象存取位置,存放的獲取到的item信息,在pipeline中開啟RedisPipeline才會存入)

dmoz dumpfilter:抓到過的request對象指紋

(指紋集合,存放的是已經進入scheduler隊列的request對象的指紋,指紋默認由請求方法,url和請求體組成)

dumpfilter的數量減去request的數量是已經抓爬取過的數量

關閉redispipeline之後,redis數據庫中數據量變化:

  • dmoz:requests 有變化(變多或者變少或者不變)
  • dmoz:dupefilter 變多
  • dmoz:items 不變

redispipeline中僅僅實現了item數據存儲到redis的過程,我們可以新建一個pipeline(或者修改默認的ExamplePipeline),讓數據存儲到任意地方,但是權重應該小於redis存儲的pipline。

scrapy-redis 源碼詳解

scrapy redis 如何生成指紋的?
import hashlib  f = hashlib.hsa1()  f.update(url.encode())  f.hexdigest()
scrapy-redis 判斷request對象是否入隊
def enqueue_request(self, request):      if not request.dont_filter and self.df.request_seen(request):        # dont_filter=False Ture  True request指紋已經存在  #不會入隊        # dont_filter=False Ture  False  request指紋已經存在 全新的url  #會入隊        # dont_filter=Ture False  #會入隊          self.df.log(request, self.spider)          return False      if self.stats:          self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)      self.queue.push(request)      return True
  • dont_filter = True ,構造請求的時候,把dont_filter置為True,該url會被反覆抓取(url地址對應的內容會更新的情況)
  • 一個全新的url地址被抓到的時候,構造request請求
  • url地址在start_urls中的時候,會入隊,不管之前是否請求過
  • 構造start_url地址的請求時候,dont_filter = True
scrapy-redis如何去重
fp = hashlib.sha1()  fp.update(to_bytes(request.method)) #請求方法  fp.update(to_bytes(canonicalize_url(request.url)))  #請求鏈接  fp.update(request.body or b'')  # 請求體  return fp.hexdigest()
  • 使用sha1加密request得到指紋
  • 把指紋存在redis的集合中
  • 下一次新來一個request,同樣的方式生成指紋,判斷指紋是否存在reids的集合中
判斷數據是否存在redis的集合中,不存在插入
added = self.server.sadd(self.key, fp)  return added != 0

scrapy-redis實戰京東圖書

爬取結果截圖

頁面分析

分析分類聚合頁

打開待爬取頁面:

https://book.jd.com/booksort.html

如下圖:

分析分類聚合頁

查看頁面源代碼,發現待爬取的內容存在其中,所以我們可以通過分析源碼寫出提取相應字段的xpath。

    def parse(self, response):          dl_list = response.xpath("//div[@class='mc']/dl/dt")          for dl in dl_list:              item = JdbookspiderItem()              item['book_sort'] = dl.xpath("./a/text()").extract_first()              em_list = dl.xpath("./following-sibling::dd/em")              for em in em_list:                  item['book_cate'] = em.xpath("./a/text()").extract_first()                  item['book_cate_url'] = em.xpath("./a/@href").extract_first()                  if item['book_cate_url'] is not None:                      item['book_cate_url'] = 'https:' + item['book_cate_url']                  yield scrapy.Request(                      item['book_cate_url'],                      callback=self.parse_cate_url,                      meta={"item": deepcopy(item)}                  )

通過抓取分類頁面分類鏈接,我們可以爬取到分類下的書籍列表頁,這個頁面包含書籍的全部信息,同樣是可以使用xpath解析出來的。

分析書籍列表頁

通過分析列表頁的請求,可以發下列表頁的請求重定向到了一個新的鏈接,所以我們只需要分析新請求鏈接的響應即可,scrapy可以自動幫我們執行重定向的操作。

頁面重定向分析

分析書籍列表頁,可以發現列表頁除了價格字段外其餘字段都可以在鏈接的響應中提取到。

書籍列表頁分析

所以我們只需要找到價格字段的請求,就可以爬取到書籍的全部字段了。我們可以直接在相應中查找價格以查看是否有相關的響應。

書籍價格分析1

查找結果如上所示,根據我們查找到的結果,我試着請求一下對應的鏈接,很幸運這裡返回的是json字符串。

書籍價格分析2

根據以上分析我們可以寫出相應的分析代碼:

    def parse_cate_url(self, response):          item = response.meta["item"]          li_list = response.xpath("//div[@id='plist']/ul/li")          for li in li_list:              item['book_img'] = li.xpath(".//div[@class='p-img']//img/@src").extract_first()              if item['book_img'] is None:                  item['book_img'] = li.xpath(".//div[@class='p-img']//img/@data-lazy-img").extract_first()              item['book_img'] = "https:" + item['book_img'] if item['book_img'] is not None else None              item['book_name'] = li.xpath(".//div[@class='p-name']/a/em/text()").extract_first().strip()              item['book_author'] = li.xpath(".//span[@class='author_type_1']/a/text()").extract_first()              item['publish_time'] = li.xpath(".//span[@class='p-bi-date']/text()").extract_first().strip()              item['book_store'] = li.xpath(".//span[@class='p-bi-store']/a/@title").extract_first().strip()              item['book_sku'] = li.xpath("./div/@data-sku").extract_first()              yield scrapy.Request(                  'https://p.3.cn/prices/mgets?skuIds=J_{}'.format(item['book_sku']),                  callback=self.parse_book_price,                  meta={"item": deepcopy(item)}              )      def parse_book_price(self, response):          item = response.meta["item"]          item['book_price'] = json.loads(response.body.decode())[0]["op"]          yield item
構建下一頁請求

這裡就比較簡單,對比最後一頁與其他頁,發現最後一頁是沒有下一頁鏈接的,根據以上分析可以成功構建相應代碼:

# 下一頁地址構建          next_url = response.xpath("//a[@class='pn-next']/@href")          if next_url:              next_url = urllib.parse.join(response.url, next_url)              yield scrapy.Request(                  next_url,                  callback=self.parse_cate_url,                  meta={"item": item}              )
數據入庫
class JdbookspiderPipeline(object):      def process_item(self, item, spider):          if isinstance(item, JdbookspiderItem):              print(item)              collection.insert(dict(item))          return item

實現持久化爬蟲

在setting文件中添加去重類和scheduler隊列,同時修改redis數據庫鏈接,並保證數據庫是可用的。

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"  SCHEDULER = "scrapy_redis.scheduler.Scheduler"  SCHEDULER_PERSIST = True  REDIS_URL = "redis://127.0.0.1:6379"    # 同時為保證能夠把數據同時存儲到mongodb中我們還要設置相關pipline  ITEM_PIPELINES = {     'jdbookSpider.pipelines.JdbookspiderPipeline': 500,     'scrapy_redis.pipelines.RedisPipeline': 400  }