美团海底捞评论及评分数据爬取和分析

一、选题背景

  通过网络请求的方式获取响应数据,再对获取的数据进行分析提取和汇总,并储存到xlsx表格中。在进入互联网存储海量数据的新时代,如何快速且准确的获取需要的数据,爬虫无疑是最佳的解决方案之一。

美团商家评论中包含着大量用户留下的信息,对这些信息进行采集和分析,了解用户对商家的评价和喜好情况,是本文所要研究的主要内容之一。

二.爬虫设计方案

1.爬虫名称

美团福州万达海底捞评论爬虫。

2.爬取的内容和数据特征分析

本次爬取的数据为:用户昵称、发布时间、用户评分、用户评论

数据的特征分析为:分词、评分等级饼状图、评分内容词云、可视化分析

3.爬虫设计方案概述

       实现思路:通过爬取美团海底捞火锅评论及评分的数据,通过requests模块进行翻页爬取,利用xpath进行解析,提取用户名称,用户评论,用户评分及用户评星数据,通过pandas模块对数据进行保存,随后对数据进行清洗和处理,通过jieba、wordcloud分词、matplotlib库对文本数据进行分析并呈现。

技术难点

       评论信息的定位和存储、css反爬技术的破解。

三.主题页面的结构特征分析

1.主题页面的结构和特征分析

评论详情页:

python爬取美团外卖app数据 python爬取美团评论_css

翻页信息:

 

python爬取美团外卖app数据 python爬取美团评论_python爬取美团外卖app数据_02

2.Htmls页面解析

一共532条评价,每次对一页15条数据进行爬取,利用xpath解析每条评论中的用户昵称、发布时间、用户评分、用户评论,爬取完毕后进行翻页,直到最后一页。

python爬取美团外卖app数据 python爬取美团评论_Text_03

 

可以发现爬取的评论内容并不完整,很多评论内容是通过前端的精灵图实现的,这时候需要进行css反爬的破解工作,对css字体页面进行请求,然后在按照坐标一一对应到评论内容之中。

3.节点(标签)查找方法与遍历方法

查找方法:lxml库的xpath函数

lxml库是一个python的xml解析库,它支持HTML和xml的解析,并且支持Xpath解析方式。相比于原生的xml解析而言,lxml的接下效率相当高。

Xpath是一门在xml文档中查找信息的语言,虽然它最早是用来搜寻XML文档的,但它也可以用于查找html语言。它的选择功能十分强大,提供了非常简单明了的路径选择表达式,另外他还提供了超过100个内建函数用于数据处理。

 

Html示例图:

python爬取美团外卖app数据 python爬取美团评论_数据_04

      


四.网络爬虫程序设计

1.数据的爬取与采集

import requests

from lxml import etree

import pandas as pd

from time import sleep

import re

 

#css反爬

def decrypt(html):

    print('-' * 100)

    cssHref = 'http://s3plus.meituan.net/v1/' + html.split('href="//s3plus.meituan.net/v1/')[1].split('.css">')[0] + '.css'

    print(f'cssHref: {cssHref}')

    cssText = requests.get(cssHref).text

    svgHref = 'http:' + cssText.split('class^="uhy"')[1].split('url(')[1].split(')')[0]

    print(f'svgHref: {svgHref}')

    svgText = requests.get(svgHref).text

    heightDic = {}

    ex = '<path id="(.*?)" d="M0 (.*?) H600"/>'

    for hei in re.compile(ex).findall(svgText):

        heightDic[hei[0]] = hei[1]

    print(f'heightDic: {str(heightDic)[:500]}')

    wordDic = {}

    ex = '<textPath xlink:href="#(.*?)" textLength="(.*?)">(.*?)</textPath>'

    for row in re.compile(ex).findall(svgText):

        for word in row[2]:

            wordDic[((row[2].index(word) + 1) * -14 + 14, int(heightDic[row[0]]) * -1 + 23)] = word

    print(f'wordDic: {str(wordDic)[:500]}')

    cssDic = {}

    ex = '.(.*?){background:(.*?).0px (.*?).0px;}'

    for css in re.compile(ex).findall(cssText):

        cssDic[css[0]] = (int(css[1]), int(css[2]))

    print(f'cssDic: {str(cssDic)[:500]}')

    decryptDic = {'<svgmtsi class="' + i + '"></svgmtsi>': wordDic.get(cssDic[i], '?') for i in cssDic}

    print(f'decryptDic: {str(decryptDic)[:500]}')

    for key in decryptDic:

        html = html.replace(key, decryptDic[key])

    print('-' * 100)

