一、抓取网页源代码
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('<p\sclass="star">\s*(.*?)\s*</p>', re.S)
title_pattern = re.compile('class="name".*?movieId.*?>(.*?)</a></p>', re.S)
index_pattern = re.compile('<dd>.*?board-index.*?>(\d+)</i>', re.S)
time_pattern = re.compile('<p\sclass="releasetime">(.*?)</p>', re.S)
score_pattern = re.compile('<p\sclass="score"><i\sclass="integer">(\d+)\.</i><i\sclass="fraction">(\d+)</i></p>', 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()
<div>
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>rank</th>
<th>title</th>
<th>actor</th>
<th>score</th>
<th>location</th>
<th>release_time</th>
</tr>
</thead>
<tbody>
<tr>
<th>0</th>
<td>1</td>
<td>霸王别姬</td>
<td>张国荣,张丰毅,巩俐</td>
<td>9.6</td>
<td>中国</td>
<td>1993-01-01</td>
</tr>
<tr>
<th>1</th>
<td>2</td>
<td>罗马假日</td>
<td>格利高里·派克,奥黛丽·赫本,埃迪·艾伯特</td>
<td>9.1</td>
<td>美国</td>
<td>1953-09-02</td>
</tr>
<tr>
<th>2</th>
<td>3</td>
<td>肖申克的救赎</td>
<td>蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿</td>
<td>9.5</td>
<td>美国</td>
<td>1994-10-14</td>
</tr>
<tr>
<th>3</th>
<td>4</td>
<td>这个杀手不太冷</td>
<td>让·雷诺,加里·奥德曼,娜塔莉·波特曼</td>
<td>9.5</td>
<td>法国</td>
<td>1994-09-14</td>
</tr>
<tr>
<th>4</th>
<td>5</td>
<td>教父</td>
<td>马龙·白兰度,阿尔·帕西诺,詹姆斯·肯恩</td>
<td>9.3</td>
<td>美国</td>
<td>1972-03-24</td>
</tr>
</tbody>
</table>
</div>
三、数据分析
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的抓取,也进行了简单的描述统计分析,下次我们再考虑下其他的网页解析工具的使用。