《流浪地球》电影评论的爬取和分析

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

爬虫-爬取数据

找到评论网页地址

先打开猫眼官网找到《流浪地球》的介绍页面:https://maoyan.com/films/248906
在这里插入图片描述

  • 打开开发者工具
  • 转换成手机浏览(因为网页版的评论数据只显示部分短评)
    点击红色箭头指向的位置,然后按F12键刷新,这时候我们就可以看到所有评论了
    在这里插入图片描述

    获取评论请求地址

    在点击打开“查看全部533685条讨论”后,屏幕上的评论往下拉,会发现浏览器的网络展示中会不断加载新页面,网络请求多出来了comments.json的请求:

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

1
2
3
4
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是当前的时间戳,看、可以用如下代码查看
1
2
3
4
5
6
7
8
9
#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来继续抓取,直到遇到第三个时间戳
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# 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并进行统计处理
1
2
3
import numpy as np
data = pd.read_csv('./data.csv')
data.info()
  • 数据详情
    共有102580条数据;
    包含字段:
    评论内容、性别、评论ID、评论者昵称、回复数量、评分、时间、点赞数量、评论者ID、评论者等级

  • 清理数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 删除无用数据
    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.低分跟哪位演员有关?

总体评价如何?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

在这里插入图片描述
低分占比

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

高分占比 8

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

满分占比

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

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

高分的评价理由是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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)

在这里插入图片描述

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

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

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

在这里插入图片描述
通过上图低分词云可以看出,网友评论低分的原因是因为剧情,虽然在中国科幻片上特效制作精良,算是中国国产科幻片里程碑作品,但剧情欠佳。

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

观众总体的性别占比

1
2
3
4
5
6
7
# 总体的性别比例
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

在这里插入图片描述
低分观众的性别占比

1
2
3
4
5
6
7
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

在这里插入图片描述
可以看出低分观众在男女比例上跟总体的男女比例基本一致

高低分跟哪位演员有关?

1
2
3
4
5
6
7
8
9
10
11
12
13
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

在这里插入图片描述

1
2
3
4
5
6
7
8
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

在这里插入图片描述

参考:

https://segmentfault.com/a/1190000018242134



觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭



wechat pay



alipay

《流浪地球》电影评论的爬取和分析
http://yuting0907.github.io/2019/03/09/《流浪地球》电影评论的爬取和分析/
作者
Echo Yu
发布于
2019年3月9日
许可协议