针对团购网站餐饮类消费数据的可视分析系统设计与开发
大多数消费者在选择团购网站消费后会做出相应评价,从而产生海量的交易数据。这些数据包含了消费者对饮食比较全面的主观性评价和量化评分,因此通过对此类餐饮数据的分析能够有效洞悉城市餐饮消费行为。而由于该类数据体量大、数据类型多等特点,使得传统的数据分析技术已经难以有效进行分析处理。如何将可视分析技术应用于团购网站餐饮类数据分析,探索城市消费行为是一个新颖的研究课题。提供某团购网站绵阳市3444家餐饮类店铺数据的基本信息及351941条评论信息,其中164982位用户参与评论。
设计开发针对团购网站餐饮类消费数据的可视分析系统,实现功能包括但不限如下:
(1)呈现城市餐饮店铺时空特征分布和热门店铺特色美食;
(2)挖掘城市餐饮消费行为的地域特征倾向和时序特征,店铺消费关联关系分析;
(3)支持针对自定义消费条件的个性化推荐;
字段
店铺ID 店铺名称 平均得分 地址 电话 营业时间 其他信息 经纬度 平均价格 品牌ID 品牌名称 展示状态 安全档案 店铺标签
字段
用户ID 平均价格 评论 图片URL 评论时间 点赞数 用户名 店铺评分 评论ID 是否匿名 店铺ID 用户等级
第一个功能:
呈现城市餐饮店铺时空特征分布和热门店铺特色美食
呈现店铺时空特征分布我选择使用百度地图来实现,采用这种方式来实现地图比较方便,对于店铺的地理位置绘制、店铺基本信息展示、店铺所属区域绘制、路线规划都能够做到,相比于简单的echarts地图更加强大。
热门店铺美食在shop_details.csv文件中并没有,所以只有自己来爬取数据。
百度地图api用的是BD09经纬度坐标,要想得到BD09坐标需要使用百度api来对美团上爬取的经纬度坐标进行转换(正好可以在爬取数据时对坐标系进行转换)
美团的移动端(https://i.meituan.com/)爬取相对容易,但是店铺的信息比较少
所以还是在桌面端爬取数据https://my.meituan.com/
店铺详情页面的url通过拼接店铺id获取(https://my.meituan.com/meishi/4615402),现在就是直接访问。就是一个简单的get请求,但是要带上完整的cookie,cookie有问题的话很快会弹验证码。一个cookie可以爬1000次后才会出现验证码,但是也有几百次出现的。我是通过手动更换cookie和ip来爬取的数据。
具体想解决该问题可以参考:https://blog.csdn.net/xing851483876/article/details/81842329
爬虫spider.py
# -*- coding: utf-8 -*-
# @Author : f
# @File : spider.py
import requests
import pandas as pd
import re
import csv
import json
import util
import time
'''
根据店铺id获取特色美食(名称、图片地址)、店铺分类
并保存到csv文件中
'''
comment_data_path = ".\static\data\shop_comments.csv"
shop_data_path = ".\static\data\shop_details.csv"
comment_df = pd.read_csv(comment_data_path)
shop_df = pd.read_csv(shop_data_path)
# 获取店铺的id列表
def get_id():
return list(shop_df['poiId'])
def get_detail_byId(id):
'''
:param list_id: 店铺id
:return: 店铺信息
'''
base_url = "https://my.meituan.com/meishi/"
open_url = base_url+str(id)
headers = {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Host': 'my.meituan.com',
'Referer': 'https://gz.meituan.com/meishi/',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
"Cookie":"_lxsdk_cuid=176a76e5465c8-08c7fda338591a-366b4108-144000-176a76e5466c8; _hc.v=47584a75-cd95-5d49-4774-f90292228934.1609128422; iuuid=DE08D7129F1C9300F442BD3F534FE1E9BEB5960DC89FC964B06DE75D92AF59FF; _lxsdk=DE08D7129F1C9300F442BD3F534FE1E9BEB5960DC89FC964B06DE75D92AF59FF; webp=1; __utma=74597006.1600449216.1609384800.1609384800.1609384800.1; __utmz=74597006.1609384800.1.1.utmcsr=link.csdn.net|utmccn=(referral)|utmcmd=referral|utmcct=/; ci=306; cityname=%E7%BB%B5%E9%98%B3; latlng=31.540156,104.689433,1609384831294; i_extend=C019032296837928515275757042931456002187_c14_e76093ef0e7669cc9c26a543f38b45487GimthomepageguessH__a; lsu=; __mta=221682599.1609128395200.1609636290109.1609637806937.14; client-id=aff308ba-8320-4796-8a4c-1303c58c5136; uuid=099f6650-8b35-423a-a6ea-3d226bb37422; _lx_utm=utm_source%3Dlink.csdn.net%26utm_medium%3Dreferral%26utm_content%3D%252F; lat=31.504214; lng=104.784832; _lxsdk_s=176ccfd3748-f1a-79a-0bb%7C%7C2"
}
res = requests.get(open_url,headers=headers)
res.encoding = "utf-8"
l = [id] # 存储店铺信息的列表
pattern = ""
rec = re.compile(pattern) # 预编译
if rec.search(res.text):
json_str = rec.search(res.text).groups()
for j in json_str:
d = json.loads(j)
l.append(d['detailInfo']['name'])
l.append(d['crumbNav'])
l.append(d['recommended'])
l.append(d['detailInfo']['avgScore'])
l.append(d['detailInfo']['address'])
l.append(d['detailInfo']['phone'])
l.append(d['detailInfo']['openTime'])
l.append(d['detailInfo']['avgPrice'])
result = util.wgs84tobd09(d['detailInfo']['longitude'],d['detailInfo']['latitude'])
l.append(result[0]['x'])
l.append(result[0]['y'])
return l
#保存店铺信息
def save_info(id_list):
'''
:param id_list: 店铺id列表
:return:
'''
with open("../static/data/shop_details02.csv","a+",encoding="utf-8",newline="") as f:
# 2. 基于文件对象构建 csv写入对象
csv_writer = csv.writer(f)
# 3. 构建列表头
csv_writer.writerow(["poiId", "name", "type","recommended","avgScore","address","phone","openTime","avgPrice","longitude","latitude"],newline="")
# 4. 写入csv文件内容
for id in id_list:
info_list = get_detail_byId(id)
print(info_list)
csv_writer.writerow(info_list)
def main():
print("------{} 开始爬取数据------".format(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))))
id_list = get_id()
save_info(id_list)
print("------{} 爬取数据结束------".format(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))))
if __name__ == '__main__':
main()
1)、店铺类型
获取到的数据是页面面包屑导航的数据,类似于
[{‘title’: ‘绵阳美团’, ‘url’: ‘http://my.meituan.com/’}, {‘title’: ‘绵阳美食’, ‘url’: ‘http://my.meituan.com/meishi/’}, {‘title’: ‘绵阳火锅’, ‘url’: ‘http://my.meituan.com/meishi/c17/’}]
需要的数据就是三级导航中的文本数据
基本数据格式都是地名+店铺类型
所以我对店铺种类进行了处理
# 处理店铺类别
def convert_shop_type(type_str):
str = eval(type_str)[2]['title'][2:]
if str[0:1] == "县" or str[0:1] == "区":
return str[1:]
return str
2)、店铺推荐菜
对于推荐菜的处理比较简单,爬取的推荐菜数据都有几十条,只取其中热门前三条
3)、店铺经纬度
百度地图api用的是BD09经纬度坐标
要想得到BD09坐标需要使用百度api来对美团上爬取的经纬度坐标进行转换
# 坐标系转换
def wgs84tobd09(lon,lat):
api_url = "http://api.map.baidu.com/geoconv/v1/?coords={},{}&from=1&to=5&ak=k4NUsxZb6DuuOxQOoZqneCKRPp3St76v".format(lon,lat)
res = requests.get(api_url)
d = json.loads(res.text)
return d["result"]
4)、店铺营业时间
对于店铺营业时间我只取了一个时间段,而且视作无休
只考虑一整天的营业时间
营业时间的格式类似于
周一至周日 10:00-13:00 16:00-21:30
由于每家店的营业时间格式都不太同,所以我就提取了其中的一段
比如上面这条数据就只提取了10:00-13:00
并由-把时间段分隔开分别进行处理
比如10:00的处理
106+0/10=60
13:00的处理
136+0/10=78
那么处理数据后店铺字段commentTime_convert={
“start”:60,
“end”:78
}
前端通过控制datazoom来控制整个地图的时间轴,时间轴的两边是1-100
但是数据的显示跨度是144(按照0:00到24:00 每10分钟作一个刻度,所以上面的数据处理就是根据这来的)
当datazoom改变时分别将开始的值和结束的值乘以1.44 然后在遍历地图所有的点进行一一对比筛选出符合营业条件的点
处理代码:
后端:
# 处理店铺营业时间
def convert_openTime(openTime):
start = 0
end = 0
if isinstance(openTime, str):
l = openTime.split(" ")
if len(l) > 1:
if l[1] == "全天" or l[1] == "周一至周日" or l[1] == "周五至周日":
start = 0
end = 144
else:
start_str = l[1].split("-")[0]
end_str = l[1].split("-")[1]
start = int(start_str.split(":")[0]) * 6 + int(start_str.split(":")[1]) / 10
end = int(end_str.split(":")[0]) * 6 + int(end_str.split(":")[1]) / 10
else:
start = 0
end = 144
return {"start":start,"end":end}
前端:
myChart.on('dataZoom', function(e) {
// console.log(e); // All params
time_start = e.start * 1.44;
time_end = e.end * 1.44;
time_mapPoints = [];
for(var i = 0;i<mapPoints.length;i++){
start = mapPoints[i].openTime_convert.start;
end = mapPoints[i].openTime_convert.end;
if(start < end){
if(start > time_start && end < time_end){
time_mapPoints.push(mapPoints[i])
}
}else{
// start 120 end 20
if(start < time_start){
time_mapPoints.push(mapPoints[i])
}else if(end > time_end){
time_mapPoints.push(mapPoints[i])
}
}
}
map.clearOverlays(); //删除所有点
overlays.length = 0;// 清空矩形区域数组
// 遍历mapPoints创建标注点
createMarks(time_mapPoints);
});
5)、用户评论时间
爬取的用户评论时间是毫秒数,需要将其转换成具体的年份和月份来对每个月的评论量作统计
# 将时间戳转成指定的日期格式
def convert_date(timeStamp):
timeArray = time.localtime(timeStamp)
otherStyleTime = time.strftime("%Y年%m月", timeArray)# %Y年%m月%d日 %H:%M:%S
# 2013--10--10 23:40:00
return otherStyleTime
整个可视化系统由大体的5个部分组成,主要采用的可视化布局方法有:echarts、d3、百度地图。
整体围绕中间的大地图,该大地图的实现采用的百度地图api,采用这种方式来实现地图比较方便,对于店铺的绘制、店铺所属区域绘制、路线规划都能够做到,相比于简单的echarts地图更加强大。
左上角采用的echarts 词云,显示绵阳店铺种类,关键词云是对海量文字内容中出现频率较高的“关键词”的视觉突出,即出现越多的“关键词”字体越大,这样更能突出绵阳市店铺种类特征。
左下角采用echarts折线图,显示每一年每一个月的用户评论量,主要用于呈现餐饮消费行为的时序特征。
右上角个性化推荐,通过筛选用户选择的选项来对店铺进行呈现
右下角采用echarts柱状图,对框选区域进行数据比较
1)、点击左上角店铺分类词云,对应地图同类型店铺所在地作高亮显示以及左下角折线图显示该类近六年的对月份作划分的评论情况,借此可以看出该类型店铺在一年中的具体热卖月份来分析餐饮消费行为的时序特征。
2)、地图上店铺所在位置高亮并提供点击显示店铺名、地址、营业时间、平均消费等,以及对路线规划的选择。
3)、通过地图下方的时间轴来重置地图中的内容,以及通过店铺的营业时间来对时间分布进行分割,可选择时间跨度以及时间范围。(时间轴操作优先级最高,其余操作需先固定时间轴
4)、通过右上角个性化推荐的勾选可以实现对于店铺评分、预算、类别以及定位做导航操作(导航前需先定位所在位置)
5)、进行定位操作后会将自身现所在位置在地图上高亮跳动显示并文本提示,并在右上角个性化推荐中目前所在地项显示(只显示到所在区)
6)、在成功进行定位及导航操作后,会提示路线规划成功并计算出路线全长,地图上显示规划好的路线(红色虚线表示)。
7)、地图右上角有框选操作,通过框选区域店铺,使右下角柱状图变化,通过区域对比来探索餐饮消费行为的地域特征,包括店铺评论量、店铺量、店铺种类。(只可实现对三个区域进行对比)可通过地图右上角的清除矩形区域来重新绘制需要对比的区域。