分布式爬虫调度及数据管理系统[Python]

一. 摘要

本系统主要包括三大部分,Scrapy爬虫部分,Flask任务调度API部分及Django后台管理部分。三大部分相对独立又有一些内在联系。如果想单独学习某一框架可以单独只看某一部分,非常适合刚接触某框架并想深入学习的同学拿来练手。

  • 本系统通过对航空公司官方网站的分析,利用Scrapy,Requests等多种方式来探索获取对应的航班信息并处理入库,异步高并发的架构提高了数据获取的速率。
  • 用Flask+Redis做接口来监控处理爬虫,处理分发任务,以分布式的结构做到保证任务可以不间断,且不受单个爬虫意外的影响。
  • 通过Django来做管理后台展示数据,Django自带的人性化的管理后台以及认证系统提高了软件开发效率。

最终实现了分布式任务调度,爬虫管理员对数据的监控管理,对爬虫的监控管理,对爬虫机子的监控管理,对爬虫任务的监控控管理,普通用户对航班机票信息的搜索查看,对自己关注信息的查看管理等, 这个系统也是本人的毕业设计,所以本篇博客大部分摘自本人的简化版的毕业论文。


二. 数据库及技术框架

数据库以及技术框架介绍网上一搜一大堆,在这里我就不多作介绍了,只是简单说明一下我用到了什么和为什么要用它。

2.1 数据库

该系统的爬虫任务, 主机信息和爬虫信息存储于Redis数据库。这些数据都有更新频繁的特征,爬虫任务还需要以队列的形式来提取和存入。爬虫获取到的航班信息数据都统一存储于MySQL数据库,因为在更新航班信息的时候需要判断这条航班信息是否有用户关注,如果有则发送到该用户的邮箱。所以这些数据需要存储于关系型数据库。

2.2 Scrapy

本系统的爬虫部分包括两个网站的爬虫,通过Python的爬虫框架Scrapy来搭建,Scrapy的异步多线程可以极大地提高爬虫获取数据的速率,提高开发效率。

2.3 Flask

本系统的分布式任务分发是通过Scrapy封装Redis任务队列来实现的,分布式以及各功能的API实现不需要前端界面,不需要复杂的功能。Python以“微”著称的Web框架Flask就是最好的选择了。

2.4 Django

在新闻环境中诞生的web框架Django, 最擅长于动态内容管理系统。本系统的航班信息,机子信息,爬虫信息都要求比较高的时效性,所以其数据管理web端用Django来做是最好不过了。

2.5 Bootstrap

数据不仅要对管理员展示,还要展示给普通用户,这时一个高大上的界面就非常有必要了。对于我这个后台汪+运维狗来说,让我设计并编写一个高端、大气上档次的前端页面,还不如直接杀了我。幸好我遇见了它——Bootstrap,后台人员的福音呐。只需要在网上下载一个,简单复制一下,再配置成自己需要的即可。


三.功能描述

3.1 爬虫功能描述

  • 从任务队列获取任务
  • 添加cookies, 头信息,UserAgent来伪装自己
  • 通过爬取指定目标网站来获取需要的数据
  • 对数据进行处理,格式化
  • 判断库里是否有数据,有的话就更新,没有就插入数据库
  • 定时心跳,证明自己是活着的,同时获取控制命令并执行
  • 针对有人关注的航班信息,如果有价格座位数的更新则发邮件通知

还有一个要和爬虫同时运行在爬虫机子的脚本,主要实现以下功能:

  • 获取本机的内存信息
  • 获取本机的CPU信息
  • 将本机的信息发送给API请求部分
  • 接收命令任务
  • 执行接收到的命令

3.2 分布式调度及API部分的功能描述

  • 列表内容
  • 添加爬虫任务
  • 暂停爬虫任务
  • 恢复爬虫任务
  • 获取所有爬虫任务
  • 获取某爬虫对应的部分小任务
  • 给爬虫主机添加命令
  • 获取某主机对应的所有命令

