Machine-Learning-笔记 -Bagging&Boosting

本文分析了猫眼电影平台上《流浪地球》的上万条评论,探讨了电影的总体评价、评价随时间的变化趋势、高分与低分评论的理由及低分人群特征。

title: 猫眼电影评论的爬取和分析
date: 2019-03-09 22:14:23
tags:
- Machine Learning
- Decision Tree
mathjax: true

header-img: “5.gif”

本文在猫眼电影上爬取了《流浪地球》的上万评论,并对其评论进行分析

爬虫-爬取数据

找到评论网页地址

先打开猫眼官网找到《流浪地球》的介绍页面:https://maoyan.com/films/248906
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9i57XUwm-1584170461659)(http://m.qpic.cn/psb?/V10c1VbY1Y4Fvm/DlzZZcnQmJlcg**2UJdVofzhw1drFquhmgZJnbTisuM!/b/dFIBAAAAAAAA&bo=ZwPdAQAAAAADJ7o!&rf=viewer_4&t=5)]

  • 打开开发者工具
  • 转换成手机浏览(因为网页版的评论数据只显示部分短评)
    点击红色箭头指向的位置,然后按F12键刷新,这时候我们就可以看到所有评论了
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XpcwolPj-1584170461660)(http://m.qpic.cn/psb?/V10c1VbY1Y4Fvm/JRF2hjEvkSVM1lW8xlO5RUgFLHO1pGd2.PrJGm8oDog!/b/dDUBAAAAAAAA&bo=ywPFAQAAAAADFz4!&rf=viewer_4&t=5)]
获取评论请求地址

在点击打开“查看全部533685条讨论”后,屏幕上的评论往下拉,会发现浏览器的网络展示中会不断加载新页面,网络请求多出来了comments.json的请求:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9HBQEhN9-1584170461661)(http://m.qpic.cn/psb?/V10c1VbY1Y4Fvm/t4bi2nk3sPPDCunbWpdBJSGco4pUaYEYjCC.RMMQjo0!/b/dL4AAAAAAAAA&bo=DQNIAgAAAAADF3Y!&rf=viewer_4&t=5)]

复制出几个comments.json页面的URL做比较寻找规律

http://m.maoyan.com/review/v2/comments.json?movieId=248906&userId=-1&offset=0&limit=15&ts=0&level=2&type=3
http://m.maoyan.com/review/v2/comments.json?movieId=248906&userId=-1&offset=15&limit=15&ts=1552143388614&level=2&type=3
http://m.maoyan.com/review/v2/comments.json?movieId=248906&userId=-1&offset=30&limit=15&ts=1552143388614&level=2&type=3

可以发现规律:

  • 初始页面的ts值为0,随后会有ts值,且保持不变。这里的ts是当前的时间戳,看、可以用如下代码查看
#coding:UTF-8
import time
#毫秒转换成秒
timeStamp = int(1552143388614/1000)
#转换成localtime
localTime = time.localtime(timeStamp)
#转换成新的时间格式(2017-09-16 11:28:54)
strTime = time.strftime("%Y-%m-%d %H:%M:%S", localTime)
print(strTime)
  • offset是请求评论开始的序号,limit为请求的条数

再看返回的json结果:

  • data.comments中是评论的具体内容
  • paging中通过hasMore来告诉我们是否还有更多(判断是否继续抓取)

构造请求url 方法一

根据上面的分析,我们构造请求的url就很明确了:

  • 从offset=0&limit=15开始
  • 通过返回的paging.hasMore来判断是否继续抓取
  • 下一个抓取的url中offset+=limit

只能抓取1000条?!

根据上述分析,在返回的json数据中是可以看到总评论数的,但是实际抓取的时候,在offset超过1000之后,返回的数据中hasMore就变成了false。

于是尝试通过浏览器一直下拉刷新,到达offset超过1000的情况,发现页面会不停的发送请求,但也无法获取数据。

那应该就是网站做了控制,不允许offset超过1000。

构造请求URL 方法二

那么就要考虑其他构造url的方法来抓取了。先观察下每个请求返回的信息:

发现每个comment里都包含有一个time信息,可以发现后台是按照时间顺序的,每分钟一个间隔,那么就可以考虑根据每次返回comment中的时间来更新url中的ts即可。

由于不确定每次请求返回的数据中包含了多长的时间段,且返回的第一个评论时间戳与第二个评论是不同的,所以抓取思路如下:

  • 获取请求数据
  • 记录第一个时间戳
  • 记录第二个时间戳
  • 当遇到第三个时间戳时,将ts设置为第二个时间戳,重新构造url
  • 如果单次抓取中每遇到第三个时间戳,则通过修改offset来继续抓取,直到遇到第三个时间戳
def parse_json(data):
    global count
    global offset
    global limit
    global ts
    ts_duration = ts
    res = json.loads(data)
    comments = res['data']['comments']
    for comment in comments:
        comment_time = comment['time']
        if ts == 0:
            ts = comment_time
            ts_duration = comment_time
        if comment_time != ts and ts == ts_duration:
            ts_duration = comment_time
        if comment_time !=ts_duration:
            ts = ts_duration
            offset = 0
            return get_url()
        else:
            content = comment['content'].strip().replace('\n', '。')
            print('get comment ' + str(count))
            count += 1
            write_txt(time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(comment_time/1000)) + '##' + content + '\n')
    if res['paging']['hasMore']:
        offset += limit
        return get_url()
    else:
        return None

