美團商品 評論 標簽 數據分析可視化呈現
代碼倉庫:https://github.com/SKPrimin/PythonCrawler/tree/master/%E7%BE%8E%E5%9B%A2
需求分析
引言
當我們想點外賣的時候,往往不由自主的優先選擇外賣平台優先推薦的店鋪,但也因此時長踩坑。在進行看三個月的Python學習后,我們嘗試對外賣平台的數據進行下載解析,自己進行數據分析,並為以后的選擇提供參考
實現方式
首先是要獲取數據,訪問美團的網站並對頁面進行檢查調試,發現美團網站使用的是動態數據加載,數據加載時瀏覽器上方顯示的網址並為發生改變,且在實踐中發現第一頁沒有數據,到了第二頁才開始有數據,進行字典解析,並保存到文件中
接下來是對數據進行分析,使用numpy,pandas對文件數據進行讀取解析,並使用matlibplot進行可視化呈現。
商店海選
商店數據獲取
打開美團官網,發現網址並未隨着頁面變化而變化,經過仔細搜尋仍未發現json數據,感到第一頁數據是隨着頁面一起送達,直接融合在HTML文件中。於是打開第二頁尋找,果然發現了整齊划一的數據,並得到了數據請求網址。
在這個頁面中,有整齊的數據
,
這些字典格式規整、數據完整,由列表與字典層層嵌套而成。數據格式如下
{code: "0", data: {totalCount: 150, template: "pcfood",…}}
code: "0"
data: {totalCount: 150, template: "pcfood",…}
cardExtension: {}
extraInfoCinema: null
recommendResult: []
searchResult: [{id: 682214201, template: "pcfood",…}, {id: 2636210, template: "pcfood",…},…]
0: {id: 682214201, template: "pcfood",…}
···
areaname: "高新區"
avgprice: 15
avgscore: 3.6
backCateName: "奶茶/果汁"
id: 682214201
imageUrl: "http://p0.meituan.net/w.h/biztone/b8df853928aacac04c8add55075cb030325791.jpg"
latitude: 31.8328
longitude: 117.1324
lowestprice: 6.66
···
showType: "food"
tag: []
template: "pcfood"
title: "茶百道(高新銀泰店)"
至此我們找到了解析的方式,使用requests發送請求獲取數據,之后使用字典列表對數據進行解析,處理過程中發現這就是大名鼎鼎的json
https://apimobile.meituan.com/group/v4/poi/pcsearch/56?uuid= &userid= &limit=32&offset=32&cateId=-1&q=%E5%A5%B6%E8%8C%B6&token= 在這個網址中,位於?之后的都是參數。可以放在param字典中待請求時自動處理
這里在編寫參數字典時可以使用我們學的正則表達式,(.*?):(.*)、'$1':'$2',直接實現替換,該方法能將冒號:兩邊的字符串都加上引號並在最后加上逗號
本程序的流程圖如下

