前言:原生的Scrapy框架为什么做不了分布式?
思考:
1. Scrapy分布式爬虫意味着几台机器通过某种方式共同执行一套爬取任务,这就首先要求每台机器都要有Scrapy框架,一套Scrapy框架就有一套Scrapy五大核心组件,引擎--调度器--下载器--爬虫--项目管道,各自独有的调度器没有办法实现任务的共享,所以不能实现分布式爬取。
2. 假设可以实现Scrapy框架的调度器共享,那么就能实现分布式爬取了吗?答案是不能,因为我们实现了任务的共享,但是框架之间的项目管道是单独的,我们的任务下载完之后,我们爬取的有效信息还是不能全部存放在某个指定的位置,所以要想实现分布式爬虫,需要同时满足调度器和项目管道的共享才可以达到分布式的效果。
实现:基于Scrapy-redis实现分布式爬虫:
scrapy-redis内部实现了调度器和项目管道共享,可以实现分布式爬虫
一、redis数据库实现RedisCrawlSpider分布式操作
案例简述:分布式爬虫爬取抽屉网全栈主题文本数据
redis的准备工作:
1.对redis配置文件进行配置:
- 注释该行:bind 127.0.0.1,表示可以让其他ip访问redis
- 将yes该为no:protected-mode no,表示可以让其他ip操作redis
2.启动redis:
mac/linux: redis-server redis.conf
windows: redis-server.exe redis-windows.conf实现分布式爬虫的操作步骤:
1. 将redis数据库的配置文件进行改动: .修改值 protected-mode no .注释 bind 127.0.0.1
2. 下载scrapy-redis
pip3 install scraps-redis
3. 创建工程 scrapy startproject 工程名
scrapy startproject 工程名
4. 创建基于scrawlSpider的爬虫文件
cd 工程名
scrapy genspider -t crawl 项目名
5. 导入RedisCrawlSpider类
from scrapy_redis.spiders import RedisCrawlSpider
6. 在现有代码的基础上进行连接提取和解析操作
class RidesdemoSpider(RedisCrawlSpider):
redis_key = "redisQueue"
7. 将解析的数据值封装到item中,然后将item对象提交到scrapy-redis组件中的管道里(自建项目的管道没什么用了,可以直接删除了,用的是组件封装好的scrapy_redis.pipelines中)
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400,
}
. 8. 管道会将数据值写入到指定的redis数据库中(在配置文件中进行指定redis数据库ip的编写)
REDIS_HOST = '192.168.137.76'
REDIS_PORT = 6379
REDIS_ENCODING = ‘utf-8’
# REDIS_PARAMS = {‘password’:’123456’}
9. 在当前工程中使用scrapy-redis封装好的调度器(在配置文件中进行配置)
# 使用scrapy-redis组件的去重队列(过滤)
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis组件自己的调度器(核心代码共享调度器)
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 是否允许暂停
SCHEDULER_PERSIST = True
11. 启动redis服务器:
redis-server redis.windows.conf windows系统
redis-server redis.conf mac系统
12. 启动redis-cli
redis-cli
13. 执行当前爬虫文件:
scrapy runspider 爬虫文件.py
14. 向队列中扔一个起始url>>>在redis-cli执行扔的操作:
lpush redis_key的value值 起始url
spider.py文件:
# -*- coding: utf-8 -*-
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import Rule
from scrapy_redis.spiders import RedisCrawlSpider
from redisScrapyPro.items import RedisscrapyproItem
class RidesdemoSpider(RedisCrawlSpider):
name = 'redisDemo'
# scrapy_redis的调度器队列的名称,最终我们会根据该队列的名称向调度器队列中扔一个起始url
redis_key = "redisQueue"
link = LinkExtractor(allow=r'https://dig.chouti.com/.*?/.*?/.*?/\d+')
link1 = LinkExtractor(allow=r'https://dig.chouti.com/all/hot/recent/1')
rules = (
Rule(link, callback='parse_item', follow=True),
Rule(link1, callback='parse_item', follow=True),
)
def parse_item(self, response):
div_list = response.xpath('//*[@id="content-list"]/div')
for div in div_list:
content = div.xpath('string(./div[@class="news-content"]/div[1]/a[1])').extract_first().strip().replace("\t","")
print(content)
item = RedisscrapyproItem()
item['content'] = content
yield item
settings.py
BOT_NAME = 'redisScrapyPro'
SPIDER_MODULES = ['redisScrapyPro.spiders']
NEWSPIDER_MODULE = 'redisScrapyPro.spiders'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400,
}
REDIS_HOST = '192.168.137.76'
REDIS_PORT = 6379
REDIS_ENCODING = 'utf-8'
# REDIS_PARAMS = {‘password’:’123456’}
# 使用scrapy-redis组件的去重队列
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis组件自己的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 是否允许暂停
SCHEDULER_PERSIST = True
更多项目代码
https://github.com/wangjifei121/FB-RedisCrawlSpider
二、redis数据库实现RedisSpider分布式操作
案例简述:分布式爬虫爬取网易新闻(国内,国际,军事,航空四个板块)
扩展知识点使用:
- selenium如何被应用到scrapy框架
- UA池的使用
- 代理IP池的使用
RedisSpider分布式操作的步骤和RedisCrawlSpider分布式的搭建步骤是相同的,参照以上步骤来学习搭建就可以。
接下来主要讲解一下拓展知识点的使用:
一、selenium如何被应用到scrapy框架
首先看spider类中增加的两个方法:
def __init__(self):
pass
def closed(self, spider):
pass
通过对网易新闻网页分析,我们可以看出网页的数据采取了动态加载数据的反爬措施,这样我们要想获取更多数据就需要使用到selenium的webdriver类
了。
使用webdriver的第一步就是实例化一个webdriver对象,而实例化的对象只需要实例化一次就能在爬取的过程中使用,所以就应该想到类的实例化方法_init_。
实例化的webdriver对象在结束使用后需要关闭,正好spider类给我们提供了closed方法来做关闭操作。这样我们就可以通过这两个方法来实现我们的想法了。
.
那我们实例化好了webdriver对象该怎么用它呢?在哪里用?
首先我们在不用selenium的时候我们发现页面的数据我们是获取不到的,也可以换个角度来说,我们获取到了数据但不是我们想要的。这样我们的需求就要求我们重新来获取想要的response,可以肯定的是,每次请求我们都要做相应的处理,这时候经验丰富的你就应该想到了三个字《中间件》,那在哪个中间件来做对应的处理呢?
很显然要在process_response
中间件中来执行我们的selenium的相关操作,这个中间件的作用就是拦截到响应对象(下载器传递给Spider的响应对象),通过处理、伪装response从而得到我们想要的数据。
.
process_response
中间件中参数解释:
request:响应对象对应的请求对象
response:拦截到的响应对象
spider:爬虫文件中对应的爬虫类的实例
.
在中间件中我们主要做了哪些操作呢?
通过实例化好的浏览器对象发动请求-->执行相应的js代码-->获取对应的页面数据-->篡改相应对象-->返回response
二、UA池代码的编写
和上面的方法一样,UA池代码也需要在中间件中编写,需要导入一个类来继承,然后构建我们的UA类:
from scrapy.contrib.downloadermiddleware.useragent import UserAgentMiddleware
三、IP代理池的编写
问题的思考方式是一样的,这里就略过了
这里需要着重注意:请求的协议头有http 和 https两种,我们需要做相应的判断最后中间件一定要在settings中进行注册!!!注册!!!注册!!!
最后附上spider.py和middlewares.py的代码:
spider.py
# -*- coding: utf-8 -*-
import scrapy
from selenium import webdriver
from wangyiPro.items import WangyiproItem
from scrapy_redis.spiders import RedisSpider
class WangyiSpider(RedisSpider):
name = 'wangyi'
# allowed_domains = ['www.xxxx.com']
# start_urls = ['https://news.163.com']
redis_key = 'wangyi'
def __init__(self):
# 实例化一个浏览器对象(实例化一次)
self.bro = webdriver.Chrome(executable_path='/Users/wangjifei/my_useful_file/chromedriver')
# 必须在整个爬虫结束后,关闭浏览器
def closed(self, spider):
print('爬虫结束')
self.bro.quit()
def parse(self, response):
lis = response.xpath('//div[@class="ns_area list"]/ul/li')
indexs = [3, 4, 6, 7]
li_list = [] # 存储的就是国内,国际,军事,航空四个板块对应的li标签对象
for index in indexs:
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发起请求,获取页面数据(标题,缩略图,关键字,发布时间,url)
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))
for div in div_list:
head = div.xpath('.//div[@class="news_title"]/h3/a/text()').extract_first()
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')
tags.append(t)
tag = "".join(tags)
# 获取meta传递过来的数据值title
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})
# print(head+":"+url+":"+imgUrl+":"+tag)
def getContent(self, response):
# 获取传递过来的item
item = response.meta['item']
# 解析当前页面中存储的新闻数据
content_list = response.xpath('//div[@class="post_text"]/p/text()').extract()
content = "".join(content_list)
item['content'] = content
yield item
middlewares.py
from scrapy.http import HtmlResponse
import time
from scrapy.contrib.downloadermiddleware.useragent import UserAgentMiddleware
import random
#UA池代码的编写(单独给UA池封装一个下载中间件的一个类)
#1,导包UserAgentMiddlware类
class RandomUserAgent(UserAgentMiddleware):
def process_request(self, request, spider):
#从列表中随机抽选出一个ua值
ua = random.choice(user_agent_list)
#ua值进行当前拦截到请求的ua的写入操作
request.headers.setdefault('User-Agent',ua)
#批量对拦截到的请求进行ip更换
class Proxy(object):
def process_request(self, request, spider):
#对拦截到请求的url进行判断(协议头到底是http还是https)
#request.url返回值:http://www.xxx.com
h = request.url.split(':')[0] #请求的协议头
if h == 'https':
ip = random.choice(PROXY_https)
request.meta['proxy'] = 'https://'+ip
else:
ip = random.choice(PROXY_http)
request.meta['proxy'] = 'http://' + ip
class WangyiproDownloaderMiddleware(object):
#拦截到响应对象(下载器传递给Spider的响应对象)
#request:响应对象对应的请求对象
#response:拦截到的响应对象
#spider:爬虫文件中对应的爬虫类的实例
def process_response(self, request, response, spider):
#响应对象中存储页面数据的篡改
if request.url in['http://news.163.com/domestic/','http://news.163.com/world/','http://news.163.com/air/','http://war.163.com/']:
spider.bro.get(url=request.url)
js = 'window.scrollTo(0,document.body.scrollHeight)'
spider.bro.execute_script(js)
time.sleep(2) #一定要给与浏览器一定的缓冲加载数据的时间
#页面数据就是包含了动态加载出来的新闻数据对应的页面数据
page_text = spider.bro.page_source
#篡改响应对象
return HtmlResponse(url=spider.bro.current_url,body=page_text,encoding='utf-8',request=request)
else:
return response
PROXY_http = [
'153.180.102.104:80',
'195.208.131.189:56055',
]
PROXY_https = [
'120.83.49.90:9000',
'95.189.112.214:35508',
]
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"
]