3.3 后台的功能描述

管理员登录到系统后台才可进行操作,其可执行的操作如下:

  • 查看航班信息
  • 修改航班信息
  • 删除某个航班信息
  • 查看用户信息
  • 删除用户
  • 更改用户信息
  • 查看特别关注
  • 修改特别关注
  • 删除特别关注
  • 查看爬虫机子信息
  • 更新爬虫机子上的爬虫程序
  • 对任务进行操作管理
  • 对爬虫进行操作管理

普通用户登录到系统才能进行操作,可进行的操作如下:

  • 查看航班信息
  • 搜索航班信息
  • 添加到特别关注
  • 删除特别关注

四. 系统的功能设计

系统整体序列图
用Markdown写序列图。。。。还真的是有点难度了,顺序不太对,先凑合着帮助理清功能了。。。。。

Created with Raphaël 2.1.2 爬虫 爬虫 API API 数据库 数据库 管理后台 管理后台 关注用户 关注用户 请求任务 从队列里pop对应的任务 返回任务给爬虫 插入任务,爬虫,机子信息等 获取所有的数据信息 返回并展示所有数据信息 插入数据 对应数据的关注者 发送邮件

数据流程图(仅限帮助理解,很不标准, 嗯。。给自己找个借口好了。。。。MAC上没有好的画图工具。。。hia hia hia~~~)
整体数据流程图

4.1 Scrapy爬虫部分

爬虫流程图:

Created with Raphaël 2.1.2 开始 获取任务 获取数据 分析并处理数据 数据库中是否有该数据 是否有人关注该数据 发送邮件通知 更新数据库中的数据 结束 插入该数据 yes no yes no

4.2 分布式和后台管理

分布式API部分和后台管理序列图均在章首的序列图里,这里就不再多作介绍了。。。画图好艰难呀。。。。。。


五. 功能实现

5.1 爬虫部分

本来是有两个网站的爬虫,然后其中的一个反爬策略变了。。。加上的话就有点小复杂,不适合作入门教程了,就给删掉了,如有需要可以私信我。
爬虫爬取效果
部分代码实现:
1. 数据解析处理(在项目的myspiders下的spiders下):

def parse(self, response):
        try:
            content = json.loads(response.text)
        except:
            self.isOK = False
            logging.info('change IP....')
            return
        if 'inbound' in content.keys():
            return
        self.isOK = True
        li = content['outbound']
        for i in range(len(li)):
            outbound = li[i]
            item = MySpidersItem()
            if outbound['totalHighestAdultGrossFare'] == 0:  # 该航班机票已售空
                continue
            if 'low' not in outbound.keys():  # 依次查找此时的最低票价
                if 'medium' not in outbound.keys() or outbound['medium']['flights'][0]['isEligible'] is False:
                    low = outbound['high']
                else:
                    low = outbound['medium']
            else:
                low = outbound['low']

            flights = low['flights']
            departTime = outbound['departDate'][:10] + ' ' + outbound['departureTime'] + ':00'
            ti = time.mktime(time.strptime(departTime, '%Y-%m-%d %H:%M:%S'))
            row = {}
            row['depTime'] = ti
            destTime = outbound['arriveDate'][:10] + ' ' + outbound['destinationTime'] + ':00'
            ti = time.mktime(time.strptime(destTime, '%Y-%m-%d %H:%M:%S'))
            row['arrTime'] = ti
            row['depAirport'] = outbound['depart']
            row['arrAirport'] = outbound['dest']
            row['currency'] = outbound['currency']
            row['adultPrice'] = float(low['totalAdultGrossFare'])
            row['adultTax'] = float(low['totalAdultTaxes'])
            row['netFare'] = float(low['totalAdultNetFare'])
            num = outbound['flightCount']
            segments = []
            if num > 1:  # 判断是否有中转
                continue
            else:
                segments.append(self.analysisSegment(flights[0].copy()))
                row['maxSeats'] = flights[0]['seatsAvailable']
                row['cabin'] = flights[0]['fare']['fareClass']
                row['carrier'] = flights[0]['flightNumber'][:2]
                row['flightNumber'] = flights[0]['flightNumber']
                row['isChange'] = 1
            row['segments'] = json.dumps(segments)
            row['getTime'] = time.time()
            item.update(row)