源代碼如下
import csv
import requests
def requestData(q):
url = 'https://apimobile.meituan.com/group/v4/poi/pcsearch/56?'
# userid為自己的用戶名
# limit為請求的數據條數
# offset 偏移量
# q 為搜索項
param = {
'uuid': '',
'userid': '',
'limit': '50',
'offset': '50',
'cateId': '-1',
'q': q,
'token': '',
}
# User-Agent:表示瀏覽器基本信息
# Cookie: 用戶信息,檢測是否有登陸賬號
# Referer: 防盜鏈,從哪里跳轉過來的請求url
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36',
'Cookie': '',
'Referer': 'https://hf.meituan.com/',
}
try:
# 將參數、表頭加載后發送請求
response = requests.get(url=url, params=param, headers=header)
# 反饋的數據進行json格式解析
data_json = response.json()
# pprint.pprint(datajson) # 標准格式打印 使用時需要import pprint
return data_json
except Exception as e:
print("requests請求失敗" + str(e))
def parseData(data):
"""對得到的json數據進行解析"""
# 根據此前對數據的分析結果,searchResult值 位於data字典中,是一個列表形式數據
searchResult = data['data']['searchResult']
with open("{}.csv".format(q), mode="a", encoding='utf-8', newline="") as f:
csvpencil = csv.DictWriter(f, fieldnames=['店鋪名', '店鋪所在位置', '人均消費', '評分', '美食名稱', '店鋪圖片鏈接',
'緯度', '經度', '最低價格', '店鋪ID', '店鋪詳情頁'])
csvpencil.writeheader() # 寫入表頭
try:
# 對searchResult列表進行索引解析,其內容是以字典形式存放,我們提取時也以字典存儲
for item in searchResult:
data_dict = {
'店鋪名': item['title'],
'店鋪所在位置': item['areaname'],
'人均消費': item['avgprice'],
'評分': item['avgscore'],
'美食名稱': item['backCateName'],
'店鋪圖片鏈接': item['imageUrl'],
'緯度': item['latitude'],
'經度': item['longitude'],
'最低價格': item['lowestprice'],
'店鋪ID': item['id'],
'店鋪詳情頁': f'https://www.meituan.com/meishi/{item["id"]}/'
}
csvpencil.writerow(data_dict)
except Exception as e:
print("數據解析失敗" + str(e))
if __name__ == '__main__':
q = "奶茶"
parseData(requestData(q))

我們獲得了288條數據數據,接下來是對數據進行分析
商店數據分析
相關模塊導入
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = ['STFangsong'] # 指定默認字體 仿宋
文件數據讀入
使用pandas讀取信息並按列分開
df = pd.read_csv('奶茶.csv')
df.columns = ['店鋪名', '店鋪所在位置', '人均消費', '評分', '美食名稱', '店鋪圖片鏈接',
'緯度', '經度', '最低價格', '店鋪ID', '店鋪詳情頁']
print(df.shape[0]) #288
數據去重
Pandas提供了去重函數:drop_duplicates()
subset:默認為None去除重復項時要考慮的標簽,當subset=None時所有標簽都相同才認為是重復項;
keep:有三個可選參數,分別是 first、last、False,默認為 first,表示只保留第一次出現的重復項;
inplace:布爾值參數,默認為 False 表示刪除重復項后返回一個副本,若為 Ture 則表示直接在原數據上刪除重復項。
df.drop_duplicates(subset=None, keep='first', inplace=True)
print(df.shape[0]) #284
價格與評分線性回歸分析圖
通過畫出人均價格與評分之間的關系,我們可以清楚的觀察到是否越貴的東西越好,同時也可以引入最低價格,從另一個方面觀察它們之間的關系
線性回歸分析圖我們發現價格8-12的評分集中於高點,其它價格評分比較分散,線性方程計算而出的結果也顯示價格高的奶茶評分較高;
而最低價格與評分關系並不大,線性方程也表明兩者幾乎無關。
import seaborn as sns
# make data
fig, axes = plt.subplots(2, 1, figsize=(12, 12))
sns.regplot(x='人均消費', y='評分', data=df, color='red', marker='+', ax=axes[0])
sns.regplot(x='最低價格', y='評分', data=df, color='green', marker='*', ax=axes[1])
plt.show()

品牌統計
品牌可以從商店名中截取出來,使用集合、列表分別存儲,成為之后的鍵值對。我們計算出各品牌出現次數並從高到低排序,再使用柱狀圖可視化呈現
列表轉字典函數
以集合的形式輸入鍵,通過對傳入的列表統計計算出值
def countDict(aSet: set, aList: list, minValue=0) -> dict:
# 列表轉為字典,統計出aSet中每個元素出現的次數
aDict = {}
for item in aSet:
counter = aList.count(item)
if counter >= minValue: #去除僅出現一次的品牌,極有可能是商家打錯了
aDict[item] = aList.count(item)
return aDict
# 傳入數據
shopNames = df['店鋪名']
# 品牌列表,將截取出的品牌名存入列表
brandList = []
# 品牌名使用集合存儲,利用集合元素唯一性自動實現去重
brandSet = set()
for shopName in shopNames:
# 店鋪名一般是 “品牌(地址)” 的形式,通過中文符號'('來截取出品牌
index = shopName.find('(') # 返回(的位置,未找到返回-1'
# 三元表達式 ,如果沒有()就直接原樣返回
brand = shopName[:index] if index != -1 else shopName
brandSet.add(brand)
brandList.append(brand)
brandDict = countDict(brandSet, brandList, 2)
# 將字典按照元素值進行逆序排序
brandDict = dict(sorted(brandDict.items(), key=lambda i: i[1], reverse=True))
# make data:
x = brandDict.keys()
y = brandDict.values()
# plot
fig = plt.subplots(figsize=(18, 9))
colors = ['tomato', 'aqua', 'bisque', 'lightgreen', 'skyblue']
plt.bar(x, y, width=0.5, color=colors, edgecolor="white", linewidth=0.7)
plt.xlabel('品牌名稱')
plt.ylabel('品牌出現次數')
plt.show()