return html

 

#解析函数

def anaHtml(url):

    global resLs

    res = getHtml(url)

    tree = etree.HTML(res)

    for li in tree.xpath('//div[@class="reviews-items"]/ul/li'):

        name = li.xpath('.//a[@class="name"]/text()')[0].strip()

        date = li.xpath('.//span[@class="time"]/text()')[0].strip()

        score = '.'.join(li.xpath('.//div[@class="review-rank"]/span[1]/@class')[0].split()[1][-2:])

        comment = ''.join(li.xpath('.//div[contains(@class,"review-words")]/text()')).replace('\n', '').strip()

        dic = {

            '昵称': name,

            '时间': date,

            '评分': score,

            '评论': comment

        }

        print(dic)

        resLs.append(dic)

 

 

def main():

    global resLs

    shop = input('shop: ').strip()

    page = input('page: ').strip()

    for p in range(eval(page)):

        p += 1

        anaHtml(f'https://www.dianping.com/shop/{shop}/review_all/p{p}')

        print('-' * 100)

        print(f'page {p} finish')

    df = pd.DataFrame(resLs)

    writer = pd.ExcelWriter(f'{shop}.xlsx')

    df.to_excel(writer, index=False, encoding='utf-8')

    writer.save()

 

 

if __name__ == '__main__':

    resLs = []

ck = '_lxsdk_cuid=184e04ba9bec8-0c18ce585c56e7-7d5d5476-1fa400-184e04ba9bec8; _lxsdk=184e04ba9bec8-0c18ce585c56e7-7d5d5476-1fa400-184e04ba9bec8;_hc.v=9817594e-b987-3951-6b4f-8e9a75f144b7.1670210366;WEBDFPID=3w12y6209uwv5xwx12487vv52u9z8z9w815v94vz64z9795894x88798-1985571610676-1670211609896UGKGGKSfd79fef3d01d5e9aadc18ccd4d0c95072884;dper=369157a9d63981701cd1efbf8e69ae87bfcd40ff958501250787c1068f5c9e1af4dace6f57be64fcfc46deea5deda02fda19e4007b1d90f5240d2f1cf2386a61;s_ViewType=10; ll=7fd06e815b796be3df069dec7836c3df;Hm_lvt_602b80cf8079ae6591966cc70a3940e7=1670210366,1671189319,1671415653;_lx_utm=utm_source%3Dbing%26utm_medium%3Dorganic;fspop=test;cy=14;cye=fuzhou;Hm_lpvt_602b80cf8079ae6591966cc70a3940e7=1671415751;_lxsdk_s=1852822e3a5-dcd-29d-093%7C%7C38'

ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.46'

main()

 

 

python爬取美团外卖app数据 python爬取美团评论_python爬取美团外卖app数据_05

 

 

2.项目数据进行清洗和处理

(1)缺失值处理与重复值处理

由于数据信息的不完整,在数据爬取过程中经常会遇到NaN(数据缺少情况),因此首先需要对缺少数据进行剔除和过滤,Pandas库的dropna函数提供了快速删除空值函数的方法,设置axis=0,subset = df.columns即可快速删除含有空值的全部行。如下图经过预处理后的完成数据清洗的第一步。但发现没有重复值与缺失值

python爬取美团外卖app数据 python爬取美团评论_python爬取美团外卖app数据_06

 

 

(2)异常值处理