yield item
  1. 入库处理(在myspiders项目的myspiders里的pipeline里, 更新发邮件什么的也在这个文件里)
def addRow(self, tablename, rowdict):
        rowdict['addTime'] = rowdict.get('getTime')
        keys = rowdict.keys()
        columns = ", ".join(keys)
        values_template = ", ".join(["%s"] * len(keys))

        sql = "insert into %s (%s) values (%s)" % (tablename, columns, values_template)
        values = tuple(rowdict[key] for key in keys)
        try:
            self.cursor.execute(sql, values)
            logging.info('%s:%s->%s at %s' % (rowdict.get('flightNumber'), rowdict.get('depAirport'), rowdict.get('arrAirport'), time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(rowdict.get('depTime')))))
        except:
            traceback.print_exc()
            logging.error('add error')

完整代码见myspiders

5.2 任务调度及API部分

各种API实现各种小功能,这里就不多加阐述了,重点看任务调度部分。在这里呢,我用的是flask_apscheduler, 一个强大的Flask定时任务包。
主要代码实现:

# 添加任务
@app.route('/addjob', methods=['GET', 'POST'])
def add_job():
    data = {}
    carrier = request.args.get('carrier').lower()
    if not carrier:
        data['status'] = 1
        data['msg'] = 'no carrier, add job error!!!'
    else:
        jobs = scheduler.get_jobs()
        for job in jobs:
            if job.id == carrier:
                break
        else:
            key = settings.STATIC + carrier
            db.set(key, 0)
            scheduler.add_job(func=push_task, id=carrier, trigger='interval', seconds=10, args=[carrier])
            data['status'] = 0
            data['msg'] = 'carrier %s has added !' % carrier
        data.setdefault('status', 2)
        data.setdefault('msg', "carrier '%s' had existed !!!" % carrier)
return json.dumps(data)

# 任务执行函数
def push_task(carrier=None):
    if not carrier:
        return
    key = settings.TASK + carrier
    if not db.llen(key) or db.llen(key) < 1000:
        # inputFile = open(str(settings.BASE_DIR + 'src/' + '%s.csv' % carrier), 'r')
        inputFile = open(os.path.join('src', '%s.csv' % carrier), 'r')
        inputReader = csv.reader(inputFile)
        ports = list(inputReader)
        inputFile.close()
        today = datetime.utcfromtimestamp(time.time())
        for diff in range(1, 11):
            date_str = (today + timedelta(days=diff)).strftime('%Y%m%d')
            for port in ports:
                print('%s-%s:%s' % (port[0], port[1], date_str))
                db.rpush(key, '%s-%s:%s:1' % (port[0], port[1], date_str))
    else:
        print('%s is full' % carrier)

本部分完整代码见spider_flask_api

5.3 管理后台

前端用的bootstrap, 下载地址sb-admin-bootstrap, 效果查看
在本系统中的部分界面如下
登录界面
注册界面
 数据展示
数据模型构建代码:

