用Python3抓取并分析猫眼电影TOP100

一、抓取网页源代码

import matplotlib as mpl
mpl.use('agg')
%matplotlib inline
import requests
import re
import pandas as pd
import time
import seaborn as sns
sns.set()
mpl.rcParams['font.sans-serif']=[u'SimHei']
mpl.rcParams['axes.unicode_minus']=False

requests是一个强大的模块,可以帮我们模拟绝大多数的浏览器网络请求,这次我们使用它的get方法来获取网页的源代码。


欢迎大家关注我的个人博客【数洞】 【备用站】

def get_one_page(url, headers):
    '''
    抓取单个网页的源码
    '''
    # 添加headers参数是为了伪装成浏览器,避免被反爬虫策略封禁
    response = requests.get(url, headers=headers)
    # 200意味着成功的请求
    if response.status_code == 200:
        return response.content.decode('utf-8')
    return None

通过观察,我们可以看到猫眼电影TOP100页面的url地址是http://maoyan.com/board/4?offset=0,其中0可以替换成10、20、……、90。这是因为TOP100榜单分了十页,每页十部电影,这个可替换的数字参数相当于每页的电影的第一部的序号。这里的编号跟Python中的编号规则一致,从0开始。

# 设置猫眼电影TOP100的url
# 为了方便,我们使用列表推导式来实现url的列举
urls = ['http://maoyan.com/board/4?offset={0}'.format(i) for i in range(0, 100, 10)]

# 用header来假装自己是浏览器,这一部分可以通过浏览器的检查功能来找到,不清楚的可以百度搜索一下,非常简单。
headers = {
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
}
# 先把所有网页源码爬下来
data = []
for url in urls:
    tmp = get_one_page(url, headers=headers)
    if not tmp == None:
        data.append(tmp)
    time.sleep(0.5)
# 我们查看一下爬取的网页数量是否符合预期
print('{0} pages crawled'.format(len(data)))
10 pages crawled

十个网页,符合预期,那接下来我们就应该解析数据了。

二、解析网页数据

Python中存在许多网页解析库,比如使用bs4中的BeautifulSoup、通过lxml使用xpath、使用pymysql,这些都是常用且好用的方案。而这次,我们要自讨苦吃,通过re模块,使用正则表达式的方法来解析数据。

关于正则表达式的语法和规则,可以自行百度。为了简单易懂,我们可以把不同数据的解析拆分开来,通过多个正则表达式的解析来实现各个字段的数据提取。但这种办法有一些缺点,比如网站的源码中对于缺失数据的处理不合预期时,就可能导致某些字段出现缺失数据,这样不同字段的数据列表长度就产生了差异,我们就无法简单地进行合并了。

事实上,针对这种列表式展示内容的网页,针对每个条目,它的不同字段都是放在一起的,前后顺序一般也是固定的。因此,假如我们将每个条目的所有字段一起解析,就可以方便地应对字段缺失的问题了。

不过猫眼电影TOP100的榜单应该是有小编进行手动维护的,所以数据比较规整,暂时不用考虑这个问题。

# 利用正则表达式,解析电影名、主演、排名、上映时间、分数数据
# 使用re.compile将各个正则表达式封装成正则表达式对象,方便后边解析使用。re.S参数是为了让'.'能匹配空格。
actor_pattern = re.compile('\s*(.*?)\s*

'
, re.S) title_pattern = re.compile('class="name".*?movieId.*?>(.*?)

'
, re.S) index_pattern = re.compile('
.*?board-index.*?>(\d+)', re.S) time_pattern = re.compile('(.*?)

'
, re.S) score_pattern = re.compile('(\d+)\.(\d+)

'
, re.S) # 使用列表来存储数据 indexes = [] actors = [] titles = [] release_times = [] scores = [] # 循环解析十个网页,将解析出来的数据附加在对应的列表中 for page in data: indexes.extend(re.findall(index_pattern, page)) titles.extend(re.findall(title_pattern, page)) actors.extend(re.findall(actor_pattern, page)) release_times.extend(re.findall(time_pattern, page)) scores.extend(re.findall(score_pattern, page))
# 清洗主演、上映时间、上映国家或地区、评分数据
actors = [i.strip('主演:') for i in actors]

