个人博客:http://lixiaohui.live
所有代码托管在Github。
最近有一部“怀旧”题材的电影——《后来的我们》。青春,爱情,梦想,一直是“怀旧”题材的核心要素,那么他的口碑怎么样呢?
借助这个问题,我尝试使用python爬取豆瓣数据并可视化了数据结果,“我最大的遗憾,就是你的遗憾,与我有关”,下面就一起来感受一下吧。
一、爬取数据
1.1 代理ip
代理IP的作用是把我们自己变成伪装者,套上保护色,在夜色中叩开豆瓣大门的时候豆瓣并不知道门外来的是谁,宛如薛定谔的猫一样让豆瓣丈二和尚摸不着头脑,他以为来的是张三,其实是我老李;他以为来的是李四,其实还是我老李手动微笑。
那我们首先从爬取IP开始,工欲善其事必先利其器嘛,代理IP提供了隐身的可能性,加上代理ip我们可以像不会骑扫把的胖女巫一样拥有绕过豆瓣反爬虫机制的能力。我这里用了西刺(ci)网,我刚开始以为是西剌网,想起了王小波的花剌子模信使那个故事,哈哈哈哈对不起扯远了。
首先装载一下必要的库,设置好cookie和useragent,可以通过chrome的F12键在网络里查看到这些信息,直接拷贝过来就行。
import requests
import re
import pandas as pd
url = 'http://www.xicidaili.com/'
cookie = {
'Cookie': '_free_proxy_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiJTAwMTNiMmQ5MGQ0NGMzMzFkNzk0ZmE4ODk4MmMzMzEyBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMXBXK0NNcHRENlhVUUh1YTFQTXYvUE1qdmJCaklYalJtbGFFME56MU84Ulk9BjsARg%3D%3D--7deda3a0bc1e4e26c36fb37dfeb5caf7003df150; Hm_lvt_0cf76c77469e965d2957f0553e6ecf59=1525346941; Hm_lpvt_0cf76c77469e965d2957f0553e6ecf59=1525347052'
}
useragent = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36'
}
西刺网主页长这样,我们需要的是IP、端口号和类型,因为requests要求的proxy需要打包成dict(json)形如下方的形式,所以我们需要这些字段。
proxy = {
"HTTP":"HTTP://xx.xxx.xxx.xxx:port"
}
爬取该网页页面内容代码简单,直接po上来(我对页面内容替换了所有换行符\n
和空格):
response = requests.get(url, cookies = cookie, headers = useragent)
content = response.text.replace("\n", '').replace(" ", '')
按F12键分析一下可以看见我们需要的信息基本都是包裹在<tr class='odd>
这样的标签中,使用正则抽取就行。
举个例子:
<td>124.237.112.100</td><td>8088</td><td>河北秦皇岛</td><tdclass="country">高匿</td><td>HTTPS</td><td>3小时</td><td>不到1分钟</td></tr>
其中的124.237.112.100,端口8088,以及类型HTTPS是我们需要抽取的,正则表达式如下:
ipPattern = re.compile(r'((?:(?:25[0-5]|2[0-4]\d|(?:1\d{2}|[1-9]?\d))\.){3}(?:25[0-5]|2[0-4]\d|(?:1\d{2}|[1-9]?\d))).*?<td>(\d*?)</td>.*?<tdclass="country">.*?</td><td>(HTTP.*?)</td>?')
proxies = re.findall(ipPattern, content)
将代理IP保存到csv文件:
df = pd.DataFrame(proxies, columns=['ip', 'port', 'type'])
df.to_csv('proxies.csv', encoding='utf-8-sig')
1.2 字段抽取
上一步准备好了代理IP,这一步我们就可以正式爬取《后来的我们》热评了,
首先加载代理:
df = pd.read_csv('proxies.csv')
select_columns = ['ip', 'port', 'type']
proxies = df[select_columns]
proxies['concated'] = proxies['type'].astype(str) + "://" + proxies['ip'].astype(str) +":" + proxies['port'].astype(str)
def pick_proxy():
proxy_dict = {}
index = np.random.choice(proxies.shape[0])
proxy = proxies['concated'][index]
head = proxies.loc[index]['type']
proxy_dict[head] = proxy
return proxy_dict
这里将代理ip拼接成HTTP://xx.xxx.xxx.xxx:port的形式方便后面使用,并定义了一个函数pick_proxy用来随机挑选代理IP
最后返回一个代理的dict。
获取网页内容:
url = 'https://movie.douban.com/subject/26683723/comments?status=P'
cookies = {
'Cookie':'"ll="118281"; bid=RXLLSV3Lpl8; _pk_ref.100001.4cf6=%5B%22%22%2C%22%22%2C1525677293%2C%22https%3A%2F%2Fwww.baidu.com%2Fs%3Fie%3Dutf-8%26f%3D8%26rsv_bp%3D0%26rsv_idx%3D1%26tn%3Dbaidu%26wd%3Ddouban%26rsv_pq%3Dec71a7d500054ab9%26rsv_t%3Daea9JlVdYh1gD%252BGRz5EBNm%252FpcUlfXaX4KGe1v0B%252FlIFjMUTaJc95TIgL9nA%26rqlang%3Dcn%26rsv_enter%3D1%26rsv_sug3%3D6%26rsv_sug1%3D6%26rsv_sug7%3D100%26rsv_sug2%3D0%26inputT%3D1530%26rsv_sug4%3D1530%22%5D; _pk_id.100001.4cf6=e4478045f12b58d7.1525677293.1.1525677596.1525677293.; _pk_ses.100001.4cf6=*; __yadk_uid=sG0bTgzZcP364YKhAUooIpRTIZSaDYwU; __utma=30149280.1936213219.1525677295.1525677295.1525677295.1; __utmb=30149280.0.10.1525677295; __utmc=30149280; __utmz=30149280.1525677295.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=223695111.788299203.1525677295.1525677295.1525677295.1; __utmb=223695111.0.10.1525677295; __utmc=223695111; __utmz=223695111.1525677295.1.1.utmcsr=baidu|utmccn=(organic)|utmcmd=organic|utmctr=douban; _vwo_uuid_v2=DC384E6C9A270E7570AA1C544579FBD63|b1ff83255465a52a2ca8a027f6fb3249; ps=y; ue="lixiaohuipb@163.com"; dbcl2="173117659:E2lDtbaaY9I"; ck=3lvF; push_noty_num=0; push_doumail_num=0"'
}
headers = {
'User-Agent':"Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0"
}
sess = requests.Session()
这一步和抽取代理IP的时候是一样的,唯一不同的是useragent是我centos上的浏览器发出的,这一点说来还蛮奇怪,我用windows上的useragent爬取的时候用不了几页就会显示403被禁止访问页面,但是用centos上的就没有问题,存疑。
获取html文本:
def get_content(url):
response = sess.get(url, proxies = pick_proxy(),headers = headers, cookies = cookies, verify=False)
content = response.text
if response.status_code == 200:
soup = BeautifulSoup(content, 'html5lib')
else:
print(response.status_code)
print(content)
return soup
获取包含评论的div,
豆瓣上的热评基本存在comment-item类的div里,准确来说是comment类里,(如下图)使用BeautifulSoup可以很方便地获取每一个div。
def find_result(soup):
result = soup.findAll(class_='comment')
print('We"ve found {} results in this page'.format(len(result)))
return result
对于每一个div,使用正则匹配红色框框中我们需要的字段:
# 评论的正则表达式
commentPatter = re.compile(r'<divclass="comment">.*?<pclass="">(.*?)</p>')
#投票数的正则表达式
votesPattern = re.compile(r'<spanclass="votes">(.*?)</span>')
#评分的正则表达式
ratingsPattern = re.compile(r'<spanclass="allstar.*?rating"title="(.*?)"></span>')
#评论时间的正则表达式
timePattern = re.compile(r'<spanclass="comment-time".*?>(.*?)</span>')
def extract_comment(result):
commentList = []
for res in result:
com = str(res).replace("\n", '').replace(" ", '')
comment = re.findall(commentPatter, com)
votes = re.findall(votesPattern, com)
ratings = re.findall(ratingsPattern, com)
time = re.findall(timePattern, com)
ratings = ratings if ratings else ["None"]
commentList.append((comment[0], votes[0], ratings[0], time[0]))
df = pd.DataFrame.from_records(commentList)
#将字段写入到csv
write2csv(df)
write2csv是写入到csv文件的函数,代码:
def write2csv(df):
df.to_csv('comment.csv',mode='a+', header=False, encoding = 'utf-8-sig')
接下来就是主爬虫函数了:
def scrapper(url):
soup = get_content(url)
result = find_result(soup)
extract_comment(result)
主要的爬取过程如下,遵循获取页面->抽取字段->保存到文件->继续获取下一页页面->直至没有下一页的爬取方式
commentCount = 520
prefix = 'https://movie.douban.com/subject/26683723/comments?start={}&limit=20&sort=new_score&status=P&percent_type='
for i in np.arange(0,commentCount,20):
time.sleep(np.random.rand()*5)
url = prefix.format(i)
print("I am scrapping : " + url)
scrapper(url)
输出结果:
I am scrapping : https://movie.douban.com/subject/26683723/comments?start=0&limit=20&sort=new_score&status=P&percent_type=
We"ve found 20 results in this page
I am scrapping : https://movie.douban.com/subject/26683723/comments?start=20&limit=20&sort=new_score&status=P&percent_type=
We"ve found 20 results in this page
I am scrapping : https://movie.douban.com/subject/26683723/comments?start=40&limit=20&sort=new_score&status=P&percent_type=
We"ve found 20 results in this page
I am scrapping : https://movie.douban.com/subject/26683723/comments?start=60&limit=20&sort=new_score&status=P&percent_type=
We"ve found 20 results in this page
最后保存下来的热评如下方:
爬数据这块主要是使用requests和BeautifulSoup以及re,主要遇到的问题有两个。
一个是豆瓣的反爬虫机制,所以我先到西刺网爬取了一些代理IP备用,再加上了cookie,在每次爬取间隔设置了冷却时间,可以成功绕过豆瓣的反爬虫;
一个是模式匹配的问题,原本我打算只用re正则表达式,但是后来发现只用正则有些难以绕回的门槛,好在豆瓣的div布局相当合理,结合BeautifulSoup可以更方便地抽取我想要的字段。
二、可视化
将所有评论写到csv文件中后我们就可以尝试做些分析和展现了,首先加载数据到工作区:
df = pd.read_csv('comment.csv', header=-1)
cols = ['index', 'comment', 'voting', 'rating', 'date']
df.columns = cols
df.head()
输出结果:
index comment voting rating date
0 0 台词矫情的令人发指! 11071 较差 2018-04-23
1 1 刘若英对着镜头唱一遍后来我都觉得比这片子感动。 10731 较差 2018-04-28
2 2 现在还把北京设定为梦想之城的,大概受众也是瞄准了小镇青年吧(多次冲北京喊话真是挺尴尬的)。剧... 7325 较差 2018-04-23
3 3 毫无看点可言。剧情处处是硬伤!这是电影吗?这是PPT,刘小姐,还是回去唱歌吧。 5973 很差 2018-04-27
4 4 最好的是演员,周冬雨完全开辟出自己的戏路。小井进步惊人,已长出美丽。最差的是编剧,没有一场完... 6336 还行 2018-04-23
我们可以先查看前10条被认为最有用的评论
df['comment'][:10]
我们再看看查看评论打分的分布情况
rating_series = df.groupby('rating')['rating'].count()
rating_series.index
ra = rating_series.sort_values(ascending = False)
plt.figure(figsize = (16,9))
plt.bar(x = ra.index, height = ra)
plt.xlabel(u'评价')
plt.ylabel(u'评价人数')
plt.title('评价情况分布', fontsize = 24)
plt.hlines(40, -.5, 5.0, colors='w')
plt.hlines(80, -.5, 5.0, colors='w')
for i, v in enumerate(ra):
plt.text(i, v + 3, str(v), color='blue', fontweight='bold', size = 'x-large')
# plt.axis('off')
plt.show()
显示结果如下:
可以看出很差和较差两根柱子都高耸入云,看起来评价的确是不大好,我们把很差和较差合并为差评后可以看见(下方饼图):过半数人给出了差评及以下的评价
rating_series['差评'] = rating_series['很差'] + rating_series['较差']
ra_formatted = rating_series.sort_values(ascending = False)
del(ra_formatted['很差'])
del(ra_formatted['较差'])
plt.figure(figsize = (9,9))
plt.pie(x = ra_formatted, labels = ra_formatted.index)
plt.title('评价情况分布', fontsize = 24)
plt.show()
我们可以看看评论数的时间分布情况:
def ratings(df):
n_rating = sum(df['rating'].value_counts())
return (pd.Series(data = {
'n_rating' : n_rating,
}))
rating_by_date = df.groupby(['date', 'rating']).apply(ratings).reset_index()
rating_by_date = rating_by_date.pivot(index='date', columns='rating', values='n_rating')
plt.figure(figsize = (24,9))
cc = sb.color_palette('husl')
for c,col in zip(cc,rating_by_date.columns):
plt.plot(rating_by_date.index, rating_by_date[col], linewidth = 2, c = c)
plt.legend(loc=1, fontsize = 'xx-large')
plt.title('打分随时间分布情况', fontsize = 24)
4月28号是首映,打分数自然最多,认为较差的人最多,还行的人次之,过后第二天都出现了强烈的下坡。
最后我们可以看看热评给我们的首要印象,使用词云尝试做到这点.词云是由两部分构成,第一部分使用jieba分词将评论截断,这是有必要的,因为wordcloud对中文的分词处理得不是很好;第二部分是用wordcloud生成词云,并保存到本地。
使用jieba分词:
content = "".join(df.comment)
rubbish = re.compile(r'<aclass=".*?></a>')
content_text = re.sub(rubbish, '', content)
content_text[:100]
cut_content = jieba.cut(content_text, cut_all=True)
content_cut = " ".join(list(cut_content))
中文里有许多助词比如“还是”,“真的”,“这个”这些是没有多大实际意义的词,我们可以通过停用词过滤掉这部分词汇。
stopwords = set(STOPWORDS)
stopwords = [u'还是',u'真的',u'如果',u'看到',u'这个',u'我们',u'可以', u'应该',u'不是',u'觉得',u'但是',u'有点', u'本来',u'不过',u'很多',u'然后',u'那么',u'所以',u'开始',u'现在',u'一点',u'就是',u'这么',u'一个',u'虽然',u'不会',u'多少',u'以为',u'因为',u'两个',u'真是',u'一部',u'同样',u'只是', u'还有',u'这样',u'一直',u'一颗',u'一些',u'所有',u'确实',u'只能',u'太多',u'什么',]
生成词云:
wordcloud = WordCloud(background_color="white", width=1000, height = 600, font_path=r"C:\simhei.ttf",max_words=200, stopwords=stopwords).generate(content_cut)
plt.figure(figsize = (16,12))
plt.imshow(wordcloud,interpolation='bilinear')
plt.axis('off')
plt.title('《后来的我们》词云图', fontsize = 24)
plt.show()
显示结果:
结果....Emmmm.....自己看吧....
我们还可以使用奶茶作为背景图片生成的词云:
naicha_coloring = np.array(Image.open("naicha.jpg"))
# 你可以通过 mask 参数 来设置词云形状
wc = WordCloud(background_color="white", max_words=300, mask=naicha_coloring,
stopwords=stopwords, max_font_size=120, font_path=r"C:\simhei.ttf", random_state=42)
wc.generate(content_cut)
# 我们还可以直接在构造函数中直接给颜色
# 通过这种方式词云将会按照给定的图片颜色布局生成字体颜色策略
# create coloring from image
image_colors = ImageColorGenerator(naicha_coloring)
plt.figure(figsize = (32,18))
plt.imshow(wc.recolor(color_func=image_colors), interpolation="bilinear")
plt.title('《后来的我们》词云图', fontsize = 24)
plt.axis("off")
显示结果:
可以将该图片保存到本地:
wc_recolor = wc.recolor(color_func=image_colors)
wc_recolor.to_file('wordCloud.png')
可视化遇见的问题主要是wordcloud的中文分词不是很好,太长的句子都不能直接切分成单词,直接可视化出来的效果并不是词云,而是句云,所以我结合了jieba分词将评论切分后再形成词云。
三、总结结论:
在对豆瓣上对于《后来的我们》进行热门短评爬取过后,看起来《后》的确口碑不佳。
1、前十热门短评只有一条表达了较为正面的意见
2、有过半数的影迷给出了差评的评价
3、大家对于刘若英
和冬雨
的话题热度比较高,可能是因为这是奶茶的电影首秀,大家对周冬雨的演技也开始有所看好。
4、词云中看出大家对该片的反映可能还不是很热情,“尴尬”,“退票”,“垃圾”,“呵呵”这一类比较贬义的词出现的次数也比较高
四、后记
纯粹是说些看了复仇者联盟之后的话,国内的青春片实在是糟心得要紧,我觉得老拍伤春悲秋的青春不多见好,跟情歌老唱爱爱恨恨情情伤伤一样不好,我是说,除了老唱衰青春之外需要有些更为积极正面的东西,令人打鸡血的,有些更深层次的东西,让人觉得“诶青春还可以是这样子的,而且本来就应该是这样子的”。令人热力澎湃二十年不好么,非要靠刺痛心灵痛觉卖座,喜欢周星驰,虽然也悲观,但是悲观里还能笑笑。
看过复仇者联盟3之后,实不相瞒,我看了之后觉得特别好,身边很多朋友觉得吐血身亡,英雄死光。
可是我不这样觉得,我觉得满足了我小时候的一个梦想——坏人怎么得也该赢一次,我是说,切切实实地赢一次,再说了,灭霸也算不得上坏人。
我以前也觉得坏人可以赢,并且理所应当要赢,六颗原石象征力量,有力量的人怎么还会输给没力量的人。有力量的人输给了没力量的人,电影可以这么拍,生活里却不这样子写。罗素在八十年前的《个人与权威》里面就领先地看到了权利势必会趋于长尾,财富始终会集中于少数人手里,最终使得这部分人话语权得以大大加强,罗素表达了他对个人的权利能否得到保障的担忧,换句话说,力量的集中会导致授人以柄,刀剑所向的时候个人或小机构不太可能具有抵抗的能力,由此才会有担忧一说。
得益于这么多年来培养出来的“好人必胜坏人必输”的观念,看完了妇联3我觉得特别颠覆。是一种久被蒙蔽突然上帝在我眼前掀开了帘的感觉,漫威敢拍不得不说是很大胆了——虽然有人说是因为初代复仇者联盟片酬太高了笑。
有时候浸淫在一种氛围里太久便容易理所当然,古人云:久居兰室不闻其香,久居鲍市不闻其臭。有时候像黑客帝国一样人们会沉浸在母体里面,也判断不出来自己是不是在母体里面。要是看多了悲伤的青春听多了悲伤的情歌深陷这种母体的概率要更大些,我觉得这种行为可能并不大恰当,不是说悲伤的东西不好,而是除了这些悲伤的东西之外还得有些开心的东西,更深层次的东西,更接近生活实际的东西,复联3就做的很好。
开头的时候说到花剌子模,王小波写的花剌子模是这样的一个人:
据野史记载,中亚古国花剌子模有一古怪的风俗,凡是给君王带来好消息的信使,就会得到提升,给君王带来坏消息的人则会被送去喂老虎。于是将帅出征在外,凡麾下将士有功,就派他们给君王送好消息,以使他们得到提升;有罪,则派去送坏消息,顺便给国王的老虎送去食物。
知道事情有两面性是值得推崇的,就像知道消息有好有坏一样,只看着青春悲伤的一面或者臆想好人手无寸铁也能打败魔王都不大值得提倡——这样子当然可以给人们提供快乐,但是我觉得事物的反面也有值得考量的地方。要是别人告诉我这就是好的东西然后往我脑袋里塞我是拒绝的,我看完正面和反面自然会做出哪些是好的哪些是坏的判断,但在此之前,我还是希望要么就离我远点让我脑袋消停,要么就少叫嚣些虚伪的话。
借用王小波的一段话结尾:
现在我要得出最后一个结论,那就是说,假设有真的学术和艺术存在的话,在人变得滑头时它会离人世远去,等到过了那一阵子,人们又可以把它召唤回来——此种事件叫做「文艺复兴」。我们现在就有召唤的冲动,但我很想打听一下召唤什么。如果是召唤古希腊,我就赞成,如果是召唤花剌子模,我就反对。