Python 网络爬虫实战:猫眼电影 38950 条评论数据告诉你《无名之辈》是否值得一看?

11月16日,一部无流量明星、无大制作、无大IP的“三无”国产电影《无名之辈》上映后,竟然连续打败了超级英雄“毒液”、会魔法的“神奇动物”、勇闯互联网的“无敌破坏王”和“憨豆特工”,成为最大赢家。

从11月21日起,《无名之辈》就在单日票房上超过了《毒液:致命守护者》《神奇动物:格林德沃之罪》《无敌破坏王2:大闹互联网》《憨豆特工3》这些进口大片,连续9天霸占了当日票房冠军。上映14天《无名之辈》票房已接近5亿元,成为今年国产喜剧片的一匹“黑马”。

笔者暂时还没有看过这部电影,不过看到这些数据时真的有点惊讶。于是乎,去猫眼电影爬一爬评论,听听大家对这部电影的看法,《无名之辈》到底值不值得看?

 

本文共分为两大部分,

第一部分为爬虫部分,详细展示了如何分析并爬取网页内容的过程,并附上了完整的 python 源码,感兴趣的话大家可以参考实践一下。

第二部分为数据分析部分,最近学习了数据可视化的一些知识,活学活用,总算是可以对爬到的数据做一些简单的处理了。这部分也附上了较为完整的 python 源码,以及我操作时候踩过的坑。

 

爬虫部分


一、分析网页结构,明确数据结构

我们的目标网页是 猫眼电影《无名之辈》http://maoyan.com/films/1208282)。

然而打开网址之后,发现 PC 端的网页评论区只能显示 10 条评论数据,显然这是不够的(因为在手机端 APP 中显示的评论数据达3万八千多条)。所以我们这里通过 m 端来获取数据。

 通过 F12 召唤开发者工具,然后点击 切换设备工具栏(toggle device toolbar),如图中所示,然后刷新页面,即可切换成移动端的界面,评论数据也都可以正常显示出来了。

评论区的数据是通过 Ajax 动态加载出来的,也就是说,向下滑动到底之后,页面再向服务器发送请求,加载后面的评论数据,请求的URL结构如下所示,关键的参数是 offset 和 startTime。(经测试,两个参数均可以实现 “翻页” 的效果,但是并不需要两个都做改变,固定一个的值,然后循环改变另一个的值即可),由于此处我是希望爬取电影上映之后的评论数据(毕竟是看过电影之后再评论的更加可靠一些),所以改变 startTime 可能更加合适一些。

http://m.maoyan.com/mmdb/comments/movie/1208282.json?_v_=yes&offset=0&startTime=0

 根据请求,服务器返回的数据格式是 json,每次有15条评论数据,如下所示。我们关注的数据主要有以下五条:

  • 用户昵称
  • 所在城市
  • 评论内容
  • 电影评分
  • 评论时间

 

 二、编写爬虫代码,爬取并存储数据

 爬虫部分比较简单,都是一些常规操作,这里简单介绍一下流程,细节就不做过多解释了(如果有不懂的可以参考一下我的其他 python 网络爬虫实战系列的博客)。

我的爬虫包含了五个函数,如下:

  • get_data :其参数是目标网页 url,这个函数可以模拟浏览器访问 url,获取并将网页的内容返回。
  • parse_data :其参数是网页的内容,这个函数主要是用来解析网页内容,筛选提取出关键的信息,并打包成列表返回。
  • save_data :其参数是数据的列表,这个函数用来将列表中的数据写入本地的文件中。
  • main :这个函数是爬虫程序的调度器,可以根据事先分析好的 url 的规则,不断的构造新的请求 url,并调用其他三个函数,获取数据并保存到本地,直到结束。
  • if __name__ == '__main__' :这是主程序的入口,在这里调用 main 函数,启动爬虫调度器即可。
import requests
import json
import time
import re
import datetime
import pandas as pd

def get_data(url):
    '''
    功能:访问 url 的网页,获取网页内容并返回
    参数:
        url :目标网页的 url
    返回:目标网页的 html 内容
    '''
    headers = {
        'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
    }

    try:
        r = requests.get(url, headers=headers)
        r.raise_for_status()
        return r.text
    
    except requests.HTTPError as e:
        print(e)
        print("HTTPError")
    except requests.RequestException as e:
        print(e)
    except:
        print("Unknown Error !")
        
def parse_data(html):
    '''
    功能:提取 html 页面信息中的关键信息,并整合一个数组并返回
    参数:html 根据 url 获取到的网页内容
    返回:存储有 html 中提取出的关键信息的数组
    '''
    json_data = json.loads(html)['cmts']
    comments = []
    
    try:
        for item in json_data:

            comment = []
            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'])
            
            comments.append(comment)
            
        return comments
    
    except Exception as e:
        print(comment)
        print(e)
        
