1. Scrapy通用爬蟲
通過Scrapy,我們可以輕松地完成一個站點爬蟲的編寫。但如果抓取的站點量非常大,比如爬取各大媒體的新聞信息,多個Spider則可能包含很多重復代碼。
如果我們將各個站點的Spider的公共部分保留下來,不同的部分提取出來作為單獨的配置,如爬取規則、頁面解析方式等抽離出來做成一個配置文件,那么我們在新增一個爬蟲的時候,只需要實現這些網站的爬取規則和提取規則即可。
這一章我們就來了解下Scrapy通用爬蟲的實現方法。
1.1 CrawlSpider
在實現通用爬蟲之前,我們需要先了解一下CrawlSpider,官方文檔:https://scrapy.readthedocs.io/en/latest/topics/spiders.html#crawlspider。
CrawlSpider是Scrapy提供的一個通用Spider。在Spider里,我們可以指定一些爬取規則來實現頁面的提取,這些爬取規則由一個專門的數據結構Rule表示。Rule里包含提取和跟進頁面的配置,Spider會根據Rule來確定當前頁面中的哪些鏈接需要繼續爬取、哪些頁面的爬取結果需要用哪個方法解析等。
CrawlSpider繼承自Spider類。除了Spider類的所有方法和屬性,它還提供了一個非常重要的屬性和方法。
rules,它是爬取規則屬性,是包含一個或多個Rule對象的列表。每個Rule對爬取網站的動作都做了定義,CrawlSpider會讀取rules的每一個Rule並進行解析。
parse_start_url(),它是一個可重寫的方法。當start_urls里對應的Request得到Response時,該方法被調用,它會分析Response並必須返回Item對象或者Request對象。
這里最重要的內容莫過於Rule的定義了:
Rule參數:
- link_extractor:是Link Extractor對象。通過它,Spider可以知道從爬取的頁面中提取哪些鏈接。提取出的鏈接會自動生成Request。它又是一個數據結構,一般常用LxmlLinkExtractor對象作為參數。
allow是一個正則表達式或正則表達式列表,它定義了從當前頁面提取出的鏈接哪些是符合要求的,只有符合要求的鏈接才會被跟進。deny則相反。allow_domains定義了符合要求的域名,只有此域名的鏈接才會被跟進生成新的Request,它相當於域名向名單。deny_domains則相反,相當於域名黑名單。restrict_xpaths定義了從當前頁面中XPath匹配的區域提取鏈接,其值是XPath表達式或XPath表達式列表。restrict_css定義了從當前頁面中css選擇器匹配的區域提取鏈接,其值是css選擇器或css選擇器列表還有一些其他參數代表了提取鏈接的標簽、是否去重、鏈接的處理等內容,使用的頻率不高。
- callback:即回調函數,和之前定義Request的callback有相同的意義。每次從link_extractor中獲取到鏈接時,該函數將會調用。該回調函數接收一個response作為其第一個參數,並返回一個包含Item或Request對象的列表。注意,避免使用pars()作為回調函數。由於CrawlSpider使用parse()方法來實現其邏輯,如果parse()方法覆蓋了,CrawlSpider將會運行失敗。
- cb_kwargs:字典,它包含傳遞給回調函數的參數。
- follow:布爾值,即True或False,它指定根據該規則從response提取的鏈接是否需要跟進。如果 callback參數為None,follow默認設置為True,否則默認為False。
- process_links:指定處理函數,從link_extractor中獲取到鏈接列表時,該函數將會調用, 它主要用於過濾。
- process_request:同樣是指定處理函數,根據該Rule提取到每個Request時,該函數部會調用,對Request進行處理。該函數必須返回Request或者None。
以上內容便是CraefISpider中的核心Rule的基本用法。但這些內容可能還不足以完成一個CrawlSpider爬蟲。下面我們利用CrawlSpider實現新聞網站的爬取實例,來更好地理解Rule的用法。
1.2 Item Loader
我們了解了利用CrawlSpider的Rule來定義頁面的爬取邏輯,這是可配置化的一部分內容。但是,Rule並沒有對Item的提取方式做規則定義。對於Item的提取,我們需要借助另一個模塊Item Loader來實現。
Item Loader提供一種便捷的機制來幫助我們方便地提取Item。它提供的一系列API可以分析原始數據對Item進行賦值。Item提供的是保存抓取數據的容器,而Item Loader提供的是填充容器的機制。有了它,數據的提取會變得更加規則化。
Item LoaderAPI參數:
item:它是Item對象,可以調用add_xpath()、add_css()或add_value()等方法來填充Item對象。
selector:它是Selector對象,用來提取填充數據的選擇器。
response:它是Response對象,用於使用構造選擇器的Response。
實例:
from scrapy.loader import ItemLoader from scrapyDemo.items import Product
def parse(self, response):
loader = ItemLoader(item=Product(),response=response)
loader.add_xpath('name','//div[@class="product_name"]')
loader.add_xpath('name','//div[@class="product_title"]')
loader.add_xpath('price','//p[@id="price"]')
loader.add_css('stock','p#stock]')
loader.add_value('last_updated','today')
return loader.load_item()
這里首先聲明一個Product Item,用該Item和Response對象實例化Item Loader,調用add_xpath()方法把來向兩個不同位置的數據提取出來,分配給name屬性,再用add_xpath()、add_css()、add_value()等方法對不同屬性依次賦值,最后調用load_item()方法實現Item的解析。這種方式比較規則化,我們可以把一些參數和規則單獨提取出來做成配置文件或存到數據庫,即可實現可配置化。
另外,Item Loader每個字段中都包含了一個Input Processor(輸入處理器)和一個Output Processor(輸出處理器)。Input Processor收到數據時立刻提取數據,Input Processor的結果被收集起來並且保存在ltemLoader內,但是不分配給Item。收集到所有的數據后,load_item()方法被調用來填充再生成Item對象 。在調用時會先調用Output Processor來處理之前收集到的數據,然后再存入Item中,這樣就生成了Item。
內置的Processor:
(1) Identity
Identity是最簡單的Processor,不進行任何處理,直接返回原來的數據。
(2) TakeFirst
TakeFirst返回列表的第一個非空值,類似extract_first()的功能,常用作Output Processor。
from scrapy.loader.processors import TakeFirst processor = TakeFirst() print(processor(['',1,2,3]))
運行結果為1。
(3) Join
Join方法相當於字符串的join()方法,可以把列表並結成字符串,字符串默認使用空格分隔。
from scrapy.loader.processors import Join processor = Join() print(processor(['one','two','three']))
運行結果為one two three。
它也可以通過參數更改默認的分隔符,例如改成逗號:
from scrapy.loader.processors import Join processor = Join(',') print(processor(['one','two','three']))
運行結果為one,two,three。
(4) Compose
Compose是用給定的多個函數的組合而構造的Processor,每個輸入值被傳遞到第一個函數,其輸出再傳遞到第二個函數,依次類推,直到最后一個函數返回整個處理器的輸出。
from scrapy.loader.processors import Compose processor = Compose(str.upper,lambda s:s.strip()) print(processor('hello world'))
運行結果為HELLO WORLD。
在這里我們構造了一個Compose Processor,傳入一個開頭帶有空格的字符串。Compose Processor的參數有兩個:第一個是str.upper,它可以將字母全部轉為大寫;第二個是一個匿名函數,它調用strip()方法去除頭尾空白字符。Compose會順次調用兩個參數,最后返回結果的字符串全部轉化為大寫並且去除了開頭的空格。
(5) MapCompose
與Compose類似,MapCompose可以迭代處理一個列表輸入值。
from scrapy.loader.processors import MapCompose processor = MapCompose(str.upper,lambda s:s.strip()) print(processor(['Hello','World','Python']))
運行結果為['HELLO','WORLD','PYTHON']。
被處理的內容是一個可迭代對象,MapCompose會將該對象遍歷然后依次處理。
(6) SelectJmes
SelectJmes可以查詢JSON,傳入Key,返回查詢所得的Value。不過需要先安裝jmespath庫才可以使用它。
pip install jmespath
安裝好jmespath之后,便可以使用這個Processor了。
from scrapy.loader.processors import SelectJmes processor = SelectJmes('foo') print(processor({'foo':'bar'}))
運行結果為:bar。
以上內容便是一些常用的Processor,在本節的實例中我們會使用Processor來進行數據的處理。接下來,我們用一個實例來了解Item Loader的用法。
1.3 通用爬蟲案例
起點作為主流的小說網站,在防止數據采集反面還是做了准備的,其對主要的數字采用了自定義的編碼映射取值,想直接通過頁面來實現數據的獲取,是無法實現的。
單獨獲取數字還是可以實現的,通過requests發送請求,用正則去匹配字符元素,並再次匹配其映射關系的url,獲取到的數據通過font包工具解析成字典格式,再做編碼匹配,起點返回的編碼匹配英文數字,英文數字匹配阿拉伯數字,最后拼接,得到實際的數字字符串,但這樣多次發送請求,爬取效率會大大降低。本次集中爬取舍棄了爬取數字,選擇了較容易獲取的評分數字。評分值默認為0 ,是從后台推送的js數據里取值更新的。
但是這太復雜了,我沒空搞,現在寫博客都是擠出時間來寫。
1.3.1 新建項目
先新建Scrapy項目,名為qd。
scrapy startproject qd
創建一個CrawlSpider,需要先制定一個模板。我們可以先看看有哪些可用模版。
scrapy genspider -l
運行結果:
之前創建Spider的時候,我們默認使用了第一個模板basic。這次要創建CrawlSpider,就需要使用第二個模板crawl。
scrapy genspider -t crawl read qidian.com
運行之后便會生成一個CrawlSpider。
這次生成的Spider內容多了一個rules屬性的定義。Rule第一個參數是LinkExtractor,就是上文所說的LxmllinkExtractor,只是名稱不同。同時,默認的回調函數也不再是parse,而是parse_item。
1.3.2 定義Rule
要實現小說信息的爬取,我們需要做的就是定義好Rule,然后實現解析函數。下面我們就來一步步實現這個過程。
首先將start_urls修改為起始鏈接。
start_urls = ['https://www.qidian.com/all?orderId=&style=1&pageSize=20&siteid=1&pubflag=0&hiddenField=0&page=1']
之后,Spider爬取start_urls里面的每一個鏈接。所以這里第一個爬取的頁面就是我們剛才所定義的鏈接。得到Response之后,Spider就會根據每一個Rule來提取這個頁面內的超鏈接,去生成進一步的Request。接下來,我們就需要定義Rule來指定提取哪些鏈接。
這是起點小說網的列表頁,下一步自然就是將列表中的每一條小說詳情的鏈接提取出來。這里直接指定這些鏈接所在區域即可。
rules:
rules = ( #匹配全部主頁面的url規則 深度爬取子頁面 Rule(LinkExtractor(allow=(r'https://www.qidian.com/all\?orderId=\&style=1\&pageSize=20\&siteid=1\&pubflag=0\&hiddenField=0\&page=(\d+)')),follow=True), #匹配詳情頁面 不作深度爬取 Rule(LinkExtractor(allow=r'https://book.qidian.com/info/(\d+)'), callback='parse_item', follow=False), )
1.3.3 解析頁面
接下來我們需要做的就是解析頁面內容了,將書名、作者、狀態、類型、簡介、評分、故事、最新章節提取出來即可。首先定義一個Item。
# -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html from scrapy import Field,Item class QdItem(Item): # define the fields for your item here like: book_name = Field() #書名 author=Field() #作者 state=Field() #狀態 type=Field() #類型 about=Field() #簡介 score=Field() #評分 story=Field() #故事 news=Field() #最新章節
然后我創建了很多方法,在分別獲取這些信息。
def get_book_name(self,response): book_name=response.xpath('//h1/em/text()').extract()[0] if len(book_name)>0: book_name=book_name.strip() else: book_name='NULL' return book_name def get_author(self,response): author=response.xpath('//h1/span/a/text()').extract()[0] if len(author)>0: author=author.strip() else: author='NULL' return author def get_state(self,response): state=response.xpath('//p[@class="tag"]/span/text()').extract()[0] if len(state)>0: state=state.strip() else: st='NULL' return state def get_type(self,response): type=response.xpath('//p[@class="tag"]/a/text()').extract() if len(type)>0: t="" for i in type: t+=' '+i type=t else: type='NULL' return type def get_about(self,response): about=response.xpath('//p[@class="intro"]/text()').extract()[0] if len(about)>0: about=about.strip() else: about='NULL' return about def get_score(self,response): def get_sc(id): urll = 'https://book.qidian.com/ajax/comment/index?_csrfToken=ziKrBzt4NggZbkfyUMDwZvGH0X0wtrO5RdEGbI9w&bookId=' + id + '&pageSize=15' rr = requests.get(urll) # print(rr) score = rr.text[16:19] return score bid=response.xpath('//a[@id="bookImg"]/@data-bid').extract()[0] #獲取書的id if len(bid)>0: score=get_sc(bid) #調用方法獲取評分 若是整數 可能返回 9," if score[1]==',': score=score.replace(',"',".0") else: score=score else: score='NULL' return score def get_story(self,response): story=response.xpath('//div[@class="book-intro"]/p/text()').extract()[0] if len(story)>0: story=story.strip() else: story='NULL' return story def get_news(self,response): news=response.xpath('//div[@class="detail"]/p[@class="cf"]/a/text()').extract()[0] if len(news)>0: news=news.strip() else: news='NULL' return news
其他部分就沒什么變化了,就settings加入了請求頭:
DEFAULT_REQUEST_HEADERS = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv 11.0) like Gecko', }
1.3.4 運行程序
scrapy crawl read
運行結果:
1.3.5 完整代碼
read.py:
# -*- coding: utf-8 -*- from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from qd.items import QdItem import requests class ReadSpider(CrawlSpider): name = 'read' # allowed_domains = ['qidian.com'] start_urls = ['https://www.qidian.com/all?orderId=&style=1&pageSize=20&siteid=1&pubflag=0&hiddenField=0&page=1'] rules = ( #匹配全部主頁面的url規則 深度爬取子頁面 Rule(LinkExtractor(allow=(r'https://www.qidian.com/all\?orderId=\&style=1\&pageSize=20\&siteid=1\&pubflag=0\&hiddenField=0\&page=(\d+)')),follow=True), #匹配詳情頁面 不作深度爬取 Rule(LinkExtractor(allow=r'https://book.qidian.com/info/(\d+)'), callback='parse_item', follow=False), ) def parse_item(self, response): item=QdItem() item['book_name']=self.get_book_name(response) item['author']=self.get_author(response) item['state']=self.get_state(response) item['type']=self.get_type(response) item['about']=self.get_about(response) item['score']=self.get_score(response) item['story']=self.get_story(response) item['news']=self.get_news(response) yield item def get_book_name(self,response): book_name=response.xpath('//h1/em/text()').extract()[0] if len(book_name)>0: book_name=book_name.strip() else: book_name='NULL' return book_name def get_author(self,response): author=response.xpath('//h1/span/a/text()').extract()[0] if len(author)>0: author=author.strip() else: author='NULL' return author def get_state(self,response): state=response.xpath('//p[@class="tag"]/span/text()').extract()[0] if len(state)>0: state=state.strip() else: st='NULL' return state def get_type(self,response): type=response.xpath('//p[@class="tag"]/a/text()').extract() if len(type)>0: t="" for i in type: t+=' '+i type=t else: type='NULL' return type def get_about(self,response): about=response.xpath('//p[@class="intro"]/text()').extract()[0] if len(about)>0: about=about.strip() else: about='NULL' return about def get_score(self,response): def get_sc(id): urll = 'https://book.qidian.com/ajax/comment/index?_csrfToken=ziKrBzt4NggZbkfyUMDwZvGH0X0wtrO5RdEGbI9w&bookId=' + id + '&pageSize=15' rr = requests.get(urll) # print(rr) score = rr.text[16:19] return score bid=response.xpath('//a[@id="bookImg"]/@data-bid').extract()[0] #獲取書的id if len(bid)>0: score=get_sc(bid) #調用方法獲取評分 若是整數 可能返回 9," if score[1]==',': score=score.replace(',"',".0") else: score=score else: score='NULL' return score def get_story(self,response): story=response.xpath('//div[@class="book-intro"]/p/text()').extract()[0] if len(story)>0: story=story.strip() else: story='NULL' return story def get_news(self,response): news=response.xpath('//div[@class="detail"]/p[@class="cf"]/a/text()').extract()[0] if len(news)>0: news=news.strip() else: news='NULL' return news
items.py:
# -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html from scrapy import Field,Item class QdItem(Item): # define the fields for your item here like: book_name = Field() #書名 author=Field() #作者 state=Field() #狀態 type=Field() #類型 about=Field() #簡介 score=Field() #評分 story=Field() #故事 news=Field() #最新章節
settings.py:
# -*- coding: utf-8 -*- # Scrapy settings for qd project # # For simplicity, this file contains only settings considered important or # commonly used. You can find more settings consulting the documentation: # # https://docs.scrapy.org/en/latest/topics/settings.html # https://docs.scrapy.org/en/latest/topics/downloader-middleware.html # https://docs.scrapy.org/en/latest/topics/spider-middleware.html BOT_NAME = 'qd' SPIDER_MODULES = ['qd.spiders'] NEWSPIDER_MODULE = 'qd.spiders' DEFAULT_REQUEST_HEADERS = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv 11.0) like Gecko', } # Crawl responsibly by identifying yourself (and your website) on the user-agent #USER_AGENT = 'qd (+http://www.yourdomain.com)' # Obey robots.txt rules ROBOTSTXT_OBEY = True # Configure maximum concurrent requests performed by Scrapy (default: 16) #CONCURRENT_REQUESTS = 32 # Configure a delay for requests for the same website (default: 0) # See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay # See also autothrottle settings and docs #DOWNLOAD_DELAY = 3 # The download delay setting will honor only one of: #CONCURRENT_REQUESTS_PER_DOMAIN = 16 #CONCURRENT_REQUESTS_PER_IP = 16 # Disable cookies (enabled by default) #COOKIES_ENABLED = False # Disable Telnet Console (enabled by default) #TELNETCONSOLE_ENABLED = False # Override the default request headers: #DEFAULT_REQUEST_HEADERS = { # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', # 'Accept-Language': 'en', #} # Enable or disable spider middlewares # See https://docs.scrapy.org/en/latest/topics/spider-middleware.html #SPIDER_MIDDLEWARES = { # 'qd.middlewares.QdSpiderMiddleware': 543, #} # Enable or disable downloader middlewares # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html #DOWNLOADER_MIDDLEWARES = { # 'qd.middlewares.QdDownloaderMiddleware': 543, #} # Enable or disable extensions # See https://docs.scrapy.org/en/latest/topics/extensions.html #EXTENSIONS = { # 'scrapy.extensions.telnet.TelnetConsole': None, #} # Configure item pipelines # See https://docs.scrapy.org/en/latest/topics/item-pipeline.html #ITEM_PIPELINES = { # 'qd.pipelines.QdPipeline': 300, #} # Enable and configure the AutoThrottle extension (disabled by default) # See https://docs.scrapy.org/en/latest/topics/autothrottle.html #AUTOTHROTTLE_ENABLED = True # The initial download delay #AUTOTHROTTLE_START_DELAY = 5 # The maximum download delay to be set in case of high latencies #AUTOTHROTTLE_MAX_DELAY = 60 # The average number of requests Scrapy should be sending in parallel to # each remote server #AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0 # Enable showing throttling stats for every response received: #AUTOTHROTTLE_DEBUG = False # Enable and configure HTTP caching (disabled by default) # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings #HTTPCACHE_ENABLED = True #HTTPCACHE_EXPIRATION_SECS = 0 #HTTPCACHE_DIR = 'httpcache' #HTTPCACHE_IGNORE_HTTP_CODES = [] #HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
middlewares.py:
# -*- coding: utf-8 -*- # Define here the models for your spider middleware # # See documentation in: # https://docs.scrapy.org/en/latest/topics/spider-middleware.html from scrapy import signals class qdSpiderMiddleware(object): # Not all methods need to be defined. If a method is not defined, # scrapy acts as if the spider middleware does not modify the # passed objects. @classmethod def from_crawler(cls, crawler): # This method is used by Scrapy to create your spiders. s = cls() crawler.signals.connect(s.spider_opened, signal=signals.spider_opened) return s def process_spider_input(self, response, spider): # Called for each response that goes through the spider # middleware and into the spider. # Should return None or raise an exception. return None def process_spider_output(self, response, result, spider): # Called with the results returned from the Spider, after # it has processed the response. # Must return an iterable of Request, dict or Item objects. for i in result: yield i def process_spider_exception(self, response, exception, spider): # Called when a spider or process_spider_input() method # (from other spider middleware) raises an exception. # Should return either None or an iterable of Request, dict # or Item objects. pass def process_start_requests(self, start_requests, spider): # Called with the start requests of the spider, and works # similarly to the process_spider_output() method, except # that it doesn’t have a response associated. # Must return only requests (not items). for r in start_requests: yield r def spider_opened(self, spider): spider.logger.info('Spider opened: %s' % spider.name) class qdDownloaderMiddleware(object): # Not all methods need to be defined. If a method is not defined, # scrapy acts as if the downloader middleware does not modify the # passed objects. @classmethod def from_crawler(cls, crawler): # This method is used by Scrapy to create your spiders. s = cls() crawler.signals.connect(s.spider_opened, signal=signals.spider_opened) return s def process_request(self, request, spider): # Called for each request that goes through the downloader # middleware. # Must either: # - return None: continue processing this request # - or return a Response object # - or return a Request object # - or raise IgnoreRequest: process_exception() methods of # installed downloader middleware will be called return None def process_response(self, request, response, spider): # Called with the response returned from the downloader. # Must either; # - return a Response object # - return a Request object # - or raise IgnoreRequest return response def process_exception(self, request, exception, spider): # Called when a download handler or a process_request() # (from other downloader middleware) raises an exception. # Must either: # - return None: continue processing this exception # - return a Response object: stops process_exception() chain # - return a Request object: stops process_exception() chain pass def spider_opened(self, spider): spider.logger.info('Spider opened: %s' % spider.name)
pipelines.py:
# -*- coding: utf-8 -*- # Define your item pipelines here # # Don't forget to add your pipeline to the ITEM_PIPELINES setting # See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html class QdPipeline(object): def process_item(self, item, spider): return item