# 精簡數據,僅采用出現三次以上的數據
brandDict = countDict(brandSet, brandList, 3)
# Pie chart, where the slices will be ordered and plotted counter-clockwise:
labels = brandDict.keys()
sizes = brandDict.values()
# only "explode" the 4nd slice (i.e. 'Hogs')
explode = [0 for _ in range(len(sizes))]
explode[3] = 0.1
colors = ['tomato', 'aqua', 'bisque', 'lightgreen', 'skyblue']
fig1, ax1 = plt.subplots()
ax1.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%', colors=colors,
shadow=True, startangle=0)
ax1.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle.
plt.show()

地點統計
店鋪名稱中通常含有地址,正事中文括號中的部分,我們同樣可以通過字符串截取出地址,但由於個別商店格式並非如此,可能會出現未填地址的情況,則會直接跳過
# 傳入數據
shopNames = df['店鋪名']
# 品牌列表,將截取出的品牌名存入列表
placeList = []
# 品牌名使用集合存儲,利用集合元素唯一性自動實現去重
placeSet = set()
for shopName in shopNames:
# 店鋪名一般是 “品牌(地址)” 的形式,通過中文符號'('來截取出品牌
index = shopName.find('(') # 返回(的位置,未找到返回-1'
if index != -1:
place = shopName[index + 1:-1]
placeSet.add(place)
placeList.append(place)
placeDict = countDict(placeSet, placeList, 2)
# 將字典按照元素值進行逆序排序
placeDict = dict(sorted(placeDict.items(), key=lambda i: i[1], reverse=False))
placeData = pd.DataFrame({'地點': [x for x in placeDict.keys()],
'出現次數': [x for x in placeDict.values()]})
# make data: 豎行柱狀圖
x = placeData['地點']
y = placeData['出現次數']
# plot
fig = plt.subplots(figsize=(18, 9))
colors = ['tomato', 'aqua', 'bisque', 'lightgreen', 'skyblue']
plt.barh(x, y, color=colors, edgecolor="white", linewidth=0.7)
plt.xlabel('地點')
plt.ylabel('地點出現次數')
plt.show()

店鋪位置模擬
我們獲取了近300家店鋪的經緯度信息,雖然我們暫時無法直接定位到地圖上,但我們可以畫出經緯度散點圖,來大致推斷出哪里奶茶店較多。
合肥所在地區的緯度大約為北緯32°,不同緯度間距相同都是1緯度約110.94,北緯32°處的緯度周長為40075×sin(90-32),因此此處的1緯度約為94.40
經緯度比例為1.1752。通過計算得出,在畫布長寬都代表1.2°時畫布設置為20×17時比例1.1764較為接近,誤差僅有1.08%
在將我們所在位置帶入散點圖,即可發現奶茶店大多在我們的東方、北方,這與城市發展狀況也較為符合
# make the data
# 獲取經緯度信息
longitude = df['經度']
latitude = df['緯度']
# size and color:
sizes = np.random.uniform(15, 80, len(longitude))
colors = np.random.uniform(15, 80, len(latitude))
# plot
fig, ax = plt.subplots(figsize=(20, 17))
ax.scatter(x=longitude, y=latitude, s=sizes, c=colors, vmin=0, vmax=100)
ax.set(xlim=(116.9, 118.1), xticks=np.arange(116.9, 118.1, 0.1),
ylim=(31.2, 32.4), yticks=np.arange(31.2, 32.4, 0.1))
# 是否顯示網格,默認不顯示
ax.grid(True)
ax.set_xlabel("東經116.9——118.1")
ax.set_ylabel("北緯31.2——32.4")
plt.show()

