Python3爬虫及数据分析实战:以猫眼为例

一、抓取网页源代码

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')
image

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

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')
image

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

3. 分数情况

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

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

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

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

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

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

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

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

看起来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)
image

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)
image

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

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

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

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的抓取,也进行了简单的描述统计分析,下次我们再考虑下其他的网页解析工具的使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,069评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,212评论 1 287
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,912评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,424评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,741评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,194评论 1 206
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,553评论 2 307
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,289评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,923评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,251评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,775评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,144评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,698评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,936评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,658评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,214评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,159评论 2 258

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,534评论 25 707
  • 李红丽 焦点网络初七 坚持分享第76天 三.辅导角色与保密的界定与澄清 对于非自愿来访者,老师在一开始辅导时,先直...
    丽日晴空阅读 254评论 0 0
  • ① 十年前,她和他在同一所初中的同一个班上成为了同桌,她三五成群学渣,他独来独往学霸。 七年前,她和他在同一所高中...
    萍萍_sunshine阅读 240评论 0 0
  • 我的家在苏北的一个小农村,我大学去了北京上学。大学毕业后我就去了广东工作,最后工作被分配到广东的一个小镇上。陌生的...
    谁是谁的谁321阅读 293评论 0 1
  • 我希望每天叫醒我的,不是闹钟,而是梦想。 有一种人,喜欢这样的鸡汤,隔三差五的喝,喝得多了,仿佛整个人都精神了许多...
    子寒飞花阅读 394评论 0 0