这里贴出另一个思路完整代码

# coding: utf-8

# 导入爬虫所需工具库
import time,  random
import datetime as dt
import requests
import json
import pandas as pd

# 8个备用user_agents
user_agents = [
    {'User-Agent': 'MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22;\
            CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1'},
    {'User-Agent': 'Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 \
            (KHTML, like Gecko) wOSBrowser/233.70 Safari/534.6 TouchPad/1.0'},
    {'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5;\
            Trident/5.0; IEMobile/9.0; HTC; Titan)'},
    {'User-Agent': 'Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019;\
            Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124'},
    {'User-Agent': 'Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) \
            AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13'},
    {'User-Agent': 'Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us)\
            AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5'},
    {'User-Agent': 'Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) \
            AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5'},
    {'User-Agent': 'Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91)\
            AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1'},
]
# 创建爬虫类
class MovieSpider(object):
    def __init__(self, filename):
        self.headers = user_agents
        self.filename = filename

    def get_data(self, header, url):
        '''
        功能:访问url的网址,获取网页内容并返回
        参数:url,目标网页的url
        返回:目标网页的html内容
        '''
        try:
            r = requests.get(url, headers=header)
            r.raise_for_status()
            return r.text
        except Exception as e:
            print(e)

    def parse_data(self, html):
        '''
        功能:提取 html 页面信息中的关键信息,并整合一个数组并返回
        参数:html 根据 url 获取到的网页内容
        返回:存储有 html 中提取出的关键信息的数组
        '''
        json_data = json.loads(html)['cmts']
        comments = []

        try:
            for item in json_data:
                comment = []
                # 提取影评中的6条数据:nickName(昵称),cityName(城市),content(评语),
                # score(评分),startTime(评价时间),gender(性别)
                comment.append(item['nickName'])
                comment.append(item['cityName'] if 'cityName' in item else '')
                comment.append(item['content'].strip().replace('\n', ''))
                comment.append(item['score'])
                comment.append(item['startTime'])
                comment.append(item['gender'] if 'gender' in item else '')
                comment.append(item['userLevel'] if 'userLevel' in item else '')
                comment.append(item['userId'] if 'userId' in item else '')

                comments.append(comment)

            return comments

        except Exception as e:
            print(comment)
            print(e)

    def save_data(self, comments):
        '''
        功能:将comments中的信息输出到文件中/或数据库中。
        参数:comments 将要保存的数据
        '''
        df = pd.DataFrame(comments)
        df.to_csv(self.filename, mode='a', encoding='utf_8_sig',
                  index=False, sep=',', header=False)

    def run(self, time_lists):
        '''
        功能:爬虫调度器,根据规则每次生成一个新的请求 url,爬取其内容,并保存到本地。
        '''
        #         start_time = dt.datetime.now().strftime('%Y-%m-%d  %H:%M:%S')
        start_time = time_lists[0]  # 电影上映时间,评论爬取到此截至
        end_time = time_lists[-1]  # 电影上映时间,评论爬取到此截至
        print('*******************')

        # 抓取评论信息
        i = 0
        while start_time > end_time:
            i += 1
            if i % 10 == 0:
                print('已爬取%s页评论' % i)
            url = 'http://m.maoyan.com/mmdb/comments/movie/248906.json?_v_=            yes&offset=0&startTime=' + start_time.replace(
                '  ', '%20')
            header = random.choice(self.headers)
            time.sleep(0.05)
            html = None

            try:
                html = self.get_data(header, url)
            except Exception as e:
                print('*************************')
                time.sleep(0.83)
                html = self.get_data(url)
                print(e)

            else:
                time.sleep(0.3)

            # 解析评论信息
            comments = self.parse_data(html)
            start_time = comments[14][4]

            start_time = dt.datetime.strptime(
                start_time, '%Y-%m-%d  %H:%M:%S') + dt.timedelta(seconds=-1)
            start_time = dt.datetime.strftime(start_time, '%Y-%m-%d  %H:%M:%S')

            self.save_data(comments)