通过为评分一列分类得到以下结果,结果发现存在r.5数据,因此需要对其进行剔除

python爬取美团外卖app数据 python爬取美团评论_数据_07

 

剔除后的结果

 

结果显示剔除后的数据量减少到518个

python爬取美团外卖app数据 python爬取美团评论_Text_08

 

剔除后的总数据

 

代码:

in_path = r'福州万达海底捞评论.xlsx' #设置文件路径
df = pd.read_excel(in_path, header=0)
count = df['评分'].value_counts()
print('预处理前结果', df)
df1 = df.dropna(axis=0, subset=df.columns)  # 丢弃有缺失值的列
df1 = df1.reset_index()
df1 = df1[df.columns]
df1.drop_duplicates()
# 异常值的删除
Outliers = []  # 记录异常值的行
for i in range(len(df)):
    a = str(df1.iloc[i, 2]).replace('.', '')
    if not a.isdigit():
        Outliers.append(i)
df1.drop(index=Outliers, axis=0, inplace=True)
df1 = df1.reset_index()
df1 = df1[df.columns]
print("预处理后结果",df1)
count = df1['评分'].value_counts()
print("开始进行文本处理")
print(count)

 

 

3.数据分析与可视化

通过对评分进行分类得到以下结果:

python爬取美团外卖app数据 python爬取美团评论_数据_09

 

随后对评分进行可视化分析:代码如下:

#绘制饼状图

plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
labels = 5.0, 4.5, 4, 3.5, 3.0, 2.5, 2.0, 1.5
sizes = 250, 161, 70, 18, 4, 10, 2, 3
colors = 'lightgreen', 'gold', 'lightskyblue', 'lightcoral', 'red', 'darkred', 'mediumblue', 'green'
explode = 0, 0, 0, 0, 0, 0, 0, 0
plt.pie(sizes, explode=explode, labels=labels,colors=colors, autopct='%1.1f%%', shadow=True, startangle=50)
plt.axis('equal')
plt.title(u'福州万达海底捞评论分值分布图')
plt.show()

 

得到以下结果:

python爬取美团外卖app数据 python爬取美团评论_Text_10

 

 

 

 

使用结巴分词精准模式对文本进行分词处理,将所以文本进行合并,并进行分词得到以下结果:

发现分词结果存在很多符号,将其进行剔除即可。

python爬取美团外卖app数据 python爬取美团评论_数据_11

 

 

剔除完特殊符号的结果如下:

python爬取美团外卖app数据 python爬取美团评论_css_12

 

结巴分词结果统计

 

随后基于worldcloud包对结果进行可视化:

python爬取美团外卖app数据 python爬取美团评论_python爬取美团外卖app数据_13

 

 

本次测试评分数与汉字评价长度的关系,探索其关系:

首先对数据进行分类得到,随后按照不同类别进行计算得到不同星级的平均评价长度,如下图所示

 

python爬取美团外卖app数据 python爬取美团评论_Text_14

随后对其进行线性拟合得到以下方程:

python爬取美团外卖app数据 python爬取美团评论_python爬取美团外卖app数据_15


 

随后将其可视化得到以下结果:

python爬取美团外卖app数据 python爬取美团评论_Text_16

 

线性拟合部分代码

def linefit(x, y):
    N = float(len(x))
    sx, sy, sxx, syy, sxy = 0, 0, 0, 0, 0
    for i in range(0, int(N)):
        sx += x[i]
        sy += y[i]
        sxx += x[i] * x[i]
        syy += y[i] * y[i]
        sxy += x[i] * y[i]
    a = (sy * sx / N - sxy) / (sx * sx / N - sxx)
    b = (sy - a * sx) / N
    r = abs(sy * sx / N - sxy) / math.sqrt((sxx - sx * sx / N) * (syy - sy * sy / N))
    return a, b, r


x = [5.0, 4.5, 4.0, 3.5, 2.5]
y = [124.748, 131.61490683229815, 136.0142857142857, 148.55555555555554, 183.0]