商店決賽
經過海選,再結合地理位置,最終選出古茗(蜀山安大店)、滬上阿姨鮮果茶(安大磬苑校區店)、書亦燒仙草(簋街大學城店)、茶百道(大學城店)四家店鋪進行詳細的評論分析
后期在實踐中發現第二個店鋪始終無法獲取數據,最終發現是這家真的沒有評論數據,而第四家也着實沒有tags
評論獲取
經過查找,我們同樣找到了數據請求鏈接,也返現了返回的字典嵌套列表嵌套字典的數據層級,同時我們發現其會返回評論數量,這樣我們可以先進行第一次查詢,
接着根據返回的total值進行循環,一頁10個數據
{status: 0, data: {,…}}
data: {,…}
comments: [{userName: "", userUrl: "", avgPrice: 12, comment: "無語住了,就這水平還說,跑來一趟容易嗎",…},…]
0: {userName: "", userUrl: "", avgPrice: 12, comment: "無語住了,就這水平還說,跑來一趟容易嗎",…}
1: {userName: "UhH816306504", userUrl: "", avgPrice: 12,…}
comment: "此次用餐總體來說不錯,覺得比較認可的地方有:性價比、口味、環境、服務、團購接待。"
menu: "黃金珍珠奶茶1杯"
star: 50
···
···
tags: [{count: 35, tag: "味道贊"}, {count: 32, tag: "環境很好"}, {count: 31, tag: "飲品贊"}, {count: 27, tag: "服務熱情"},…]
0: {count: 35, tag: "味道贊"}
1: {count: 32, tag: "環境很好"}
···
total: 471
status: 0
代碼實現
import pprint
import time
import requests
import csv
def requestCommentTags(id, page):
url = 'https://www.meituan.com/meishi/api/poi/getMerchantComment?'
# userid為自己的用戶名
# limit 為一次請求數據,一次10條
# offset為偏移量,類似於頁數
# id 為店鋪id
param = {
'uuid': 'f79f1498663140408d8d.1638116128.1.0.0',
'platform': '1',
'partner': '126',
'originUrl': f'https://www.meituan.com/meishi/{id}/',
'riskLevel': '1',
'optimusCode': '10',
'id': id,
'userId': '2726751799',
'offset': page * 10,
'pageSize': '10',
'sortType': '1',
}
# User-Agent:表示瀏覽器基本信息
# Cookie: 用戶信息,檢測是否有登陸賬號
# Referer: 防盜鏈,從哪里跳轉過來的請求url,相當於定位地址
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36',
'Cookie': '',
'Referer': f'https://www.meituan.com/meishi/{id}/',
}
try:
# 將參數、表頭加載后發送請求
response = requests.get(url=url, params=param, headers=header)
# 反饋的數據進行json格式解析
data_json = response.json()
pprint.pprint(data_json) # 標准格式打印 使用時需要import pprint
return data_json
except Exception as e:
print("requests請求失敗" + str(e))
def parseComment(data):
"""對得到的json數據進行解析,選出評論"""
try:
# 根據此前對數據的分析結果,searchResult值 位於data字典中,是一個列表形式數據
comments = data['data']['comments']
# 對searchResult列表進行索引解析,其內容是以字典形式存放,我們提取時也以字典存儲
for item in comments:
comments_dict = {
'評論內容': item['comment'],
'購買商品': item['menu'],
'星級': item['star'],
}
# 逐行立刻寫入數據,以防出錯導致的前功盡棄,同樣是依照字典進行
commentpencil.writerow(comments_dict)
total = data['data']['total']
return total
except Exception as e:
print("評論數據解析失敗" + str(e))
def parseTags(data, shopName):
"""對得到的json數據進行解析,選出評論"""
try:
tags = data['data']['tags']
for tag in tags:
tags_dict = {
'店鋪': shopName,
'標簽': tag['tag'],
'數量': tag['count']
}
tagpencil.writerow(tags_dict)
except Exception as e:
print("標簽數據解析失敗" + str(e))
if __name__ == '__main__':
shops = {1088411800: '古茗(蜀山安大店)', 1479103527: '書亦燒仙草(簋街大學城店)', 1616840469: '茶百道(大學城店)'}
for shopId in shops.keys():
shopName = shops[shopId]
with open("{}.csv".format(shopName), mode="a", encoding='utf-8', newline="") as f, open("商店標簽信息.csv", mode="a",
encoding='utf-8',
newline="") as tf:
commentpencil = csv.DictWriter(f, fieldnames=['評論內容', '購買商品', '星級'])
commentpencil.writeheader() # 寫入表頭
tagpencil = csv.DictWriter(tf, fieldnames=['店鋪', '標簽', '數量'])
tagpencil.writeheader() # 寫入表頭
# 進行第一頁獲取
jsdata = requestCommentTags(id=shopId, page=0)
# 得到總評論數
total = parseComment(jsdata)
# 解析其tags,一家店鋪僅需一次
parseTags(jsdata, shopName)
pages = int(total / 10)
# 進行其他頁數獲取
for i in range(1, pages):
# 暫停三秒,模擬人瀏覽頁面正常翻頁
time.sleep(3)
parseComment(requestCommentTags(id=shopId, page=i))
數據截圖