# 通过改变时间点,选择爬取信息所处的时间段
t1 = ['2019-02-12  18:59:59', '2019-02-05  00:00:00']
time_lists = t1
filename = '流浪地球%s_comments.csv' % time_lists[1].split()[0]
spider = MovieSpider(filename)
spider.run(time_lists)
print('爬取信息结束')

数据分析

  • 读取数据
    前面已经将评论的时间和内容通过csv的格式保存下来,并使用;分割。这里我们将使用pandas读取csv并进行统计处理
import numpy as np
data = pd.read_csv('./data.csv')
data.info()
  • 数据详情
    共有102580条数据;
    包含字段:
    评论内容、性别、评论ID、评论者昵称、回复数量、评分、时间、点赞数量、评论者ID、评论者等级

  • 清理数据

# 删除无用数据
data = data.dropna(axis = 0, how = "any")
# 删除重复评论
data = data.drop_duplicates(subset='content')
# 将本来是object类型的time,转换成时间类型
data['time'] = pd.to_datetime(data['time'],format='%Y-%m-%d %H:%M:%S')
# 日期筛选为上映后的日期
data = data[data['time']>=pd.to_datetime('2019-02-05 00:00:00')]
# 将时间设置为index
data.set_index(data['time'],inplace=True)
  • 分析问题
    1.总体评价如何?
    2.总体评价的时间走向如何?
    3.高分的评价理由是什么?
    4.低分的评价理由是什么?
    5.低分的人群有哪些特征?(性别、等级)
    6.低分跟哪位演员有关?

总体评价如何?

from pyecharts import Bar
from pyecharts import Overlap
from pyecharts import Line

score_total = data['score'].value_counts().sort_index()
bar = Bar('《流浪地球》各评分数量',width=700)
overlap = Overlap(width=700)
bar.add("", score_total.index, score_total.values, is_label_show=True,
       bar_category_gap='40%', label_color = ['#130f40'],
       legend_text_size=18,xaxis_label_textsize=18,yaxis_label_textsize=18)