# 可以看到,上映地区的数据在上映时间后边的括号里,有很多电影上映时间后边没有括号了,通过观察我们发现这些都是中国大陆上映的电影,
# 那我们就将这些默认缺失的部分补充为'中国'
locs = [i.strip('上映时间:')[10:].strip('()') if len(i.strip('上映时间:')) > 10 else '中国' for i in release_times]

# 我们把字符串中‘上映时间:’这些没用的去掉,然后取十位,也就是'YYYY-mm-dd'的长度,事实上这一步我们也可以在正则表达式中解决,
# 比如用'\d'匹配数字等,详细的大家可以自己尝试,这样还可以解决数据格式不符合预期的问题。
# 事实上电影天空之城的上映时间的格式还真的跟其他的不一样,不过此次我们不考虑这个问题
release_times = [i.strip('上映时间:')[:10] for i in release_times]

# 网页里边将分数的个位数与小数用了不同的格式,所以解析的时候我们分开提取了它们,因此需要处理一下
scores = [int(i) + int(j)/10 for i, j in scores]

pandas是Python中数据分析的一个神器,它的很多功能和用法都借鉴了R语言。

这里我们就使用DataFrame来存储并分析数据。

# 生成DataFrame
df = pd.DataFrame({
    'rank': indexes,
    'title': titles,
    'actor': actors,
    'release_time': release_times,
    'score': scores,
    'location': locs
})
# 修改列名
df = df[['rank', 'title', 'actor', 'score', 'location', 'release_time']]
# 保存到本地csv文件中
df.to_csv('./maoyan_top100_movie.csv', index=False)

# 展示一下数据
df.head()
rank title actor score location release_time
0 1 霸王别姬 张国荣,张丰毅,巩俐 9.6 中国 1993-01-01
1 2 罗马假日 格利高里·派克,奥黛丽·赫本,埃迪·艾伯特 9.1 美国 1953-09-02
2 3 肖申克的救赎 蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿 9.5 美国 1994-10-14
3 4 这个杀手不太冷 让·雷诺,加里·奥德曼,娜塔莉·波特曼 9.5 法国 1994-09-14
4 5 教父 马龙·白兰度,阿尔·帕西诺,詹姆斯·肯恩 9.3 美国 1972-03-24

三、数据分析

1. 上映时间分布

首先,我们看一下猫眼TOP100电影都是什么年头的。

# 我们的上映日期是以字符串存储的,需要将上映年份解析出来
df['上映年份'] = df['release_time'].map(lambda x: int(x[:4]))
df['上映年份'].value_counts()
2011    9
2010    7
2013    6
1993    5
2012    5
1994    5
2008    5
2006    4
1998    4
2003    4
2002    4
2001    3
2000    3
1997    3
1999    3
2004    3
1992    3
1965    2
2009    2
2014    2
1954    1
1966    1
1957    1
2017    1
1953    1
1974    1
1940    1
1972    1
1995    1
1975    1
1984    1
1987    1
1988    1
1989    1
1990    1
2015    1
2007    1
1939    1
Name: 上映年份, dtype: int64

虽然我们能看到有不少电影集中分散在千禧年之后的某几年,比如2010-2013年就占了100部电影中的27部,但是这样数据看起来还是太过分散,我们可以考虑以5年为一个区间将数据分布集中起来。

df['上映年份区间'] = pd.cut(df['上映年份'], bins=[1938, 1980, 1990, 1995, 2000, 2005, 2010, 2015, 2018])
df['上映年份区间'].value_counts().sort_index().plot(kind='bar')

可以看到,年头近一些的电影还是更符合当代人的口味,那么我们看看最古老和最新的电影分别是什么。

df.iloc[df['上映年份'].idxmin()]
rank                               10
title                            乱世佳人
actor           费雯·丽,克拉克·盖博,奥利维娅·德哈维兰
score                             9.1
location                           美国
release_time               1939-12-15
上映年份                             1939
上映年份区间                   (1938, 1980]
Name: 9, dtype: object
df.iloc[df['上映年份'].idxmax()]
rank                     100
title                   英雄本色
actor             狄龙,张国荣,周润发
score                    9.2
location                  中国
release_time      2017-11-17
上映年份                    2017
上映年份区间          (2015, 2018]
Name: 99, dtype: object

