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