分布式爬虫

Redis

redis是一种支持分布式的nosql数据库,他的数据是保存在内存中的,同时redis可以定时把内存数据同步到磁盘,即可以将数据持久化。

分布式爬虫架构

  1. 核心服务器:一台master,用于启动爬虫,爬虫开始的url以及爬虫爬取的数据都保存在此主机中
  2. 跑爬虫程序的机器:多个slave(叶子节点),用于爬取数据

我们在Master上搭建一个Redis数据库(此数据库只用作url的存储),并对每个需要爬取的网站类型都开辟一个单独的列表字段,通过设置slave上的scrapy-redis获取Url的地址为master地址,这样的结果就是:尽管有多个slave,然而大家获取url的地方只有一个,那就是服务器master上的redis数据库。

并且,由于scrapy-redis自身的队列机制,slave获取的链接不会相互冲突,这样各个slave在完成抓取任务后,再把获取的结果汇总到服务器上

使用场景

  1. 登录会话机制:存储在redis中,数据不会丢失
  2. 排行榜/计数器:实时热搜榜,文章阅读量
  3. 作为消息队列
  4. 当前在线人数
  5. 一些常用的数据缓存
  6. 把前200篇文章缓存或评论缓存:一般用户浏览网站,只会浏览前面一部分的文章或评论,那么可以把前面的200篇文章和对应的评论缓存起来,不用每次请求数据库
  7. 好友关系:微博的好友关系使用redis实现
  8. 发布和订阅功能:可以用来做聊天软件

Scrapy=-Redis分布式爬虫组件

Scrapy是一个框架,他本身是不支持分布式的,如果我们想要分布式的爬虫,就需要一个叫做Scrapy-Redos的组件,这个组件利用了Redis可以分布式的功能,集成到Scrapy框架中,使得爬虫可以进行分布式。可以充分利用多个资源(多个ip、多带宽、同步爬取)来提高爬虫的爬行效率。

分布式爬虫的优点

  • 可以充分利用多台机器的带宽
  • 可以充分利用多台机器的Ip地址
  • 多台机器,爬取效率更高

分布式爬虫必须解决的问题

  • 分布式爬虫是好几台机器在同时运行,如何保证在不同的机器爬取页面时不会出现重复爬取的问题
  • 分布爬虫在不同的机器上运行,在把数据爬完后如何保证保存在同一个地方

分布式爬虫架构

  • Redis服务器:不运行爬虫代码,作用是管理爬虫服务器请求的URL并去重,存储爬虫服务器爬下来的数据(存在内存里)
  • 爬虫服务器:从Redis获取请求,把爬取下来的数据送给Redis服务器

其他机器访问本机Redis服务器

想要让其他机器访问本机的Redis服务器,要么修改redis.conf的配置文件,将bind[本机ip地址/0.0.0.0],其他机器才能访问

注:bind绑定的是本机网卡的ip地址,而不是想让其他机器连接的ip地址。如果有多块网卡,那么可以绑定多个网卡的ip地址,如果绑定的ip地址是0.0.0.0,那么意味着其他机器可以同通过本机所有的ip地址进行访问。

实际操作

  1. 在windows打开redis-server
  2. 在虚拟机(Kali)中连接windows服务器
    redis-cli -h windows的IP地址 -p 6379

Redis操作

列表

类似于python的列表,redis以键key-值value方式存储

  1. 向key列表左侧加入一个值:

    lpush key value #key表示一个列表
    lpush websites baidu.com
  2. 向key列表右侧加入一个元素:

    rpush key value
    rpush websites google.com
  3. 打印key列表的元素:

    lrange websites 范围
    lrange websites 0 -1   #打印列表所有元素
  4. 移除并返回列表最左侧的元素:

    lpop key 
    lpop websites
  5. 移除并返回列表最右侧的元素:

    rpop key
    rpop websites
  6. 移除列表指定位置的元素:

    lrem key count value
    lrem websites 1 baidu.com

注:根据参数count的值,移除列表中与参数value相等的元素,count的值可以是以下几种: 1.count>0:从表头开始向表尾搜索,移除与value相等的元素,移除的数量为count 2. count<0:从表尾开始向表头搜索,移除与value相等的元素,移除的数量为count的绝对值。 3.count=0,移除表中所有与value相等的值

  1. 指定返回第几个元素:

    lindex key index
    lindex websites 1
  2. 获取列表中的元素个数:

    llen key
    llen websites

集合操作

集合与python的集合相同,集合与列表的不同:

  • 集合是无序的,列表是有序的
  • 集合的元素是唯一的,列表的元素可以重复
  1. 添加元素

    sadd set value2 #set表述集合
    sadd team zby
  2. 查看元素:

    smembers set
    smembers team
  3. 查看集合中的元素个数:

    scard set
    scard team
  4. 获取多个集合的交集:

    sinter set1 set2
    sinter team1 team2
  5. 获取多个集合的并集:

    sunion set1 set2
    sunion team1 team2
  6. 获取多个集合的差集:

    sdiff set1 set2
    sdiff team1 team2

实战

在房天下(www.fang.com )分布式爬虫

步骤