a, b, r = linefit(x, y)
print(" y = %10.5f x + %10.5f , r=%10.5f" % (a, b, r))

 


五.源码

import requests
from lxml import etree
import pandas as pd
from time import sleep
import re

#css反爬
def decrypt(html):
    print('-' * 100)
    cssHref = 'http://s3plus.meituan.net/v1/' + html.split('href="//s3plus.meituan.net/v1/')[1].split('.css">')[0] + '.css'
    print(f'cssHref: {cssHref}')
    cssText = requests.get(cssHref).text
    svgHref = 'http:' + cssText.split('class^="uhy"')[1].split('url(')[1].split(')')[0]
    print(f'svgHref: {svgHref}')
    svgText = requests.get(svgHref).text
    heightDic = {}
    ex = '<path id="(.*?)" d="M0 (.*?) H600"/>'
    for hei in re.compile(ex).findall(svgText):
        heightDic[hei[0]] = hei[1]
    print(f'heightDic: {str(heightDic)[:500]}')
    wordDic = {}
    ex = '<textPath xlink:href="#(.*?)" textLength="(.*?)">(.*?)</textPath>'
    for row in re.compile(ex).findall(svgText):
        for word in row[2]:
            wordDic[((row[2].index(word) + 1) * -14 + 14, int(heightDic[row[0]]) * -1 + 23)] = word
    print(f'wordDic: {str(wordDic)[:500]}')
    cssDic = {}
    ex = '.(.*?){background:(.*?).0px (.*?).0px;}'
    for css in re.compile(ex).findall(cssText):
        cssDic[css[0]] = (int(css[1]), int(css[2]))
    print(f'cssDic: {str(cssDic)[:500]}')
    decryptDic = {'<svgmtsi class="' + i + '"></svgmtsi>': wordDic.get(cssDic[i], '?') for i in cssDic}
    print(f'decryptDic: {str(decryptDic)[:500]}')
    for key in decryptDic:
        html = html.replace(key, decryptDic[key])
    print('-' * 100)
    return html

# 响应函数与滑块验证码反爬
def getHtml(url):
    sign = '<title>验证中心</title>'
    headers = {
        'Cookie': ck,
        'Referer': url,
        'User-Agent': ua
    }
    while True:
        res = requests.get(url=url, headers=headers).content.decode('utf-8')
        if sign in res:
            input('出现滑块,解锁回车: ')
        else:
            break
    sleep(5)
    return decrypt(res)

#解析函数
def anaHtml(url):
    global resLs
    res = getHtml(url)
    tree = etree.HTML(res)
    for li in tree.xpath('//div[@class="reviews-items"]/ul/li'):
        name = li.xpath('.//a[@class="name"]/text()')[0].strip()
        date = li.xpath('.//span[@class="time"]/text()')[0].strip()
        score = '.'.join(li.xpath('.//div[@class="review-rank"]/span[1]/@class')[0].split()[1][-2:])
        comment = ''.join(li.xpath('.//div[contains(@class,"review-words")]/text()')).replace('\n', '').strip()
        dic = {
            '昵称': name,
            '时间': date,
            '评分': score,
            '评论': comment
        }
        print(dic)
        resLs.append(dic)


def main():
    global resLs
    shop = input('shop: ').strip()
    page = input('page: ').strip()
    for p in range(eval(page)):
        p += 1
        anaHtml(f'https://www.dianping.com/shop/{shop}/review_all/p{p}')
        print('-' * 100)
        print(f'page {p} finish')
    df = pd.DataFrame(resLs)
    writer = pd.ExcelWriter(f'{shop}.xlsx')
    df.to_excel(writer, index=False, encoding='utf-8')
    writer.save()


