需求:爬取的是基於文字的網易新聞數據(國內、國際、軍事、航空)。
基於Scrapy框架代碼實現數據爬取後,再將當前項目修改成基於RedisSpider的分佈式爬蟲形式。html
$ scrapy startproject wangyiPro $ cd wangyiPro/ $ scrapy genspider wangyi news.163.com # 基於scrapy.Spider建立爬蟲文件
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') # 獲取指定的四個列表元素(國內三、國際五、軍事六、航空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,說明解析成功:python
$ scrapy crawl wangyi --nolog http://news.163.com/domestic/:國內 http://news.163.com/world/:國際 http://war.163.com/:軍事 http://news.163.com/air/:航空
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') # 獲取指定的四個列表元素(國內三、國際五、軍事六、航空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實例化對象中獲取的頁面數據,這個數據中包含動態加載的數據內容。git
需求分析:當點擊國內超鏈進入國內對應的頁面時,會發現當前頁面展現的新聞數據是被動態加載出來的,若是直接經過程序對url進行請求,是獲取不到動態加載出的新聞數據的。則就須要咱們使用selenium實例化一個瀏覽器對象,在該對象中進行url的請求,獲取動態加載的新聞數據。
響應對象response從下載器傳給Spiders爬蟲文件時,必定會穿過下載中間件。
能夠在下載中間件對響應對象進行攔截,對響應對象中存儲的頁面數據進行篡改,將動態加載的頁面數據加入到響應對象中。
經過selenium能夠篡改響應數據,並將頁面數據篡改爲攜帶了新聞數據的數據。github
當引擎將國內板塊url對應的請求提交給下載器後,下載器進行網頁數據的下載,而後將下載到的頁面數據,封裝到response中,提交給引擎,引擎將response在轉交給Spiders。
Spiders接受到的response對象中存儲的頁面數據裏是沒有動態加載的新聞數據的。要想獲取動態加載的新聞數據,則須要在下載中間件中對下載器提交給引擎的response響應對象進行攔截,切對其內部存儲的頁面數據進行篡改,修改爲攜帶了動態加載出的新聞數據,而後將被篡改的response對象最終交給Spiders進行解析操做。web
(1)在爬蟲文件中導入webdriver類redis
from selenium import webdriver
(2)重寫爬蟲文件的構造方法
在構造方法中使用selenium實例化一個瀏覽器對象(由於瀏覽器對象只須要被實例化一次)chrome
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中存儲的頁面數據。windows
(5)在配置文件中開啓下載中間件瀏覽器
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() # 瀏覽器關閉
# Enable or disable downloader middlewares # See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html DOWNLOADER_MIDDLEWARES = { 'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543, }
讓瀏覽器對象去發起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 # 這是原來的響應對象
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') # 獲取指定的四個列表元素(國內三、國際五、軍事六、航空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
注意:
因爲爬蟲作了兩次解析,所以如何將第一次解析的數據加入item對象是最大的難點。
解決方法:meta屬性請求傳參。
# 對url發起請求,獲取對應頁面中存儲的新聞內容數據 yield scrapy.Request(url=url, callback=self.getContent, meta={"item":item})
對文章url發起請求,欲獲取對應頁面中存儲的新聞內容數據,調用新的回調函數getContent。
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
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()
(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)執行爬蟲輸出爬取的新聞信息
下載中間件(Downloader Middlewares) 位於scrapy引擎和下載器之間的一層組件。
(1)引擎將請求傳遞給下載器過程當中, 下載中間件能夠對請求進行一系列處理。好比設置請求的 User-Agent,設置代理等
(2)在下載器完成將Response傳遞給引擎中,下載中間件能夠對響應進行一系列處理。好比進行gzip解壓等。
咱們主要使用下載中間件處理請求,通常會對請求設置隨機的User-Agent ,設置隨機的代理。目的在於防止爬取網站的反爬蟲策略。
做用:儘量多的將scrapy工程中的請求假裝成不一樣類型的瀏覽器身份。
操做流程:
1.在下載中間件中攔截請求
2.將攔截到的請求的請求頭信息中的UA進行篡改假裝
3.在配置文件中開啓下載中間件
代碼實現:
# 在middlewares.py中單獨給UA池封裝一個下載中間件的類 from scrapy.contrib.downloadermiddleware.useragent import UserAgentMiddleware import random class RandomUserAgent(UserAgentMiddleware): # 繼承UserAgentMiddleware def process_request(self, request, spider): """每次攔截請求,都會從列表中隨機抽選一個ua賦值給當前攔截的請求""" # 從列表中隨機抽選出一個ua值 ua = random.choice(user_agent_list) # 請求頭信息設置,賦值隨機抽取的ua(當前攔截請求ua寫入操做) request.headers.setdefault('User-Agent', ua) user_agent_list = [ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 " "(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1", "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 " "(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 " "(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 " "(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 " "(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 " "(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 " "(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 " "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 " "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24" ]
做用:儘量多的將scrapy工程中的請求的IP設置成不一樣的。
操做流程:
1.在下載中間件中攔截請求
2.將攔截到的請求的IP修改爲某一代理IP
3.在配置文件中開啓下載中間件
代碼實現:
# 在middlewares.py中單獨給代理池封裝一個下載中間件的類 # 批量對攔截的請求進行Ip更換 class Proxy(object): def process_request(self, request, spider): # 對攔截到的請求url進行判斷(協議頭究竟是http仍是https) # 代理IP對協議頭有嚴格區分 # request.url返回值形式:http://www.xxx.com/ h = request.url.split(":")[0] # 切割獲取協議頭 if h == "https": ip = random.choice(PROXY_https) # 利用meta修改代理ip request.meta['proxy'] = 'https://' + ip else: ip = random.choice(PROXY_http) request.meta['proxy'] = 'http://' + ip # 可被選用的代理IP——去www.goubanjia.com獲取免費代理IP PROXY_http = [ '153.180.102.104:80', '195.208.131.189:56055', ] PROXY_https = [ '120.83.49.90:9000', '95.189.112.214:35508', ]
注意:請求url的協議頭究竟是http仍是https。
# Enable or disable downloader middlewares # See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html DOWNLOADER_MIDDLEWARES = { 'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543, 'wangyiPro.middlewares.RandomUserAgent': 542, 'wangyiPro.middlewares.Proxy': 541, }
每發一個請求,這個請求都會被中間件攔截,對當前請求的ip和user-agent進行更換。
目的是攻克網站的反爬機制,由於每發一個請求,它的代理ip和ua都是不同的,門戶網站就很難發現當前爬取的載體是一個爬蟲。
from scrapy_redis.spiders import RedisSpider # class WangyiSpider(scrapy.Spider): class WangyiSpider(RedisSpider):
注意:這裏將爬蟲類的父類,修改爲RedisSpider。
# start_urls = ['https://news.163.com/'] redis_key = 'wangyi'
redis_key屬性:表示調度器隊列的名稱。
# 不註釋時,只容許本機的客戶端鏈接 # bind 127.0.0.1 # yes改成no,關閉redis的保護模式,客戶端能夠對服務器進行讀寫操做 protected-mode no
管道默認會鏈接且將數據存儲到本機的redis服務中,若是想要鏈接存儲到其餘redis服務中須要在settings.py中進行以下配置
# 若是redis服務器不在本身本機,則須要作以下配置 REDIS_HOST = '192.168.31.31' # redis數據庫所在機器的Ip地址 REDIS_PORT = 6379 # 可選配置 # REDIS_ENCODING = 'utf-8' # REDIS_PARAMS = {'password':'123456'} # 若是redis數據庫有密碼時配置
使用scrapy-redis組件中封裝好的能夠被共享的管道。
能夠將每臺機器爬取到的數據存儲經過該管道存儲到redis數據庫中,從而實現了多臺機器的管道共享。
# Configure item pipelines # See https://doc.scrapy.org/en/latest/topics/item-pipeline.html ITEM_PIPELINES = { # 'wangyiPro.pipelines.WangyiproPipeline': 300, 'scrapy_redis.pipelines.RedisPipeline': 400, }
使用scrapy-redis組件中封裝好的調度器,將全部的url存儲到該指定的調度器中,從而實現了多臺機器的調度器共享。
如下代碼可在settings.py中任意位置粘貼:
# 使用scrapy-redis組件的去重隊列 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis組件本身的調度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 核心配置 # 是否容許暫停 SCHEDULER_PERSIST = True # 值爲True表示:宕機恢復服務時,從宕機的那個地方開始爬取,不用從頭開始
# MAC/Linux $ pwd /Users/hqs/redis-5.0.2 $ src/redis-server redis.conf # windows $ redis-server.exe redis-windows.conf
$ pwd /Users/hqs/ScrapyProjects/wangyiPro/wangyiPro/spiders $ scrapy runspider wangyi.py
$ src/redis-cli 127.0.0.1:6379> lpush wangyi https://news.163.com (integer) 1 127.0.0.1:6379> keys * 1) "data" 2) "qiubai:items" 3) "name" 4) "qiubai:dupefilter" 5) "wangyi:items" 127.0.0.1:6379> lrange wangyi:items 0 -1 # 從頭至尾查看數據值
提交起始url後,爬蟲開始幹活: