理论基础:分布式爬虫的原理
- 分布式数据库中有4个key:
- xxx:start_urls : 起始url列表,用于存放我们通过服务器脚本加入的起始url
- xxx:requests : 用于存储爬虫过程中新产生的那些url对应的请求对象
- xxx:items : 用于存储抓到的数据
- xxx:dupefilter: 用于去重
实际操作
-
系统要求:一台性能较好Linux的主机,和若干台windows系统的主机。
-
工作环境和安装:
- Linux主机作为分布式系统服务端(也叫master端),主要提供数据存取等方面服务
- windows主机作为分布式系统的爬虫业务端(也叫slaver端),windows提供爬虫的业务等工作
- 安装:
- master端:安装redis数据库并且能够远程连接(master一般只提供数据服务,不提供业务服务)
- slaver端:安装scrapy(主要用于做爬虫业务),安装分布式调度组件scrapy_redis(由于scrapy中的调度器是本地调度器,只能进行当前scrapy的相关的业务的调度,无法进行多台主机之间协同调度,所以我们需要一个分布式调度的组件scrapy_redis
- 分布式部署
-
建立slaver端与master端的数据通路(首先,把scrapy_redis的管道组件配置上;然后,加上管道组件的主机名、端口号等信息以定位master端的服务程序)
-
将slaver端的调度器切换成分布式的调度器(首先,配置SCHEDULER值为scrapy_redis里面的相关调度组件的位置,然后给调度组件加入一些配置参数:例如去重组件)
-
把我们的slaver端的爬虫的父类修改成RedisCrawlSpider
-
把起始的url提取方式由当前类实例的start_urls属性,切换至从redis的相关的数据库中来提取(把start_urls这个属性去掉,加上redis_key=":start_urls"这个属性)
-
- 分布式爬虫执行:
- 将所有的slaver端的爬虫都运行起来(指令:scrapy crawl 爬虫名),运行起来以后所有的爬虫都在等待master端xxx:start_urls这个键里出现起始url
- 写一个服务器脚本,用于将我们的起始url全部加入xxx:start_urls这个key中
- 连接redis,redis-cli -h 主机名,连接成功后 执行lpush 爬虫名:start_urls 网站首页连接
- 例如:
lpush dushu:start_urls https://www. dushu.com/book/1002.html
- 竞争到起始url的那些爬虫开始爬取,没有竞争到的继续等待
-
settings配置
#settings文件配置
ITEM_PIPELINES = {
# 'DushuPro.pipelines.DushuproPipeline': 300, # 分布式爬虫数据不存在在slaver的本地,而是要存储在master端
# 我们应该把数据管道切换至分布式管道,有分布式管道将数据存入相关的master的数据库中
"scrapy_redis.pipelines.RedisPipeline":300, # 这个组件路径就是scrapy_redis的管道组件路径,这个管道作用就是建立一个slaver与master端的数据通路;这个数据通路的建立除了这个管道组件以外还需要一些配置信息
}
# scrapy_redis组件的配置信息
REDIS_HOST = '主机名'
REDIS_PORT = 6379
# 有密码的需再加一个密码配置
REDIS_PARAMS = {"password":"xxxxxx"}
# 将scrapy的调度器切换成分布式调度器
SCHEDULER ="scrapy_redis.scheduler.Scheduler"
# 调度过程是否允许暂停
SCHEDULER_PERSIST = True
# 调度过程的去重组件
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
分布式爬虫的执行流程
- 当所有的slaver主机运行起来以后,会通过管道组件不停的监视数据库中start_urls和requests两个key,从中竞争资源
- 通过服务器脚本加入xxx:start_urls这个key以后,某些slaver主机就会竞争到这个资源,就会开始爬取,如果请求成功这个过程中产生的新的url将会封装成一个request对象被加入xxx:requests中,产生的数据将会被加入xxx:items中,这个url本身会被加入到xxx:dupefilter中;如果请求失败,这个start_url对应的request对象将会被重新加入到xxx:requests中供其他主机再提取
- 新产生的那些requests列表中的url会被所有的slaver竞争提取,重复步骤2)
- 当所有的数据全部被抓取完,爬虫将继续等到是否有新的任务产生
代码实现
dushu网爬取
爬虫器
import scrapy
from scrapy.linkextractors import LinkExtractor
# 导入链接提取器类,从一个url的网页上根据一定的规则来提取新的链接
from scrapy.spiders import CrawlSpider, Rule
# CrawlSpider是spiders一个派生类,在基本爬虫的基础上扩展功能
# Rule规则对象,根据规则安排url的提取、组合与调度
from DushuPro.items import DushuproItem
from scrapy_redis.spiders import RedisCrawlSpider
# 这个类是CrawlSpider的派生类,在增量爬虫的基础上加入了分布式调度的相关方法
class DushuSpider(RedisCrawlSpider):
name = 'dushu'
allowed_domains = ['dushu.com']
# start_urls = ['https://www.dushu.com/book/1002.html']
# 分布式爬虫的所有的url都是master端的redis数据库来提供,start_urls这个属性要去掉
redis_key = "dushu:start_urls" # redis_key属性,指定起始url的应该从哪里来提取
rules = (
Rule(LinkExtractor(allow=r'/book/1002_\d\.html'), callback='parse_item', follow=True),)
def parse_item(self, response):
booklist = response.xpath("//div[@class='bookslist']//li")
for book in booklist:
item = DushuproItem()
item["title"] = book.xpath(".//h3/a/text()").extract_first()
# extract_first()从selector列表将内容取出,然后从内容列表中取出首元素,如果列表为空,直接去None
item["author"] = "".join(book.xpath(".//div[@class='book-info']/p[1]//text()").extract())
# print(item)
# 匹配出二级页面的链接
next_url = "https://www.dushu.com" + book.xpath(".//h3/a/@href").extract_first()
# 向二级页面发起请求
yield scrapy.Request(url=next_url,callback=self.parse_Info,meta={"item":item})
# 回调函数,用于解析下级页面
def parse_Info(self, response):
# 把上级页面送的item提取出来
item = response.meta["item"]
# 继续解析item
item["price"] = response.xpath("//span[@class='num']/text()").extract_first()
item["publisher"] = response.xpath("//div[@class='book-details-left']/table//tr[2]//a/text()").extract_first()
item["authorInfo"] = response.xpath("//div[@class='text txtsummary']//text()").extract()[1]
item["content"] = response.xpath("//div[@class='text txtsummary']//text()").extract()[0]
item["mulu"] = "\n".join(response.xpath("//div[@class='text txtsummary']")[2].xpath(".//text()").extract())
yield item
zhipin网爬取(代理池的应用)
settings配置
ROBOTSTXT_OBEY = False
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
DOWNLOAD_DELAY = 1
DOWNLOADER_MIDDLEWARES = {
'BossPro.middlewares.BossproDownloaderMiddleware': 543,
}
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300,
}
REDIS_HOST = 'www.fanjianbo.com'
REDIS_PORT = 6379
SCHEDULER = 'scrapy_redis.scheduler.Scheduler'
SCHEDULER_PERSIST = True
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
spiders模块
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from BossPro.items import BossproItem
from scrapy_redis.spiders import RedisCrawlSpider
class JobSpider(RedisCrawlSpider):
name = 'job'
allowed_domains = ['zhipin.com']
# start_urls = ['https://www.zhipin.com/c101010100-p100109/?page=1&ka=page-1']
redis_key = "job:start_urls"
rules = (
Rule(LinkExtractor(allow=r'page=\d+'), callback='parse_item', follow=True),
# /c101010100-p100109/?page=2
)
def parse_item(self, response):
joblist = response.xpath("//div[@class='job-list']//li")
for job in joblist:
# 一级页面没有item中的字段,匹配出下级页面url,请求下级页面取解析
next_url = "https://www.zhipin.com" + job.xpath(".//div[@class='info-primary']//h3/a/@href").extract_first()
yield scrapy.Request(url=next_url,callback=self.parse_info)
# 封装一个回调函数,用于解析职位详情页
def parse_info(self, response):
# print(response)
item = BossproItem()
item["jobName"] = response.xpath("//div[@class='info-primary']//h1/text()").extract_first()
item["salary"] = response.xpath("//span[@class='salary']/text()").extract_first()
item["require"] = " ".join(response.xpath("//div[starts-with(@class,'job-primary')]/div[@class='info-primary']//p//text()").extract()[1:])
item["jobInfo"] = "".join(response.xpath("//div[@class='job-sec']/div[@class='text']/text()").extract())
item["companyInfo"] = "".join(response.xpath("//div[@class='job-sec company-info']/div[@class='text']/text()").extract())
item["companySize"] = " ".join(response.xpath("//div[@class='sider-company']/p//text()").extract()[1:4])
item["companyFuli"] = " ".join(response.xpath("//div[@class='job-tags']")[0].xpath(".//text()").extract())
item["address"] = response.xpath("//div[@class='location-address']/text()").extract_first()
yield item
items模块
import scrapy
class BossproItem(scrapy.Item):
# 岗位名称
jobName = scrapy.Field()
# 薪资待遇
salary = scrapy.Field()
# 岗位要求
require = scrapy.Field()
# 岗位描述
jobInfo = scrapy.Field()
# 公司介绍
companyInfo = scrapy.Field()
# 公司规模
companySize = scrapy.Field()
# 公司福利
companyFuli = scrapy.Field()
# 公司地址
address = scrapy.Field()
middlewares模块
from scrapy import signals
import redis
import requests
class BossproDownloaderMiddleware(object):
# 定义一个成员变量用于提取代理
def get_ippool(self):
rds = redis.StrictRedis(host="www.fanjianbo.com",port=6379,db=6)
ip_list = rds.lrange("ippool",0,rds.llen("ippool"))
return ip_list
def process_request(self, request, spider):
print("中间件.....")
# 取出代理池
ip_list = self.get_ippool()
print(ip_list)
for ip in ip_list:
try:
# 检查代理是否能用
requests.get(url="https://www.baidu.com/", headers={"user-agent":'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}, proxies={"https": ip})
print("当前代理为:",ip)
# 给当前的request设置代理服务器
request.meta["proxy"] = {"https":ip.decode("utf8")}
break
except Exception as e:
print("代理%s已经过期!"%ip)
def process_response(self, request, response, spider):
return response
拼接url模块(单独解析)
from selenium import webdriver
from bs4 import BeautifulSoup
import redis
from time import sleep
# 【请求】
def request_data(url):
opt = webdriver.ChromeOptions()
opt.add_argument("--headless")
opt.add_argument('--disable-gpu')
driver = webdriver.Chrome(options=opt)
driver.get(url)
sleep(1)
return driver.page_source
# 【解析】
def analysis_data(html):
soup = BeautifulSoup(html,'lxml')
# 解析城市编码
provence_list = soup.select(".dorpdown-city > ul")[1:]
city_list = []
for provence in provence_list:
cities = provence.select("li")
for city in cities:
city_list.append(city.get("data-val"))
# 解析职位编码
job_list = soup.select(".job-menu dl ul a")
for job in job_list:
# 取出job的编号
job_code = job.get("href").split("-")[-1]
# 每一个工作编号和所有的城市组合成一个岗位招聘的url
for city in city_list:
job_url = "https://www.zhipin.com/" + "c" + city + "-" + job_code
yield job_url
# 【存储】
def write_to_redis(url_list):
rds = redis.StrictRedis(host="主机名",port=6379,db=0)
for url in url_list:
rds.lpush("job:start_urls",url)
print("链接:%s已经进入数据库!"%url)
if __name__ == '__main__':
url = "https://www.zhipin.com/"
html = request_data(url=url)
url_list = analysis_data(html)
write_to_redis(url_list)
提取代理服务器与代理池
import requests
import json
import redis
# 提取代理
def get_proxies(url):
res = requests.get(url=url)
return res.text
# 质检
def check_proxis(proxies):
headers = {"user-agent":'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
proxy_list = proxies.split("\r\n")
print(proxy_list)
for proxy in proxy_list:
try:
requests.get(url="https://www.baidu.com/",headers=headers,proxies={"https":proxy})
# 如果代理没有失效则存入数据库
rds = redis.StrictRedis(host="www.fanjianbo.com",port=6379,db=6)
rds.lpush("ippool",proxy)
print("代理:%s已经存入数据库!"%proxy)
except Exception as e:
print("代理%s已经失效!"%proxy)
if __name__ == '__main__':
url = "http://api3.xiguadaili.com/ip/?tid=559324242289181&num=100&delay=5&filter=on"
proxies = get_proxies(url)
# print(proxies)
check_proxis(proxies)