def save_data(comments):
    '''
    功能:将comments中的信息输出到文件中/或数据库中。
    参数:comments 将要保存的数据  
    '''
    filename = 'Data/comments.csv'
    
    dataframe = pd.DataFrame(comments)
    dataframe.to_csv(filename, mode='a', index=False, sep=',', header=False)
    

def main():
    '''
    功能:爬虫调度器,根据规则每次生成一个新的请求 url,爬取其内容,并保存到本地。
    '''

    start_time = datetime.datetime.now().strftime('%Y-%m-%d  %H:%M:%S')
    end_time = '2018-11-16  00:00:00'  # 电影上映时间,评论爬取到此截至
    
    while start_time > end_time:
        url = 'http://m.maoyan.com/mmdb/comments/movie/1208282.json?_v_=yes&offset=0&startTime=' + start_time.replace('  ', '%20')
        html = None
        
        try:
            html = get_data(url)
        
        except Exception as e:
            time.sleep(0.5)
            html = get_data(url)
        
        else:
            time.sleep(0.1)
            
        comments = parse_data(html)
        #print(url)
        start_time = comments[14][4]
        print(start_time)
        
        start_time = datetime.datetime.strptime(start_time, '%Y-%m-%d  %H:%M:%S') + datetime.timedelta(seconds=-1)
        start_time = datetime.datetime.strftime(start_time, '%Y-%m-%d  %H:%M:%S')
        
        save_data(comments)
    
    
if __name__ == '__main__':
    main()
    print("完成!!")

如果大家想用这段代码爬取猫眼电影上其他的电影评论数据,需要注意以下地方:

1. 安装必要的 python 库,就是代码一开头 import 的那些,安装的方式也很简单,在python 终端输入命令行:

pip install ****

(PS:**** 处写库的名字,如 pip install json) 

2. 修改请求 URL , 请将函数 main 里的 url 中的数字改成你要爬取的电影的 ID( 1208282 是电影《无名之辈》的 id )。

http://m.maoyan.com/mmdb/comments/movie/1208282.json?_v_=yes&offset=0&startTime=0

3. 修改 end_time 的值,请将这里的值改为你要爬取的电影的上映时间(或者你希望爬取截止的日期)

    end_time = '2018-11-16  00:00:00'  # 电影上映时间,评论爬取到此截至

4. 修改 save_data 中 ,请将这里的 filename 修改为你希望的存储路径及文件名。

filename = 'Data/comments.csv'

 

基本就是这些了。运行程序后(为了展示爬取进度,以免误认为电脑死机,我这里输出每一次请求的 startTime),当结果输出 “完成!!” 字样时,表示程序已经运行完成。

 

 

数据分析部分 


我准备从四个角度进行分析:观众的地理位置分布,观众的评论日期时间分布,观众的评分情况,以及电影评论的词云图。

数据统计分析方面我使用的是 pandas 和collections 库,中文分词方面我使用的是 jieba 库,数据可视化方面我使用的 pyecharts 库。

 一、观众的地理位置分布

 通过 pyecharts 中的地图模块,将观众的所在城市标注在中国地图上相应位置,这样可以直观地看到观众主要集中在五块区域周围,北部的北京、东部沿海的上海南京、西南的四川和重庆、南方的广州和深圳,以及中部武汉和西安等。

这是观影人数最为集中的20个城市排行榜,可以发现,观影人数跟城市经济发展水平是呈正相关的,经济越发达的地区,人们越愿意去观影并评论电影。

 

二、观众的评论日期时间分布

 

《无名之辈》上映的那天是周五,估计是大家要上班没时间,也可能是同期各个国外大片的竞争挤压,上映前几天数据其实并不是特别好。反而是在上映后第二周的周末(11月24日和25日),观影人数达到了顶峰。

我猜测,一方面可能是同期的国外大片热度已过,大家逐渐将视线放到优秀的国产电影上来,另一方面,由于这部电影 “三无” 的特点前期知名度不高,而后期通过不错的口碑逐渐被大家接受。

从这个角度来看,这部电影应该是非常不错的。

观众评论的时间段,主要集中在晚上九点到十一点之间,估计大概应该是晚上电影散场后到家的时间点,很合理,没毛病。

其实我做这部分的分析主要是为了防止数据造假(之前马蜂窝的那件事),统计分析可以看出,不论是观影的日期,还是评论的时间段,数据的分布其实都是比较合理的,所以这里的评论数据可信度还是比较高的。

 