收集信息

  1. 获取所有城市的url链接
    https://www.fang.com/SoufunFamily.htm
  2. 获取所有城市的新房的url链接
    例如:深圳新房:https://newhouse.sz.fang.com/house/s

注:以上城市的url对北京不适用,北京的链接没有城市前缀

创建爬虫项目

  1. 将开始爬取的url改为 https://www.fang.com/SoufunFamily.htm
  2. 找到包含城市的html框架,分析每个城市元素的html架构:
  • 找到最上层的html标签:发现整个城市名的框架处于一个div标签中
  • 所有城市处于一个table标签中
  • 每一行是一个tr标签
  • 每行中包含三个td标签,分别对应字母/省份/城市
  • 在第三个td标签中包含a标签,每个a标签为一个城市名
  1. sfw.py 获取城市链接,返回城市/链接

    import re
     def parse(self, response):
         trs = response.xpath("//div[@class='outCont']//tr") #获取最外层div中的每个tr标签
         province = None
         for tr in trs:  #遍历每个tr标签
             #筛选我们的要的标签,即第三个td标签,根据第三个td属性进行筛选
             tds = tr.xpath(".//td[not(@class)]")  #没有class标签,筛掉第一个td
             province_td = tds[0]
             province_text = province_td.xpath(".//text()").get() #获取第二个td内容
             province_text = re.sub(r"\s","",province_text)
             if province_text:
                 province = province_text #如果有省份就替换省份
             if province == "其它":   #不爬取国外网站
                 continue
             city_td = tds[1]
             city_links = city_td.xpath(".//a") #提取所有a标签
             for city_link in city_links:
                 city = city_link.xpath(".//text()").get() #获取城市名字
                 city_url = city_link.xpath(".//@href").get() #获取城市url
                 #构建新房的url链接
                 url_module= city_url.split("//") #将url以//进行分割
                 first = url_module[0] #分割http
                 second = url_module[1] #分割域名
                 list1 = second.split(".")
                 location = list1[0] #获取域名中的城市名字
                 fang = list1[1]    #获取域名中fang
                 com = list1[2]    #获取域名中com
                 newhouse_url = first + "//"+location+".newhouse."+fang+"."+com+"house/s/"
                 yield scrapy.Request(url=newhouse_url,callback=self.parse_newhouse,meta={"info":(province,city)})  #以该参数的形式方式请求并调用parse_newhouse函数
     def parse_newhouse(self,response):
         province,city= response.meta.get('info')   #将上一个省份和城市传递到这个函数来使用
         #详细代码见下文
         pass
  2. items.py 进行绑定

    class NewHouseItem(scrapy.Item):
     # define the fields for your item here like:
     # name = scrapy.Field()
     province = scrapy.Field()
     city = scrapy.Field()
     name = scrapy.Field()  #小区名字
     price = scrapy.Field()  #价格
     rooms = scrapy.Field()  #有几居
     area = scrapy.Field()  #面积
     address = scrapy.Field() #地址
     sale = scrapy.Field()   #是否在售
     origin_url = scrapy.Field() #详情页面的url
  3. sfw.py 处理新房的函数

    from fang.items import NewHouseItem
      def parse_newhouse(self,response):
         province,city= response.meta.get('info')   #将上一个省份和城市传递到这个函数来使用
         lis = response.xpath(".//div[contains(@class,'nl_con')]/ul/li")   #div下包含nl_con的类下的ul下的li
         for li in lis:
             name=li.xpath(".//div[@class='nlcd_name']/a/text()").get().strip()
             rooms = li.xpath(".//div[contains(@class,'house_type')]/a/text()").getall()  #获取div下标签a的所有文本,/text()表示获取子标签下的文本
             area = "".join(li.xpath(".//div[contains(@class,'house_type')]/text()").getall())   #获取面积 join转换为字符串,没有join会有换行符
             area = re.sub(r"\s|-|/","",area)  #去除多余字符
             address = "".join(li.xpath(".//div[@class='address']/a/text()").getall())
             address =re.sub(r"\s","",address)
             sale = li.xpath(".//div[@class='fangyuan']/span/text()").get()
             price = "".join(li.xpath(".//div[@class='nhouse_price']//text()").getall())   #获取同级标签的所有文本需要//text().getall()
             price = re.sub(r"\s","",price)
             origin_url = li.xpath(".//div[@class='nlcd_name']/a/@href").get()
             item = NewHouseItem(name=name,rooms=rooms,area=area,address=address,
                                 sale=sale,price=price,origin_url=origin_url,
                   province=province,city=city)
             yield item
         next_url = response.xpath("//div[@class='page']/a[@class='next']/@href").get()
         if next_url:
             yield scrapy.Request(url=response.urljin(next_url),callback=self.parse_newhouse(),
                                  meta={"info":(province,city)})    #请求下一页的url同样调用新房函数处理,并传递省份城市的参数

部署

分布式爬虫架构

  1. 核心服务器:一台master,用于启动爬虫,爬虫开始的url以及爬虫爬取的数据都保存在此主机中
  2. 跑爬虫程序的机器:多个slave(叶子节点),用于爬取数据