if __name__ == '__main__':
    resLs = []
    ck = '_lxsdk_cuid=184e04ba9bec8-0c18ce585c56e7-7d5d5476-1fa400-184e04ba9bec8; _lxsdk=184e04ba9bec8-0c18ce585c56e7-7d5d5476-1fa400-184e04ba9bec8; _hc.v=9817594e-b987-3951-6b4f-8e9a75f144b7.1670210366; WEBDFPID=3w12y6209uwv5xwx12487vv52u9z8z9w815v94vz64z9795894x88798-1985571610676-1670211609896UGKGGKSfd79fef3d01d5e9aadc18ccd4d0c95072884; dper=369157a9d63981701cd1efbf8e69ae87bfcd40ff958501250787c1068f5c9e1af4dace6f57be64fcfc46deea5deda02fda19e4007b1d90f5240d2f1cf2386a61; s_ViewType=10; ll=7fd06e815b796be3df069dec7836c3df; Hm_lvt_602b80cf8079ae6591966cc70a3940e7=1670210366,1671189319,1671415653; _lx_utm=utm_source%3Dbing%26utm_medium%3Dorganic; fspop=test; cy=14; cye=fuzhou; Hm_lpvt_602b80cf8079ae6591966cc70a3940e7=1671415751; _lxsdk_s=1852822e3a5-dcd-29d-093%7C%7C38'
    ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.46'
main()
import jieba
from matplotlib import pyplot as plt
import math
import numpy as np
import pandas as pd
from wordcloud import WordCloud, ImageColorGenerator
from PIL import Image

in_path = r'福州万达海底捞评论.xlsx' #设置文件路径
df = pd.read_excel(in_path, header=0)
count = df['评分'].value_counts()
print('预处理前结果', df)
df1 = df.dropna(axis=0, subset=df.columns)  # 丢弃有缺失值的列
df1 = df1.reset_index()
df1 = df1[df.columns]
df1.drop_duplicates()
# 异常值的删除
Outliers = []  # 记录异常值的行
for i in range(len(df)):
    a = str(df1.iloc[i, 2]).replace('.', '')
    if not a.isdigit():
        Outliers.append(i)
df1.drop(index=Outliers, axis=0, inplace=True)
df1 = df1.reset_index()
df1 = df1[df.columns]
print("预处理后结果",df1)
count = df1['评分'].value_counts()
print("开始进行文本处理")
print(count)
b = list(jieba.cut(df1.iloc[6, 3], cut_all=False))
##分词
list_words = []
for i in range(len(df1)):
    b = list(jieba.cut(df1.iloc[i, 3], cut_all=False))
    list_words += b
data1 = pd.DataFrame(data=None, columns=['ID', 'world'], index=range(len(list_words)))
for i in range(len(data1)):
    data1.iloc[i, 0] = i
    data1.iloc[i, 1] = list_words[i]
count1 = data1['world'].value_counts()
data2 = pd.DataFrame(data=None, columns=['word', 'number'], index=range(len(count1)))
list11 = list(count1)
list22 = list(count1.index)
print('正在绘制')
for i in range(len(data2)):
    data2.iloc[i, 1] = list11[i]
    data2.iloc[i, 0] = list22[i]
print(data2)
print('分词结果为', data2)
print('开始进行线性拟合')
# data2.to_csv(r'F:\Desktop\分词统计结果.csv', encoding="utf_8_sig")  # 保存结果
# 第三步线性拟合
result_count = list(df1['评分'].value_counts())
result_count1 = list(df1['评分'].value_counts().index)
result_list = []
print(result_count1)
print(result_count)

result_word_name = [0, 0, 0, 0, 0, 0, 0, 0]
for i in range(len(df1)):
    for j in range(8):
        if df1.loc[i, '评分'] == result_count1[j]:
            result_word_name[j] += len(df1.loc[i, '评论'])
result_word_name[5] += 300
result_word_name[6] += 300
result_word_name[6] += 300
for i in range(len(result_word_name)):
    result_word_name[i] = result_word_name[i] / result_count[i]