可以看到,最古老的电影是1939年上映的由费雯·丽主演的《乱世佳人》,鼎鼎大名,名不虚传。数洞更感兴趣的,是最新的电影《英雄本色》,这部电影面世三十多年之后,经过4K技术的修复,在国内正式上映,着实赚了不少忠实粉丝的眼泪。小马哥不是一个角色,而是一个时代。

2. 上映地区分布

看完了上映时间情况,我们再看看上映地区的信息。

df['location'].value_counts().plot(kind='bar')

可以看到,大陆片和美国片最受大家欢迎,日本韩国也有一定受众,素有浪漫之风的法国、意大利紧随其后,中国香港排名第七有些出乎意料,看来当年港片的辉煌已经一去不复返了。

3. 分数情况

我们来看看这TOP100电影评分的分布情况如何:

df.groupby('score')['title'].count().sort_index().plot(kind='bar')

df.score.hist(bins=5)

看来高分电影还是很稀有的,即使在TOP100中,评分在9.5以上的仅有5部。另外分数越高电影越少这一现象也符合预期。

那接下来呢,我们看看哪个地区的电影评分更高:

import matplotlib.pyplot as plt
plt.figure(figsize=(20,8))
sns.boxplot(x='location', y='score', data=df)

可以看到,香港和意大利虽然量少,但整体风评更好,韩国电影相对来说评价较差。

那我们再看看哪些年头的电影评价更好:

plt.figure(figsize=(20,8))
sns.boxplot(x='上映年份区间', y='score', data=df)

看起来1995-2000年的电影质量相当不错

接下来我们综合上映地区和年份来看看分数的情况:

df_heat = df.groupby(['上映年份区间', 'location'])['score'].mean().reset_index().pivot('上映年份区间', 'location', 'score')
cmap = sns.diverging_palette(220, 10, as_cmap=True)
sns.heatmap(df_heat, center=8.8, annot=True, cmap=cmap, linewidths=.5)


df_heat = df.groupby(['上映年份区间', 'location'])['title'].count().reset_index().pivot('上映年份区间', 'location', 'title')
cmap = sns.diverging_palette(220, 10, as_cmap=True)
sns.heatmap(df_heat, center=1, annot=True, cmap=cmap, linewidths=.5)

嗯……由于总共只有100部电影,分不到这么多区间里还是太过稀疏,数据量较大的时候基本上就不会出现这种情况了。数据这么稀疏的情况下,我们看到的结果可能不太具有代表性,不过在此我们就不纠结这个问题。

df['rank'] = df['rank'].map(int)
sns.lmplot(x='score', y='rank', data=df)

榜单排名和评分大致呈反比,即分数越高,排名越靠前,符合预期。

4. 演员情况

我们使用collections中的defaultdict来统计每个演员入榜的电影数:

from collections import defaultdict
actor_movie_cnt = defaultdict(int)

for index, row in df.iterrows():
    for actor in row['actor'].split(','):
        actor_movie_cnt[actor] += 1
sorted(actor_movie_cnt.items(), key=lambda x: x[1], reverse=True)[:10]
[('张国荣', 6),
 ('周星驰', 4),
 ('梁朝伟', 4),
 ('巩俐', 3),
 ('阿尔·帕西诺', 3),
 ('莫文蔚', 3),
 ('克里斯蒂安·贝尔', 3),
 ('布拉德·皮特', 3),
 ('加里·奥德曼', 2),
 ('娜塔莉·波特曼', 2)]

前三名分别是哥哥、星爷以及小编心目中最帅的男人之一——梁朝伟,这三位小编都非常喜欢。

到此为止,我们就完成了猫眼TOP100的抓取,也进行了简单的描述统计分析,下次我们再考虑下其他的网页解析工具的使用。

你可能感兴趣的:(互联网拾遗)