line = Line("", width=700)
line.add("",score_total.index, score_total.values+500,is_smooth=True)
overlap.add(bar)
overlap.add(line)
overlap

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cBRSat8O-1584170461662)(http://m.qpic.cn/psb?/V10c1VbY1Y4Fvm/cpKS154bQyeLY02bQ96*Ib6SRAy1z1xyKt0XBlHR6Zg!/b/dFMBAAAAAAAA&bo=vAKQAQAAAAADFx0!&rf=viewer_4&t=5)]
低分占比

# 低分占比 <5
score_total[:5].sum()/score_total.sum()*100

高分占比 8

# 高分占比 8
score_total[8:].sum()/score_total.sum()*100

满分占比

# 满分占比
score_total[10:].sum()/score_total.sum()*100

通过上述分析计算,高分占比达到90%以上,满分占比也高达70%以上,可以看出《流浪地球》整体评分很高

高分的评价理由是什么?

import jieba
from collections import Counter
from pyecharts import WordCloud

# 比较偏的就不可以被正确分词了 add_word函数提供了解决方法
jieba.add_word('屈楚萧')
jieba.add_word('刘启')
jieba.add_word('吴京')
jieba.add_word('刘培强')
jieba.add_word('李光洁')
jieba.add_word('王磊')
jieba.add_word('吴孟达')
jieba.add_word('达叔')
jieba.add_word('韩子昂')
jieba.add_word('赵今麦')
jieba.add_word('韩朵朵')

swords = [x.strip() for x in open ('stopwords.txt',encoding='utf-8')]

def plot_word_cloud(data, swords):
    text = ''.join(data['content'])
    words = list(jieba.cut(text))
    ex_sw_words = []
    for word in words:
        if len(word)>1 and (word not in swords):
            ex_sw_words.append(word)
    c = Counter()
    c = Counter(ex_sw_words)
    wc_data = pd.DataFrame({'word':list(c.keys()), 'counts':list(c.values())}).sort_values(by='counts', ascending=False).head(100)
    wordcloud = WordCloud(width=1300, height=620)
    wordcloud.add("", wc_data['word'], wc_data['counts'], word_size_range=[20, 100])
    return wordcloud
# 高分的评价
plot_word_cloud(data=data[data['score']>6], swords=swords)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2smOMR3J-1584170461663)(http://m.qpic.cn/psb?/V10c1VbY1Y4Fvm/Suvm.I8ESBV9Fkc3Bn5PHLMX4QQBcl3X9.4tLRCicRQ!/b/dL8AAAAAAAAA&bo=FAVsAgAAAAADN20!&rf=viewer_4&t=5)]

# nlargest函数不需要排序直接看最大的
for i in data[data['score']>6].nlargest(10, 'upCount')['content']:
    print(i+'\n')

通过热度词云可以看出,《流浪地球》评分高的原因是因为中国国产的科幻片,特效的制作精良。

# 低分的评价
plot_word_cloud(data=data[data['score']<5], swords=swords)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fwpTGUEb-1584170461664)(http://m.qpic.cn/psb?/V10c1VbY1Y4Fvm/c1TmBZ57ca1AEW4KqsGBt4tYqpisemfDnSInFfkEjto!/b/dC0BAAAAAAAA&bo=FAVsAgAAAAADN20!&rf=viewer_4&t=5)]
通过上图低分词云可以看出,网友评论低分的原因是因为剧情,虽然在中国科幻片上特效制作精良,算是中国国产科幻片里程碑作品,但剧情欠佳。

低分的人群有哪些特征?(性别、等级)

观众总体的性别占比

# 总体的性别比例
gender_total = data['gender'].value_counts()
bar = Bar("《流浪地球》观众性别", width=700)
bar.add("",['未知', '男', '女'],gender_total.values, is_stack=True, is_label_show=True,
               bar_category_gap='60%', label_color = ['#130f40'],
       legend_text_size=18,xaxis_label_textsize=18,yaxis_label_textsize=18)
bar

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DTuA8Bhw-1584170461664)(http://m.qpic.cn/psb?/V10c1VbY1Y4Fvm/1esG2QMjcqJC1.mYkRqhwpW9AksjehmcW2LJxEQfpTg!/b/dD4BAAAAAAAA&bo=vAKQAQAAAAADFx0!&rf=viewer_4&t=5)]
低分观众的性别占比

gender_low = data[data['score']<5]['gender'].value_counts()

bar = Bar("《流浪地球》低分评论观众性别", width=700)
bar.add("",['未知', '男', '女'],gender_low.values, is_stack=True, is_label_show=True,
        bar_category_gap='60%', label_color = ['#130f40'],
       legend_text_size=18,xaxis_label_textsize=18,yaxis_label_textsize=18)
bar

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UPqjuux2-1584170461665)(http://m.qpic.cn/psb?/V10c1VbY1Y4Fvm/R7Q0V7LQMy*UacK3XIfKxxNisOP7KhjRJ3epJzoU2OM!/b/dDMBAAAAAAAA&bo=vAKQAQAAAAADFx0!&rf=viewer_4&t=5)]
可以看出低分观众在男女比例上跟总体的男女比例基本一致

高低分跟哪位演员有关?

mapping = {'liucixin':'刘慈欣|大刘', 'guofan':'郭帆', 'quchuxiao':'屈楚萧|刘启|户口', 'wujing':'吴京|刘培强', 
           'liguangjie':'李光洁|王磊', 'wumengda':'吴孟达|达叔|韩子昂', 'zhaojinmai':'赵今麦|韩朵朵'}
for key,values in mapping.items():
    data[key] = data['content'].str.contains(values)

staff_count = pd.Series({key: data.loc[data[key],'score'].count() for key in mapping.keys()}).sort_values()
staff_count

bar = Bar("《流浪地球》演职员总体提及次数", width=700)
bar.add("",['李光洁','郭帆','赵今麦','吴孟达','屈楚萧','刘慈欣','吴京'],staff_count.values,is_stack=True, is_label_show=True
       ,bar_category_gap='60%',label_color = ['#130f40']
        ,legend_text_size=18,xaxis_label_textsize=18,yaxis_label_textsize=18)
bar

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5M9SnJae-1584170461665)(http://m.qpic.cn/psb?/V10c1VbY1Y4Fvm/rnGVOTZd7zmRNS1tc4g0i8rfqLGHXuj8YMkHRGFC6NI!/b/dLgAAAAAAAAA&bo=vAKQAQAAAAADFx0!&rf=viewer_4&t=5)]

staff_low = pd.Series({key: data.loc[data[key]&(data['score']<5),'score'].count() for key in mapping.keys()}).sort_values()

staff_count_pct = np.round(staff_low/staff_count*100, 2).sort_values()

bar = Bar("《流浪地球》演职员低分评论提及百分比", width=700)
bar.add("",['郭帆','刘慈欣','李光洁','屈楚萧','赵今麦','吴京','吴孟达'],staff_count_pct.values,is_stack=True,is_label_show=True,bar_category_gap='60%',label_color = ['#130f40']
       ,legend_text_size=18,xaxis_label_textsize=18,yaxis_label_textsize=18)
bar

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cizaqu7k-1584170461666)(http://m.qpic.cn/psb?/V10c1VbY1Y4Fvm/aOLybbDPCMTpLrTYK3d.SNPKptH3AFEWdqp0fGHRf2o!/b/dD4BAAAAAAAA&bo=vAKQAQAAAAADFx0!&rf=viewer_4&t=5)]

参考:

https://segmentfault.com/a/1190000018242134


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值