Scrapy框架之基於RedisSpider實現的分布式爬蟲
需求:爬取的是基於文字的網易新聞數據(國內、國際、軍事、航空)。
基於Scrapy框架代碼實現數據爬取後,再將當前項目修改為基於RedisSpider的分布式爬蟲形式。
一、基於Scrapy框架數據爬取實現
1、項目和爬蟲文件創建
$ scrapy startproject wangyiPro
$ cd wangyiPro/
$ scrapy genspider wangyi news.163.com # 基於scrapy.Spider創建爬蟲文件
2、爬蟲文件編寫——解析新聞首頁獲取四個板塊的url
import scrapy class WangyiSpider(scrapy.Spider): name = 'wangyi' # allowed_domains = ['news.163.com'] start_urls = ['https://news.163.com/'] def parse(self, response): lis = response.xpath('//div[@class="ns_area list"]/ul/li') # 獲取指定的四個列表元素(國內3、國際5、軍事6、航空7) indexes = [3, 4, 6, 7] li_list = [] # 四個板塊對應的li標簽對象 for index in indexes: li_list.append(lis[index]) # 獲取四個板塊中的超鏈和文字標題 for li in li_list: url = li.xpath('./a/@href').extract_first() title = li.xpath('./a/text()').extract_first() # 板塊名稱 print(url + ":" + title) # 測試
執行爬蟲文件,控制臺打印輸出四個url,說明解析成功:
$ scrapy crawl wangyi --nolog
http://news.163.com/domestic/:國內
http://news.163.com/world/:國際
http://war.163.com/:軍事
http://news.163.com/air/:航空
3、爬蟲文件編寫——對每個板塊url發請求,進一步解析
import scrapy class WangyiSpider(scrapy.Spider): name = 'wangyi' # allowed_domains = ['news.163.com'] start_urls = ['https://news.163.com/'] def parse(self, response): lis = response.xpath('//div[@class="ns_area list"]/ul/li') # 獲取指定的四個列表元素(國內3、國際5、軍事6、航空7) indexes = [3, 4, 6, 7] li_list = [] # 四個板塊對應的li標簽對象 for index in indexes: li_list.append(lis[index]) # 獲取四個板塊中的超鏈和文字標題 for li in li_list: url = li.xpath('./a/@href').extract_first() title = li.xpath('./a/text()').extract_first() # 板塊名稱 """對每一個板塊對應url發起請求,獲取頁面數據""" # 調用scrapy.Request()方法發起get請求 yield scrapy.Request(url=url, callback=self.parseSecond) def parseSecond(self, response): """聲明回調函數""" # 找到頁面中新聞的共有標簽類型,排除廣告標簽 div_list = response.xpath('//div[@class="data_row news_article clearfix"]') print(len(div_list)) # 非空則驗證xpath是正確的 for div in div_list: # 文章標題 head = div.xpath('.//div[@class="news_title"]/h3/a/text()').extract_first() # 文章url url = div.xpath('.//div[@class="news_title"]/h3/a/@href').extract_first() # 縮略圖 imgUrl = div.xpath('./a/img/@src').extract_first() # 發布時間和標簽:提取列表中所有的元素 tag = div.xpath('.//div[@class="news_tag"]//text()').extract() # 列表裝化為字符串 tag = "".join(tag)
編寫到這裏時,再次執行爬蟲腳本,會發現print(len(div_list))輸出的是4個0,但是xpath表達式卻是正確的。
這是由於新浪網的新聞列表信息是動態加載的,而爬蟲程序向url發請求無法獲取動態加載的頁面信息。
因此需要selenium幫忙在程序中實例化一個瀏覽器對象,由瀏覽器對象向url發請求,再通過調用page_source屬性拿到selenium實例化對象中獲取的頁面數據,這個數據中包含動態加載的數據內容。
二、將selenium應用到Scrapy項目中
需求分析:當點擊國內超鏈進入國內對應的頁面時,會發現當前頁面展示的新聞數據是被動態加載出來的,如果直接通過程序對url進行請求,是獲取不到動態加載出的新聞數據的。則就需要我們使用selenium實例化一個瀏覽器對象,在該對象中進行url的請求,獲取動態加載的新聞數據。
可以在下載中間件對響應對象進行攔截,對響應對象中存儲的頁面數據進行篡改,將動態加載的頁面數據加入到響應對象中。
通過selenium可以篡改響應數據,並將頁面數據篡改成攜帶了新聞數據的數據。
1、selenium在scrapy中使用原理
當引擎將國內板塊url對應的請求提交給下載器後,下載器進行網頁數據的下載,然後將下載到的頁面數據,封裝到response中,提交給引擎,引擎將response在轉交給Spiders。
Spiders接受到的response對象中存儲的頁面數據裏是沒有動態加載的新聞數據的。要想獲取動態加載的新聞數據,則需要在下載中間件中對下載器提交給引擎的response響應對象進行攔截,切對其內部存儲的頁面數據進行篡改,修改成攜帶了動態加載出的新聞數據,然後將被篡改的response對象最終交給Spiders進行解析操作。
2、selenium在scrapy中使用流程總結
(1)在爬蟲文件中導入webdriver類
from selenium import webdriver
(2)重寫爬蟲文件的構造方法
在構造方法中使用selenium實例化一個瀏覽器對象(因為瀏覽器對象只需要被實例化一次)
class WangyiSpider(scrapy.Spider):
def __init__(self):
# 實例化瀏覽器對象(保證只會被實例化一次)
self.bro = webdriver.Chrome(executable_path='/Users/hqs/ScrapyProjects/wangyiPro/wangyiPro/chromedriver')
(3)重寫爬蟲文件的closed(self,spider)方法
在其內部關閉瀏覽器對象。該方法是在爬蟲結束時被調用。
class WangyiSpider(scrapy.Spider):
def closed(self, spider):
# 必須在整個爬蟲結束後關閉瀏覽器
print('爬蟲結束')
self.bro.quit() # 瀏覽器關閉
(4)重寫下載中間件的process_response方法
讓process_response方法對響應對象進行攔截,並篡改response中存儲的頁面數據。
(5)在配置文件中開啟下載中間件
3、項目代碼示例
(1)引入selenium定義瀏覽器開啟和關閉
import scrapy
from selenium import webdriver
from wangyiPro.items import WangyiproItem
class WangyiSpider(scrapy.Spider):
name = 'wangyi'
# allowed_domains = ['news.163.com']
start_urls = ['https://news.163.com/']
def __init__(self):
# 實例化瀏覽器對象(保證只會被實例化一次)
self.bro = webdriver.Chrome(executable_path='./wangyiPro/chromedrive')
def closed(self, spider):
# 必須在整個爬蟲結束後關閉瀏覽器
print('爬蟲結束')
self.bro.quit() # 瀏覽器關閉
(2)使用下載中間件攔截settings.py修改
# Enable or disable downloader middlewares
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
}
(3)在下載中間件中進行攔截
讓瀏覽器對象去發起get請求,獲取四大版塊對應的頁面數據,瀏覽器對url發送請求,瀏覽器是可以獲取到動態加載的頁面數據的。
獲取到這部分動態數據後,可以將這部分數據裝回到攔截的response對象中去。然後將篡改好的response對象發給Spiders。
Spiders接收到response對象後,將response賦值給回調函數parseSecond的response參數中。
middlewares.py內容如下所示:
# 下載中間件
from scrapy.http import HtmlResponse # 通過這個類實例化的對象就是響應對象
import time
class WangyiproDownloaderMiddleware(object):
def process_request(self, request, spider):
"""
可以攔截請求
:param request:
:param spider:
:return:
"""
return None
def process_response(self, request, response, spider):
"""
可以攔截響應對象(下載器傳遞給Spider的響應對象)
:param request: 響應對象對應的請求對象
:param response: 攔截到的響應對象
:param spider: 爬蟲文件中對應的爬蟲類的實例
:return:
"""
print(request.url + "這是下載中間件")
# 響應對象中存儲頁面數據的篡改
if request.url in ['http://news.163.com/domestic/', 'http://news.163.com/world/', 'http://war.163.com/', 'http://news.163.com/air/']:
# 瀏覽器請求發送(排除起始url)
spider.bro.get(url=request.url)
# 滾輪拖動到底部會動態加載新聞數據,js操作滾輪拖動
js = 'window.scrollTo(0, document.body.scrollHeight)' # 水平方向不移動:0;豎直方向移動:窗口高度
spider.bro.execute_script(js) # 拖動到底部,獲取更多頁面數據
time.sleep(2) # js執行給頁面2秒時間緩沖,讓所有數據得以加載
# 頁面數據page_text包含了動態加載出來的新聞數據對應的頁面數據
page_text = spider.bro.page_source
# current_url就是通過瀏覽器發起請求所對應的url
# body是當前響應對象攜帶的數據值
return HtmlResponse(url=spider.bro.current_url, body=page_text, encoding="utf-8", request=request)
else:
# 四個板塊之外的響應對象不做修改
return response # 這是原來的響應對象
三、爬蟲代碼完善及item處理
1、爬蟲文件
import scrapy
from selenium import webdriver
from wangyiPro.items import WangyiproItem
class WangyiSpider(scrapy.Spider):
name = 'wangyi'
# allowed_domains = ['news.163.com']
start_urls = ['https://news.163.com/']
def __init__(self):
# 實例化瀏覽器對象(保證只會被實例化一次)
self.bro = webdriver.Chrome(executable_path='/Users/hqs/ScrapyProjects/wangyiPro/wangyiPro/chromedriver')
def closed(self, spider):
# 必須在整個爬蟲結束後關閉瀏覽器
print('爬蟲結束')
self.bro.quit() # 瀏覽器關閉
def parse(self, response):
lis = response.xpath('//div[@class="ns_area list"]/ul/li')
# 獲取指定的四個列表元素(國內3、國際5、軍事6、航空7)
indexes = [3, 4, 6, 7]
li_list = [] # 四個板塊對應的li標簽對象
for index in indexes:
li_list.append(lis[index])
# 獲取四個板塊中的超鏈和文字標題
for li in li_list:
url = li.xpath('./a/@href').extract_first()
title = li.xpath('./a/text()').extract_first() # 板塊名稱
"""對每一個板塊對應url發起請求,獲取頁面數據"""
# 調用scrapy.Request()方法發起get請求
yield scrapy.Request(url=url, callback=self.parseSecond, meta={'title': title})
def parseSecond(self, response):
"""聲明回調函數"""
# 找到頁面中新聞的共有標簽類型,排除廣告標簽
div_list = response.xpath('//div[@class="data_row news_article clearfix"]')
# print(len(div_list)) # 非空則驗證xpath是正確的
for div in div_list:
# 文章標題
head = div.xpath('.//div[@class="news_title"]/h3/a/text()').extract_first()
# 文章url
url = div.xpath('.//div[@class="news_title"]/h3/a/@href').extract_first()
# 縮略圖
imgUrl = div.xpath('./a/img/@src').extract_first()
# 發布時間和標簽:提取列表中所有的元素
tag = div.xpath('.//div[@class="news_tag"]//text()').extract()
# 列表裝化為字符串
tags = []
for t in tag:
t = t.strip(' \n \t') # 去除空格 \n換行 \t相當於tab
tags.append(t) # 重新裝載到列表中
tag = "".join(tags)
# 獲取meta傳遞的數據值
title = response.meta['title']
# 實例化item對象,將解析到的數據值存儲到item對象中
item = WangyiproItem()
item['head'] = head
item['url'] = url
item['imgUrl'] = imgUrl
item['tag'] = tag
item['title'] = title
# 對url發起請求,獲取對應頁面中存儲的新聞內容數據
yield scrapy.Request(url=url, callback=self.getContent, meta={"item":item})
def getContent(self, response):
"""新聞內容解析的回調函數"""
# 獲取傳遞過來的item對象
item = response.meta['item']
# 解析當前頁碼中存儲的頁面數據
# 由於新聞的段落可能有多個,每個段落在一個p標簽中。因此使用extract()方法
content_list = response.xpath('//div[@class="post_text"]/p/text()').extract()
# 列表轉字符串(字符串才能保持在item對象中)
content = "".join(content_list)
item["content"] = content
# item對象提交給管道
yield item
註意:
(1)將解析到的數據值存儲到item對象
由於爬蟲做了兩次解析,因此如何將第一次解析的數據加入item對象是最大的難點。
解決方法:meta屬性請求傳參。
# 對url發起請求,獲取對應頁面中存儲的新聞內容數據
yield scrapy.Request(url=url, callback=self.getContent, meta={"item":item})
對文章url發起請求,欲獲取對應頁面中存儲的新聞內容數據,調用新的回調函數getContent。
(2)新聞內容解析後將item對象提交給管道
class WangyiSpider(scrapy.Spider):
"""同上省略"""
def getContent(self, response):
"""新聞內容解析的回調函數"""
# 獲取傳遞過來的item對象
item = response.meta['item']
# 解析當前頁碼中存儲的頁面數據
# 由於新聞的段落可能有多個,每個段落在一個p標簽中。因此使用extract()方法
content_list = response.xpath('//div[@class="post_text"]/p/text()').extract()
# 列表轉字符串(字符串才能保持在item對象中)
content = "".join(content_list)
item["content"] = content
# item對象提交給管道
yield item
2、items.py文件
import scrapy
class WangyiproItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
head = scrapy.Field()
url = scrapy.Field()
imgUrl = scrapy.Field()
tag = scrapy.Field()
title = scrapy.Field()
content = scrapy.Field()
3、管道文件pipeline.py處理
(1)pipelines.py
class WangyiproPipeline(object):
def process_item(self, item, spider):
print(item['title']+ ':'+ item['content'])
return item
(2)settings.py中放開管道
# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'wangyiPro.pipelines.WangyiproPipeline': 300,
}
(3)執行爬蟲輸出爬取的新聞信息
四、UA池和代理池在Scrapy中應用
Scrapy框架之基於RedisSpider實現的分布式爬蟲