三、 观众的评分情况

统计了 37086 条评分数据,结果有 59.88% 的观众给了 5 分,19.54% 的观众给了 4.5 分。好评率是相当高。

 

四、电影评论的词云图

最后生成电影评论的词云图,看一看大家都在说什么。

数据分析及可视化部分的 python 源码如下,供大家参考学习交流:

import pandas as pd
from collections import Counter
from pyecharts import Map, Geo, Bar
import jieba
import jieba.analyse
import matplotlib.pyplot as plt
from wordcloud import WordCloud,STOPWORDS,ImageColorGenerator
import snownlp
from PIL import Image
import numpy as np

def read_csv(filename, titles):
    comments = pd.read_csv(filename, names=titles, encoding='gbk')
    return comments

def draw_map(comments):
    try:
        attr = comments['cityName'].fillna("zero_token")
        data = Counter(attr).most_common(300)
        data.remove(data[data.index([(i,x) for i,x in (data) if i == 'zero_token'][0])])
        
        geo = Geo("《毒液》观众位置分布", "数据来源:猫眼电影 - SmartCrane采集", title_color="#fff", title_pos="center", width=1000, height=600, background_color='#404a59')
        attr, value = geo.cast(data)
        geo.add("", attr, value, visual_range=[0, 1000], maptype='china',visual_text_color="#fff", symbol_size=10, is_visualmap=True)
        geo.render("./data/观众位置分布-地理坐标图.html")#生成html文件
        geo#直接在notebook中显示
    except Exception as e:
        print(e)

def draw_bar(comments):
    data_top20 = Counter(comments['cityName']).most_common(20)
    bar = Bar('《毒液》观众来源排行TOP20', '数据来源:猫眼-Ryan采集', title_pos='center', width=1200, height=600)
    attr, value = bar.cast(data_top20)
    bar.add('', attr, value, is_visualmap=True, visual_range=[0, 3500], visual_text_color='#fff', is_more_utils=True, is_label_show=True)
    bar.render('./data/观众来源排行-柱状图.html')
    print("success")
        
def draw_wordCloud(comments):
    data = comments['content']

    comment_data = []
    
    for item in data:
        if pd.isnull(item) == False:
            comment_data.append(item)

    #print(comment_data)
    comment_after_split = jieba.cut(str(comment_data), cut_all=False)
    words = ' '.join(comment_after_split)
    
    #c=Counter(words).most_common()
    #print(c)

    stopwords = STOPWORDS.copy()
    stopwords.add('电影')
    stopwords.add('一部')
    stopwords.add('一个')
    stopwords.add('没有')
    stopwords.add('什么')
    stopwords.add('有点')
    stopwords.add('感觉')
    stopwords.add('毒液')
    stopwords.add('就是')
    stopwords.add('觉得')
    
    wc = WordCloud(width=1080, height=960, background_color='white', font_path='STKAITI.TTF', stopwords=stopwords, max_font_size=400, random_state=50)
    wc.generate_from_text(words)

    plt.figure(figsize=(10, 8))
    plt.imshow(wc)
    plt.axis('off')
    plt.savefig('./data/WordCloud.png')
    plt.show()

def draw_DateBar(comments):
    time = comments['startTime']
    timeData = []
    for t in time:
        if pd.isnull(t) == False:
            date = t.split(' ')[0]
            timeData.append(date)

    data = Counter(timeData).most_common()
    data = sorted(data, key=lambda data : data[0])
    # 由于数据中有两个 11月8日 的数据,不应该出现在我们的数据中,故删去
    del data[0]
    
    print(data)
    
    bar = Bar('《毒液》观众评论数量与日期的关系', '数据来源:猫眼电影-SmartCrane采集', title_pos='center', width=1200, height=600)
    attr, value = bar.cast(data)
    bar.add('', attr, value, is_visualmap=True, visual_range=[0, 3500], visual_text_color='#fff', is_more_utils=True, is_label_show=True)
    bar.render('./data/观众评论日期-柱状图.html')
    print("success")
    
def draw_TimeBar(comments):
    time = comments['startTime']
    timeData = []
    for t in time:
        if pd.isnull(t) == False:
            time = t.split(' ')[1]
            hour = time.split(':')[0]
            timeData.append(hour)

    data = Counter(timeData).most_common()
    data = sorted(data, key=lambda data : data[0])
    # 由于数据中有一个 11月15日 的数据,不应该出现在我们的数据中,故删去
    #del data[0]   
    print(data)
    
    bar = Bar('《毒液》观众评论数量与时间的关系', '数据来源:猫眼电影-SmartCrane采集', title_pos='center', width=1200, height=600)
    attr, value = bar.cast(data)
    bar.add('', attr, value, is_visualmap=True, visual_range=[0, 3500], visual_text_color='#fff', is_more_utils=True, is_label_show=True)
    bar.render('./data/观众评论时间-柱状图.html')
    print("success")
    