在master服务器上搭建一个redis数据库,并将要抓取的url存放到redis数据库中,所有的slave爬虫服务器在抓取的时候从redis数据库中去链接,由于scrapy_redis自身的队列机制,slave获取的url不会相互冲突,然后抓取的结果最后都存储到数据库中。master的redis数据库中还会将抓取过的url的指纹存储起来,用来去重。

实现

  1. 使用三台机器,一台win10,两台kali linux,分别在两台Linux上进行分布式抓取
  2. win10的ip地址为192.168.1.152,用来作为redis的master端,linux机器作为slave
  3. master的爬虫运行时会把提取到的Url封装成request放到redis数据库:”dmoz:requests”,并且从该数据库中提取request后下载网页,再把网页的内容放到redis的另一个数据库中”dmoz:items”
  4. slave从master的redis去除待抓取的request,下载完网页后就把网页的内容送回master的redis
  5. 重复上面的3和4,直到master的redis中的”dmoz:dupfilter”是用来存储抓取过的url的指纹(使用哈希函数将url运算后的结果),是防止重复抓取的

linux环境搭建

  1. 安装python环境:kali默认安装python3
  2. 安装scrapy-redis:
    在终端输入以下命令:
    pip install scrapy-redis
  3. 克隆以上虚拟机

winodws

服务器

redis默认只支持Linux,如果要在windows里安装需要去github里进行下载:www.github.com/MicrosoftArchive/redis/releases
安装之后打开cmd,进入到redis安装的上一级文件夹下执行以下命令,打开redis服务器,或者直接点击redis-server.exe

redis-server.exe redis.windows.conf

客户端

命令行

直接点击redis-cli.exe,或者重新打开一个cmd进入redis安装的上一级文件夹,连接redis服务器:

redis-cli

分布式爬虫部署

要将一个scrapy项目变成一个scrapy-redis 项目只需修改以下三点:

  1. 将爬虫的类从scrapy.Spider变成scrapy_redis.spider.RedisSpider或者从scrapy.CrawlSpider变成scrapy_redis.spiders.RdiesCrwalSpider(见下文spider.py)
  2. 将爬虫中的start_urls删掉,增加一个redis_key=”xxx”,这个redis_key是为了以后在redis中控制爬虫启动的。爬虫的第一个url,就是在redis中通过这个发送出去的。
  3. 在settings.py文件中增加配置(见下文settings.py)

settings.py

修改robot/user-agent/item_pipelines

#确保request存储在redis中
SCHEDULER = "scrapy_redis.sheduler.Scheduler"
#确保所有爬虫共享相同的去重指纹
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.REPDupeFilter"
#设置redis为item pipelins
ITEM_PIPELINES ={
    'scrapy_redis.pipelines.RedisPipeline':300
}
#在redis中保持scrapy-redis用到的队列,不会清理redis中的队列,从而可以实现暂停和恢复的功能
SCHEDULER_PERSIST = True
#设置连接redis信息
REDIS_HOST = 'redis服务器所在主机的ip地址'
REDIS_PORT = 6379

spider.py

# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapy_redis.spiders import RedisCrawlSpider
class A2345SpiderSpider(RedisCrawlSpider):
    name = '2345_spider'
    allowed_domains = ['ruanjian.2345.cc']
    #start_urls = ['http://ruanjian.2345.cc/list/0_0_2_1.html']
    redis_key = 't2345:start_urls'
    rules = (
        Rule(LinkExtractor(allow=r'/list/.+\.html'), callback='parse_item', follow=False), #跟进全部选择true
    )
    def parse_item(self, response):
        download_url = response.xpath("//div[@class='cont']//ul//a[@class='btn-b']/@href").extract_first()
        name = response.xpath("//div[@class='cont']//ul//em/text()").extract_first()
        update_time = response.xpath("//div[@class='cont']//ul//span[@class='ml10']/text()").extract_first()
       # print(update_times)
        yield {
            "download_url":download_url,
            "name":name,
            "update_time":update_time
        }

运行爬虫

  1. 打开redis服务器

  2. 在爬虫服务器上,进入爬虫文件所在的路径,然后输入命令:

    scrapy runspider [爬虫名]

    出现以下字样则表示连接成功

  3. 在redis数据库(通过redis-cli)中推入一个url链接:

    lpush test:start_urls 开始爬取的url
  4. 通过图形化界面查看redis数据库

数据导入到mongoDB中

  1. 等爬虫结束后,如果要把数据存储到mongoDB中,就应该新建一个process_items.py文件(命名任意):

    import redis
    import json
    import pymongo
    redis_clinet = redis.Redis(host='localhost',port=6379,db=0)
    mongo_client = pymongo.MongoClient()
    collection = mongo_client.t2345.softInfo
    while True:  #实时取出数据,数据取出后redis数据库中对应数据删除
     key,data=redis_clinet.blpop(['2345_spider:items'])   #爬虫名称
     print(key)
     d = json.loads(data)
     collection.insert_one(d)
  2. 打开mongoDB服务器

  3. 查看mongoDB数据库


   转载规则


《分布式爬虫》 fightingtree 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录