一. 摘要
本系统主要包括三大部分,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写序列图。。。。还真的是有点难度了,顺序不太对,先凑合着帮助理清功能了。。。。。
数据流程图(仅限帮助理解,很不标准, 嗯。。给自己找个借口好了。。。。MAC上没有好的画图工具。。。hia hia hia~~~)
4.1 Scrapy爬虫部分
爬虫流程图:
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
- 入库处理(在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。我是不会轻易放弃该系统的,毕竟可以用来学习好多东西呢,部分功能也在不断的完善中,路过的大神们有什么好的建议或想法欢迎指导。用这个做入门练手的如果哪里不明白也欢迎在评论区里提问。如果需要某一模块的完整搭建步骤的也可以在评论区里提建议,我会认真考虑哒~~~