# 数据模型
class Tickets(models.Model):
    flightNumber = models.CharField('航班号', max_length=20)
    depTime = models.IntegerField('起始时间')
    arrTime = models.IntegerField('到达时间')
    depAirport = models.CharField('起始机场', max_length=6)
    arrAirport = models.CharField('到达机场', max_length=6)
    currency = models.CharField('货币', max_length=6)
    adultPrice = models.IntegerField('价格', default=0)
    adultTax = models.IntegerField('税价', default=0)
    netFare = models.IntegerField('净票价', default=0)
    maxSeats = models.IntegerField("座位数", default=0)
    cabin = models.CharField('舱位', max_length=10)
    carrier = models.CharField('航司', max_length=5)
    isChange = models.IntegerField('是否中转')
    getTime = models.IntegerField('更新时间')
    addTime = models.IntegerField('添加时间')
    segments = models.TextField('航班信息')
    user = models.ManyToManyField('auth.User', blank=True, null=True)

    def depDatetime(self):
        return time.strftime('%Y-%m-%d %H:%M', time.localtime(self.depTime))

    def arrDatetime(self):
        return time.strftime('%Y-%m-%d %H:%M', time.localtime(self.depTime))

    def getDatetime(self):
        return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.getTime))

    def __str__(self):
        return self.flightNumber

    class Meta:
        verbose_name = '机票信息'
        verbose_name_plural = '机票信息管理'
        ordering = ['getTime']

    depDatetime.short_description = '出发时间'
    arrDatetime.short_description = '到达时间'

    dep, arr = property(depDatetime), property(arrDatetime)

# 主机模型
class Host(models.Model):
    host_name = models.CharField('host_name', max_length=50)
    mem_total = models.CharField('mem_total', max_length=50)
    mem_free = models.CharField('mem_free', max_length=50)
    mem_available = models.CharField('mem_available', max_length=50)
    cpu_Mhz = models.CharField('cpu_Mhz', max_length=50)
    model = models.CharField('model', max_length=50)
    update_time = models.DateTimeField('update_time', auto_now=True)

    def update_datetime(self):
        return self.update_time.strftime('%Y-%m-%d %H:%M:%S')

# 爬虫模型
class Spider(models.Model):
    carrier = models.CharField('carrier', max_length=50)
    host = models.CharField('host', max_length=50)
    num = models.IntegerField('num', default=1)
    last_time = models.DateTimeField('update_time', auto_now=True)
    permins = models.IntegerField('items/min', default=0)

    def last_datetime(self):
        return self.last_time.strftime('%Y-%m-%d %H:%M:%S')

# 任务模型
# static==0: 正常运行
# static==1: 暂停状态
# static==3: 还没添加
class Task(models.Model):
    carrier = models.CharField('carrier', max_length=50)
    num = models.IntegerField('host', default=0)
    update_time = models.DateTimeField('update_time', auto_now=True)
    static = models.IntegerField('static', default=0)

    def update_datetime(self):
        return self.update_time.strftime('%Y-%m-%d %H:%M:%S')

    def static_str(self):
        return settings.TASK_STATIC[self.static]

# 关注模型
class Concerned(models.Model):
    depAirport = models.CharField('出发机场', max_length=10, null=True)
    arrAirport = models.CharField('到达机场', max_length=10, null=True)
    flightNumber = models.CharField('航班号', max_length=10, null=True)
    ticket = models.ForeignKey(Tickets, blank=True, null=True)
    depTime = models.DateTimeField('出发日期', auto_now=True)
    user = models.ForeignKey('auth.User', blank=True, null=True)

    def __str__(self):
        return self.user

    def get_username(self):
        return self.user.username

    class Meta:
        verbose_name = '特别关注'
        verbose_name_plural = '特别关注管理'

本部分完整代码见ticket_info_manage

六. 小结

大学四年算是完整的结束了,这个项目耗尽了我一个月的心血,毕业答辩的前一天晚上实现了三个部分相互之间的各种交互,早上六点搞定全部的时候觉得自己都要猝死了。。。然而答辩的时候老师就问了下爬虫。。。。。。吐血。。。。。其实这个系统还有很多不够完善的地方,功能实现的方法也还有点low。我是不会轻易放弃该系统的,毕竟可以用来学习好多东西呢,部分功能也在不断的完善中,路过的大神们有什么好的建议或想法欢迎指导。用这个做入门练手的如果哪里不明白也欢迎在评论区里提问。如果需要某一模块的完整搭建步骤的也可以在评论区里提建议,我会认真考虑哒~~~

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:C马雯娟 返回首页