評論數據分析
已經獲取了三家店鋪的評論數據,其中兩家又獲取了標簽,根據大家的評論進行更詳細的數據分析,第一步要做的依然是進行導包
import pandas as pd
import matplotlib.pyplot as plt
import wordcloud
plt.rcParams['font.family'] = ['STFangsong'] # 指定默認字體 仿宋
評論標簽分析
根據兩家店鋪評論標簽的數量進行分析
# 數據讀取
df = pd.read_csv('商店標簽信息.csv')
df.columns = ['店鋪', '標簽', '數量']
# 數據分離,將兩家店鋪信息拆開,並繪制餅狀圖
guming = df[df["店鋪"] == '古茗(蜀山安大店)']
shuyi = df[df["店鋪"] == '書亦燒仙草(簋街大學城店)']
# Pie chart, where the slices will be ordered and plotted counter-clockwise:
guminglabels = guming['標簽']
gumingsizes = guming['數量']
shuyilabels = shuyi['標簽']
shuyisizes = shuyi['數量']
colors = ['tomato', 'aqua', 'bisque', 'lightgreen', 'skyblue']
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
axes[0].pie(gumingsizes, colors=colors, labels=guminglabels, autopct='%1.1f%%',
shadow=True, startangle=90)
axes[0].set_title('古茗(蜀山安大店)')
axes[1].pie(shuyisizes, colors=colors, labels=shuyilabels, autopct='%1.1f%%',
shadow=True, startangle=90)
axes[1].set_title('書亦燒仙草(簋街大學城店)')
fig.suptitle("店鋪評論中標簽數量占比")
plt.show()

將兩家店鋪放入同一個折線圖中,需要將兩個標簽合並,選出其中相同的部分,其次由於評論數差異較大,本次使用不同y坐標軸
# 將兩個標簽轉成集合形式再進取並集
labelset = set(guminglabels) & set(shuyilabels)
# 再轉為列表從而實現有序,能夠迭代
xlabel = list(labelset)
gmlabel = []
sylabel = []
for label in xlabel:
x = guming[guming["標簽"] == label]
x = list(x['數量'])
gmlabel.append(x[0])
x = shuyi[shuyi["標簽"] == label]
x = list(x['數量'])
sylabel.append(x[0])
# 根據求出的三個列表畫出分y軸的折線圖
from mpl_toolkits.axes_grid1 import host_subplot
host = host_subplot(111)
par = host.twinx()
host.set_xlabel("標簽")
host.set_ylabel("古茗(蜀山安大店)")
par.set_ylabel("書亦燒仙草(簋街大學城店)")
p1, = host.plot(xlabel, gmlabel, label="古茗(蜀山安大店)")
p2, = par.plot(xlabel, sylabel, label="書亦燒仙草(簋街大學城店)")
leg = plt.legend()
host.yaxis.get_label().set_color(p1.get_color())
leg.texts[0].set_color(p1.get_color())
par.yaxis.get_label().set_color(p2.get_color())
leg.texts[1].set_color(p2.get_color())
plt.show()
#### 評論內容分析