if __name__ == "__main__":
    filename = "./data/comments.csv"
    titles = ['nickName','cityName','content','score','startTime']
    comments = read_csv(filename, titles)
    draw_map(comments)
    draw_bar(comments)
    draw_wordCloud(comments)
    draw_DateBar(comments)
    draw_TimeBar(comments)
    print("success")

 这里要注意的几点是:

1. 绘制地图使用的地图图表,在 pyecharts 中已经不再内置,需要自行安装对应的地图文件包。地图文件被分成了三个 Python 包,分别为:

 全球国家地图: echarts-countries-pypkg (1.9MB)

中国省级地图: echarts-china-provinces-pypkg (730KB)

中国市级地图: echarts-china-cities-pypkg (3.8MB)

直接使用 python 的 pip 安装:

pip install echarts-countries-pypkg
pip install echarts-china-provinces-pypkg
pip install echarts-china-cities-pypkg

这里要提醒大家,一定要注意,安装完地图包以后一定要重启jupyter notebook,不然是无法显示地图的。

2. echarts 地图文件包中的城市数据其实并不全,使用爬虫拿到的城市数据去绘制地图时,经常会出现像这样的问题。

ValueError: No coordinate is specified for 达州

原因是地图文件包中的城市数据中找不到 “达州” 这个城市(不过有 “达州市” ),所以解决方案有两种,一种是处理你的城市数据,规范化(该加 “市” 的加 “市” ,该加 “县” 的加 “县”);另一种就是找到城市数据的 json 文件,将缺失的城市数据补进去(找到 city_coordinates.json 这个文件,一般位于 site-packages\pyecharts\datasets 文件夹下)。

注意,修改完文件之后,爬虫程序一定要重新启动,否则是不会生效的。

 3. 编辑完 city_coordinates.json 文件后运行程序,可能会报这样的错误(如果没有报就忽略这一条)

json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)错误

 这是由于我们用 记事本(或者其他编辑器)编辑保存 json 文件时,编辑器自动在文件头部添加了 BOM 字符,导致错误。

解决办法也比较简单,使用代码编辑器(我直接用的是 jupyter Notebook)打开这个 json 文件,会发现文件第一行最前面有个小红点,然后删掉它保存,重新启动运行爬虫程序,解决。

4. 数据为空的情况。由于没有做数据预处理,数据中有许多空值的情况出现,在程序中读取出来后显示是 nan ,我最开始对这个真的是一筹莫展,因为我不知道如何判断一个值是空值(我希望如果读取到某个值是空值,则跳过,但是我使用 “”,nan,“nan”,Null 等等去试都没有用)。

后来查阅 pandas 的相关文档,发现一个函数 pd.isnull(value) ,如果 value 值为空,则函数的返回值为 True,否则返回 False。

5. 绘制词云图时,遇到了一些小问题,如果大家也遇到的同样的问题,可以参考一下。

(1)最初我生成的词云图里,有重复的词汇,这是由于在分词中,分词模式选择不当。jieba.cut 函数中,设置 cut_all = False 即可。

    comment_after_split = jieba.cut(str(comment_data), cut_all=False)
    words = ' '.join(comment_after_split)

(2)词云图片的保存。我是用的是 matplotlib.plot 库,熟悉的话跳过就好了,不熟悉的需要注意一下这样的问题。

    plt.figure(figsize=(10, 8))
    plt.imshow(wc)
    plt.axis('off')
    plt.savefig('./data/WordCloud.png')
    plt.show()

① 就是设置的 figsize ,参数是宽和高,像我这样设置(10,8)的话,生成的图片是 1000*800 的尺寸。 

② savefig 一定要在 show 函数之前,imshow 函数之后,否则保存的图片是空白的。

 

写在后面的话


最近学了大数据课程的数据可视化,突然对这方面产生了很大的兴趣,于是查阅了很多资料,完成了这个爬虫项目。我这篇文章其实也不见得分析的有多么专业多么深入,主要是为了技术交流,而且本着开源的态度,文中也附上了项目的全部 python 源码,希望对这方面感兴趣的大家有所帮助。

 

参考文章 :https://blog.csdn.net/csdnnews/article/details/84531483 

  • 10
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

机灵鹤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值