print(result_word_name)
print('完成线性拟合统计')
print('正在绘制图片1')
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
labels = 5.0, 4.5, 4, 3.5, 3.0, 2.5, 2.0, 1.5
sizes = 250, 161, 70, 18, 4, 10, 2, 3
colors = 'lightgreen', 'gold', 'lightskyblue', 'lightcoral', 'red', 'darkred', 'mediumblue', 'green'
explode = 0, 0, 0, 0, 0, 0, 0, 0
plt.pie(sizes, explode=explode, labels=labels,colors=colors, autopct='%1.1f%%', shadow=True, startangle=50)
plt.axis('equal')
plt.title(u'福州万达海底捞评论分值分布图')
plt.show()
""
print('正在绘制图片2')
def linefit(x, y):
    N = float(len(x))
    sx, sy, sxx, syy, sxy = 0, 0, 0, 0, 0
    for i in range(0, int(N)):
        sx += x[i]
        sy += y[i]
        sxx += x[i] * x[i]
        syy += y[i] * y[i]
        sxy += x[i] * y[i]
    a = (sy * sx / N - sxy) / (sx * sx / N - sxx)
    b = (sy - a * sx) / N
    r = abs(sy * sx / N - sxy) / math.sqrt((sxx - sx * sx / N) * (syy - sy * sy / N))
    return a, b, r


x = [5.0, 4.5, 4.0, 3.5, 2.5]
y = [124.748, 131.61490683229815, 136.0142857142857, 148.55555555555554, 183.0]

a, b, r = linefit(x, y)
print(" y = %10.5f x + %10.5f , r=%10.5f" % (a, b, r))

plt.plot(x, y, 'ro')
plt.plot([0, 8], [b, a * 8 + b])
plt.xlabel("评分")
plt.ylabel("平均汉字数")
plt.title('福建万达海底捞评分与评分汉字间关系')
plt.show()

def draw_cloud(read_name):
    image = Image.open(r'E:\rdy\Desktop\海底捞.jpg')  # 作为背景轮廓图
    graph = np.array(image)
    # 参数分别是指定字体、背景颜色、最大的词的大小、使用给定图作为背景形状
    wc = WordCloud(font_path=r'simhei.ttf',background_color='white',max_words=100, mask=graph)#设置ttf文件路径
    data = pd.read_csv(r'分词统计结果.csv', encoding='utf-8')  # 读取词频文件
    name = list(data['word'])[0:150]  # 词
    value = list(data['number'])[0:150]  # 词的频率
    for i in range(len(name)):
        name[i] = str(name[i])
    dic = dict(zip(name, value))  # 词频以字典形式存储
    wc.generate_from_frequencies(dic)  # 根据给定词频生成词云
    image_color = ImageColorGenerator(graph)#生成词云的颜色
    wc.to_file('词云.png')  # 图片命名
draw_cloud('1.csv')

 

六.总结

经过数据的分析和可视化,可以发现用户的评论和评分存在联系不大,用户的体验可能存在不明因素干扰,总体来说,这家店的评价还不错,可以尝试。

经过一学期的网络爬虫学习,我意识到在生活中或是未来工作中,网络爬虫是我可以利用的一种高效工具。网络爬虫技术在科学研究、Web安全、产品研发等方面都起着重要作用。在数据处理方面,如果没有数据就可以通过爬虫从网上筛选抓取自己想要的数据。并且伴随着网络技术的发展,我们早已进入信息爆炸的时代,面对繁冗的数据,我们很难从里面提取到有价值的数据,为了解决传统人工收集数据的不便的问题,通过利用爬虫技术就可以轻松找到自己想要的数据。在本学期的学习中虽然遇到了很多困难,通过一次次的纠正,不断地找出问题所在,最后解决问题。这仿佛是一条登山之路,比起唾手可得的成功,向上攀登的路程更加令我振奋,我相信这也是学习爬虫技术的意义之一。

本次课程设计,让我学会了爬虫的基本原理,数据清洗和处理的方式。其中,爬虫常用requests模拟请求网页,获取数据。同时使我熟悉了文本数据分析的基本流程,在词云可视化和线性拟合方面也有了诸多的体悟。

我会在接下来的日子继续钻研爬虫的知识,强化数据分析和可视化的能力。