詞雲圖
# 讀取三個評論數據文件
gmdf = pd.read_csv('古茗(蜀山安大店).csv')
gmdf.columns = ['評論內容','購買商品','星級']
sydf = pd.read_csv('書亦燒仙草(簋街大學城店).csv')
sydf.columns = ['評論內容','購買商品','星級']
cbddf = pd.read_csv('茶百道(大學城店).csv')
cbddf.columns = ['評論內容','購買商品','星級']
import jieba
def makewordscloud(df,name):
words = []
stopwords = ['可以','非常','哈哈哈','真的']
for i,comment in enumerate(df['評論內容']):
if pd.isnull(df.at[i,'評論內容'])==False:
comment.replace("\n",'')
wordlist = jieba.cut(comment)
for word in wordlist:
if len(word)>1 and word not in stopwords:
words.append(word)
font = r'C:\Windows\Fonts\simfang.ttf'
w = wordcloud.WordCloud(
font_path=font,
background_color='white',
width=3840,
height=2160,
)
w.generate(' '.join(words))
w.to_file(f'{name}.png')
# 三個店鋪的詞雲圖
# makewordscloud(gmdf,'古茗詞雲圖')
makewordscloud(sydf,'書亦燒仙草詞雲圖')
#makewordscloud(cbddf,'茶百道詞雲圖')
-
茶百道詞雲圖
-
古茗詞雲圖
-
書亦燒仙草詞雲圖
評分分布
由於茶百道數據過少,只有十幾條,我們接下來只使用 書亦燒仙草、古茗 兩份數據
列表轉字典函數
以集合的形式輸入鍵,通過對傳入的列表統計計算出值,與上文中相同
def countDict(aSet: set, aList: list, minValue=0) -> dict:
# 列表轉為字典,統計出aSet中每個元素出現的次數
aDict = {}
for item in aSet:
counter = aList.count(item)
if counter >= minValue: #去除僅出現一次的品牌,極有可能是商家打錯了
aDict[item] = aList.count(item)
return aDict
DataFrame轉字典函數
先生成列表,再借用列表轉字典函數
import math
def dfToDict(df,minValue=2):
# 傳入數據
milkTeaNames = df['購買商品']
# 品牌列表,將截取出的品牌名存入列表
milkTeaList = []
# 品牌名使用集合存儲,利用集合元素唯一性自動實現去重
milkTeaSet = set()
for milkTeaName in milkTeaNames:
milkTeaSet.add(milkTeaName)
milkTeaList.append(milkTeaName)
milkTeaDict = countDict(milkTeaSet, milkTeaList, minValue)
# 將字典按照元素值進行逆序排序
milkTeaDict = dict(sorted(milkTeaDict.items(), key=lambda i: i[1], reverse=True))
return milkTeaDict
# make data:
gmDict = dfToDict(gmdf)
x1 = gmDict.keys()
y1 = gmDict.values()
syDict = dfToDict(sydf,8)
x2 = syDict.keys()
y2 = syDict.values()
# plot
fig,axes= plt.subplots(1,2,figsize=(18, 9))
colors = ['tomato', 'aqua', 'bisque', 'lightgreen', 'skyblue']
axes[0].bar(x1, y1, width=0.3, color=colors, edgecolor="white", linewidth=0.5)
axes[1].bar(x2, y2, width=0.3, color=colors, edgecolor="white", linewidth=0.4)
plt.xticks(rotation=90)
plt.xlabel('奶茶名稱')
plt.ylabel('奶茶出現次數')
plt.show()




