实战项目 2:多种方法爬取并分析猫眼 TOP 100 电影
Python入门爬虫与数据分析

通常来说,实现一个爬虫分三个步骤:

1 获取网页
爬虫就是爬取网页上的内容,首先获取网页内容,比如文本、图片、视频等,这些信息都包裹在网页的 HTML 源代码中,爬虫程序先请求获取网页的源代码,接下来在源代码中提取我们想要的信息。

2 提取信息
从杂乱的网页源代码中,提取出整洁有序的信息是爬虫中最重要的一步。通常需提取的信息是包裹在一层又一层的网页源代码节点中的,有多种方法可以来提取出这些信息,最通用万能的方法是采用「正则表达式」提取,使用它的好处在于不用去分析源代码中繁复错杂的节点信息,直接使用正则表达式语法去提取,简单直接。不过正则表达式语法写起来比较复杂,也容易出错,效率不高,所以一般不把它当作首选方法。

Python 中有一些专门提取网页信息的库,比如 Beautiful Soup 、PyQuery 、lxml 等。这些库可以利用 CSS 选择器或者 XPath 来提取信息,比写正则表达式简单许多。借助一些插件,信息提取更容易,速度更快。

3 存储数据
信息提取好可保存下来以便后续使用,Python 中可选择多种数据保存形式,
比如 CSV 、txt,也可以保存到数据库,如 MySQL 、MongoDB 等。

了解实现爬虫的基本步骤后,我们上手一个实战项目来加深理解,这个项目是用多种方法爬取并分析「猫眼 TOP 100 电影」。

项目知识点:

Requests 请求库用法

正则表达式用法

lxml+XPath 用法

Beatutiful Soup+CSS 选择器用法

Beatutiful Soup+Find_all 用法

  1. 流程分析
    这是要爬取的网页:http://maoyan.com/board/4?offset=0

13258-3hstx1trei8.png

该网页上有 100 电影的电影名称、演员名、评分、上映时间等信息,需要爬取下来,然后存储到本地 CSV 文件。
12246-9qvthopkrto.png

接着,对这些数据做简单地分析,分析内容包括这几方面:

哪部电影评分最高?

哪位演员作品数量最多?

哪个国家/地区上榜电影数量最多?

哪一年上榜电影数量最多?

  1. 网页抓取
    先来分析下 URL 规律,下拉页面到底部,点击第 2 页,网址变成:

http://maoyan.com/board/4?offset=10

offset 表示偏移,10 代表一个页面的电影偏移数量,可以推断出 url 的变化规律,即:第一页电影是从 0-10,第二页电影是从 11-20。要获取全部 100 部电影,只需要构造出 10 个 url,然后用 Python 请求库请求网页,从返回的响应中提取出所需内容就可以了。

常用的 Python 请求库有 urllib 和 Requests,相比 urllib,Requests 更强大好用,所以我们直用 Requests 方法来请求网页。

2.1. Requests 请求网页
先定义一个函数 get_one_page() ,传入 url 参数来请求第一页内容。

def get_one_page(url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36'}
    # 不加 headers 爬不了
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        return response.text

接下来在 main()函数中设置 url。

def main():
    url = 'http://maoyan.com/board/4?offset=0'
    html = get_one_page(url)
    print(html)
if __name__ == '__main__':
    main()

运行程序,首页源代码就爬取下来了:
20850-h6llv7m0zze.png

接下来就需要从 HTML 源代码中提取出所需内容,我们前述所说的四种方法来解析提取,下面一一介绍。

2.2. 正则表达式提取
正则表达式从字面上难以理解,下面这串看起来乱七八糟的符号就是正则表达式。

'<dd>.*?board-index.*?>(\d+)</i>.*?data-src="(.*?)".*?name"><a.*?>(.*?)</a>.*?'

它是一种强大的字符串处理工具,之所以叫正则表达式,是因为它能别正则字符串。什么意思呢,就是说:“ 如果你给我的字符串符合规则,就返回它;如果字符串不符合规则,就忽略它”。Requests 请求网页返回 HTML是一堆字符串,使用它处理后便可提取出想要的内容。

常见的正则表达式语法有这些:

模式    描述
\w    匹配字母数字及下划线
\W    匹配非字母数字及下划线
\s    匹配任意空白字符,等价于 [\t\n\r\f]
\S    匹配任意非空字符
\d    匹配任意数字,等价于 [0-9]
\D    匹配任意非数字
\n    匹配一个换行符
\t    匹配一个制表符
^    匹配字符串开始位置的字符
$    匹配字符串的末尾
.    匹配任意字符,除了换行符
[...]    用来表示一组字符,单独列出:[amk] 匹配 'a','m' 或 'k'
[^...]    不在 [ ] 中的字符
*    匹配前面的字符、子表达式或括号里的字符 0 次或多次
+    同上,匹配至少一次
?    同上,匹配 0 到 1 次
{n}    匹配前面的字符、子表达式或括号里的字符 n 次
{n, m}    同上,匹配 m 到 n 次(包含 m 或 n)
( )    匹配括号内的表达式,也表示一个组

正则表达式不需要专门或者深入去学习,实战中遇到的问题在线搜索解决就好,推荐两个不错的教程:

http://www.runoob.com/regexp/regexp-syntax.html

https://www.w3cschool.cn/regexp/zoxa1pq7.html

下面,就来提取所需信息。右键网页-检查-Network 选项,选中左边第一个文件然后定位到电影信息的相应位置,如下图:
99483-xa62mwmc96.png

可以看到每部电影的相关信息都在dd这个节点之中,就可以从该节点运用正则提取。
第 1 个要提取的内容是电影排名,是数字。

它位于 class="board-index"的i节点内。不需要提取的内容用'.*?'替代,需要提取的数字排名用()括起来,()里面的数字表示为(d+)。正则表达式可以写为:

'<dd>.*?board-index.*?>(\d+)</i>'

第 2 个需要提取的是封面图片网址,URL 位于 img 节点的'data-src'属性中,正则表达式可写为:

'data-src="(.*?)".*?'

第 1 和第 2 个正则之间的代码是不需要的,用'.*?'替代,所以这两部分合起来写就是:

'<dd>.*?board-index.*?>(\d+)</i>.*?data-src="(.*?)"

同理,可以依次用正则写下主演、上映时间和评分等内容,完整的正则表达式如下:

'<dd>.*?board-index.*?>(\d+)</i>.*?data-src="(.*?)".*?name"><a.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime">(.*?)</p.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>'

正则表达式写好以后,可以定义一个页面解析提取方法:parse_one_page(),用来提取内容:

def parse_one_page(html):
    pattern = re.compile(
        '<dd>.*?board-index.*?>(\d+)</i>.*?data-src="(.*?)".*?name"><a.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime">(.*?)</p.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S)
    # re.S 表示匹配任意字符,如果不加,则无法匹配换行符
    items = re.findall(pattern, html)
    # print(items)
    for item in items:
        yield {
            'index': item[0],
            'thumb': get_thumb(item[1]),  # 定义 get_thumb()方法进一步处理网址
            'name': item[2],
            'star': item[3].strip()[3:],
            # 'time': item[4].strip()[5:],
            # 用两个方法分别提取 time 里的日期和地区
            'time': get_release_time(item[4].strip()[5:]),
            'area': get_release_area(item[4].strip()[5:]),
            'score': item[5].strip() + item[6].strip()
            # 评分 score 由整数+小数两部分组成
        }

tips:

re.S:匹配任意字符,如果不加则无法匹配换行符;
yield:使用 yield 的好处是作为生成器,可以遍历迭代,并且将数据整理形成字典,输出结果美观。
.strip():用于去掉字符串中的空格。

为了便于提取内容,这里又定义了 3 个方法:get_thumb()、get_release_time()和 get_release_area():

# 获取封面大图
def get_thumb(url):
    pattern = re.compile(r'(.*?)@.*?')
    thumb = re.search(pattern, url)
    return thumb.group(1)
# http://p0.meituan.net/movie/5420be40e3b755ffe04779b9b199e935256906.jpg@160w_220h_1e_1c
# 去掉@160w_220h_1e_1c 就是大图  
# 提取上映时间函数
def get_release_time(data):
    pattern = re.compile(r'(.*?)(\(|$)')
    items = re.search(pattern, data)
    if items is None:
        return '未知'
    return items.group(1)  # 返回匹配到的第一个括号(.*?)中结果即时间
# 提取国家/地区函数
def get_release_area(data):
    pattern = re.compile(r'.*\((.*)\)')
    # $表示匹配一行字符串的结尾,这里就是(.*?);\(|$,表示匹配字符串含有(,或者只有(.*?)
    items = re.search(pattern, data)
    if items is None:
        return '未知'
    return items.group(1)

tips:

'r':正则前面加上'r' 是为了告诉编译器这个 string 是个 raw string,不要转意'\'。当一个字符串使用了正则表达式后,最好在前面加上'r';
'|' ''|' '
:
正
则
表
示
或
,


'表示匹配一行字符串的结尾;
.group(1):意思是返回 search 匹配的第一个括号中的结果,即(.*?),gropup()则返回所有结果 2013-12-18(,group(1)返回'('。

接下来,修改 main()函数来输出爬取的内容:

def main():
    url = 'http://maoyan.com/board/4?offset=0'
    html = get_one_page(url)
    for item in parse_one_page(html):  
        print(item)
if __name__ == '__main__':
    main()

tips:

if __ name__ == '_ main_': 当.py 文件被直接运行时,if __ name__ == '_ main_'之下的代码块将被运行;当.py 文件以模块形式被导入时,if __ name__ == '_ main_'之下的代码块不被运行。

参考:https://blog.csdn.net/yjk13703623757/article/details/77918633

最后运行程序,就能提取出第一页我们需要的全部信息:

{'index': '1', 'thumb': 'http://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg', 'name': '霸王别姬', 'star': '张国荣,张丰毅,巩俐', 'time': '1993-01-01', 'area': '中国香港', 'score': '9.6'}
{'index': '2', 'thumb': 'http://p0.meituan.net/movie/54617769d96807e4d81804284ffe2a27239007.jpg', 'name': '罗马假日', 'star': '格利高里·派克,奥黛丽·赫本,埃迪·艾伯特', 'time': '1953-09-02', 'area': '美国', 'score': '9.1'}
{'index': '3', 'thumb': 'http://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg', 'name': '肖申克的救赎', 'star': '蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿', 'time': '1994-10-14', 'area': '美国', 'score': '9.5'}
{'index': '4', 'thumb': 'http://p0.meituan.net/movie/e55ec5d18ccc83ba7db68caae54f165f95924.jpg', 'name': '这个杀手不太冷', 'star': '让·雷诺,加里·奥德曼,娜塔莉·波特曼', 'time': '1994-09-14', 'area': '法国', 'score': '9.5'}
{'index': '5', 'thumb': 'http://p1.meituan.net/movie/f5a924f362f050881f2b8f82e852747c118515.jpg', 'name': '教父', 'star': '马龙·白兰度,阿尔·帕西诺,詹姆斯·肯恩', 'time': '1972-03-24', 'area': '美国', 'score': '9.3'}
...
}
[Finished in 1.9s]

以上是第 1 种提取方法,如果还不习惯正则表达式这种复杂的语法,可以试试下面的第二种方法。

2.3. lxml 结合 XPath 提取
该方法需要用到 lxml 这款解析利器,同时利用 XPath 语法的路径选择表达式,高效提取网页内容。lxml 包为第三方包,需要自行安装。

XPath,全称 XML Path Language,即 XML 路径语言,是一门在XML文档中查找信息的语言,同样适用于 HTML 文档的搜索。是一个爬虫信息抽取利器。

xpath 常用规则如下:

表达式    描述
nodename    选取此节点的所有子节点
/    绝对路径,从当前节点选取直接子节点
//    x相对路径,从当前节点选取子孙节点
.    选取当前节点
..    选取当前节点的父节点
@    选取属性
*    匹配任何元素节点。
@*    匹配任何属性节点。

在实例中的具体用法,可以下面这篇文章:

https://zhuanlan.zhihu.com/p/29436838

下面,我们利用 lxml 和 XPath 来提取信息。

</div>
    <div class="container" id="app" class="page-board/index" >
<div class="content">
    <div class="wrapper">
        <div class="main">
            <p class="update-time">2018-08-18<span class="has-fresh-text">已更新</span></p>
            <p class="board-content">榜单规则:将猫眼电影库中的经典影片,按照评分和评分人数从高到低综合排序取前 100 名,每天上午 10 点更新。相关数据来源于“猫眼电影库”。</p>
            <dl class="board-wrapper">
                <dd>
                        <i class="board-index board-index-1">1</i>
    <a href="/films/1203" title="霸王别姬" class="image-link" data-act="boarditem-click" data-val="{movieId:1203}">
      <img src="//ms0.meituan.net/mywww/image/loading_2.e3d934bf.png" alt="" class="poster-default" />
      <img data-src="http://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c" alt="霸王别姬" class="board-img" />
    </a>
    <div class="board-item-main">
      <div class="board-item-content">
              <div class="movie-item-info">
        <p class="name"><a href="/films/1203" title="霸王别姬" data-act="boarditem-click" data-val="{movieId:1203}">霸王别姬</a></p>
        <p class="star">
                主演:张国荣,张丰毅,巩俐
        </p>
<p class="releasetime">上映时间:1993-01-01(中国香港)</p>    </div>
    <div class="movie-item-number score-num">
<p class="score"><i class="integer">9.</i><i class="fraction">6</i></p>    
    </div>
      </div>
    </div>
                </dd>
                <dd>

根据截取的部分 html 网页,先来提取第 1 个电影排名信息,有两种方法。
第一种是直接复制。

右键-Copy-Copy Xpath,得到 XPath 路径为://*[@id="app"]/div/div/div[1]/dl/dd[1]/i,为了能够提取到页面所有的排名信息,需进一步修改为:/*[@id="app"]/div/div/div[1]/dl/dd/i/text(),如果想要再精简一点,可以省去中间部分绝对路径'/'然后用相对路径'//'代替,最后进一步修改为://*[@id="app"]//div//dd/i/text()。

24189-nd42ahyb5kb.png

第二种:观察网页结构自己写。

注意到id = app的 div 节点,因为在整个网页结构 id 是唯一的不会有第二个相同的,所有可以将该 div 节点作为 xpath 语法的起点,然后往下观察分别是 3 级 div 节点,可以省略写为://div,再往下分别是是两个并列的p节点、dl节点、dd节点和最后的i节点文本。中间可以随意省略,只要保证该路径能够选择到唯一的文本值'1'即可,例如省去 p 和 dl 节点,只保留后面的节点。这样,完整路径可以为:*//*[@id="app"]//div//dd/i/text(),和上式一样。

12475-2b115s1ktfy.png

根据上述思路,可以写下其他内容的 XPath 路径。观察到路径的前一部分://*[@id="app"]//div//dd都是一样的,从后面才开始不同,因此为了能够精简代码,将前部分路径赋值为一个变量 items,最终提取的代码如下:

2 用 lxml 结合 xpath 提取内容
from lxml import etree
def parse_one_page2(html):
    parse = etree.HTML(html)
    items = parse.xpath('//*[@id="app"]//div//dd')
    # 完整的是//*[@id="app"]/div/div/div[1]/dl/dd
    # print(type(items))
    # *代表匹配所有节点,@表示属性
    # 第一个电影是 dd[1],要提取页面所有电影则去掉[1]
    # xpath://*[@id="app"]/div/div/div[1]/dl/dd[1]  
    for item in items:
        yield{
            'index': item.xpath('./i/text()')[0],
            #./i/text()前面的点表示从 items 节点开始
            #/text()提取文本
            'thumb': get_thumb(str(item.xpath('./a/img[2]/@data-src')[0].strip())),
            # 'thumb': 要在 network 中定位,在 elements 里会写成@src 而不是@data-src,从而会报 list index out of range 错误。
            'name': item.xpath('./a/@title')[0],
            'star': item.xpath('.//p[@class = "star"]/text()')[0].strip(),
            'time': get_release_time(item.xpath(
                './/p[@class = "releasetime"]/text()')[0].strip()[5:]),
            'area': get_release_area(item.xpath(
                './/p[@class = "releasetime"]/text()')[0].strip()[5:]),
            'score' : item.xpath('.//p[@class = "score"]/i[1]/text()')[0] + \
            item.xpath('.//p[@class = "score"]/i[2]/text()')[0]
        }

tips:

[0]:XPath 后面添加了[0]是因为返回的是只有 1 个字符串的 list,添加[0]是将 list 提取为字符串,使其简洁;
Network:要在最原始的 Network 选项卡中定位,而不是 Elements 中,不然提取不到相关内容;
p[@class = "star"]/text():提取 class 属性为"star"的 p 节点的文本值;
img[2]/@data-src':提取 img 节点的 data-src 属性值,属性值后面无需添加'/text()'

运行程序,就可成功地提取出所需内容,结果和第一种方法一样。

如果不太习惯 XPath 语法,可以试试下面的第三种方法。

2.4. Beautiful Soup + CSS 选择器
Beautiful Soup 同 lxml 一样,是一个非常强大的 Python 解析库,可以从 HTML 或 XML 文件中提取效率非常高,常用的语法如下:

节点    含义
soup.title    提取 title 节点的信息
soup.title.string    提取 title 节点下的文本
soup.title.name    提取 title 节点名称
soup.title.attrs [’ name’]    提取 title 节点下的 attrs 属性名称
更多用法可参考下面的教程:
https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/

不过 Beautiful Soup 通常结合 CSS 选择器一起使用,形成 soup.select 方法,提取信息更简单。CSS 选择器选是一种模式,用于选择需要添加样式的元素,使用它的语法同样能够快速定位到所需节点,然后提取相应内容。

CSS 选择器常用的规则 :

选择器    例子    例子描述
.class    .intro    选择 class="intro" 的所有元素。
id    firstname    选择 id="firstname" 的所有元素。
*    *    选择所有元素。
element    p    选择所有 p 元素。
element,element    div,p    选择所有 div 元素和所有 p 元素。
element?element    div p    选择 div 元素内部的所有 p 元素。
element>element    div>p    选择父元素为 div 元素的所有 p 元素。
element+element    div+p    选择紧接在 div 元素之后的所有 p 元素。
[attribute]    [target]    选择带有 target 属性所有元素。
[attribute=value]    [target=_blank]    选择 target="_blank" 的所有元素。
更多用法可参考下面的教程:
http://www.w3school.com.cn/cssref/css_selectors.asp

下面就利用这种方式来提取:

3 用 beautifulsoup + css 选择器提取
def parse_one_page3(html):
    soup = BeautifulSoup(html, 'lxml')
    items = range(10)
    for item in items:
        yield{
            'index': soup.select('dd i.board-index')[item].string,
            # iclass 节点完整地为'board-index board-index-1',写 board-index 即可
            'thumb': get_thumb(soup.select('a > img.board-img')[item]["data-src"]),
            # 表示 a 节点下面的 class = board-img 的 img 节点,注意浏览器 eelement 里面是 src 节点,而 network 里面是 data-src 节点,要用这个才能正确返回值
            'name': soup.select('.name a')[item].string,
            'star': soup.select('.star')[item].string.strip()[3:],
            'time': get_release_time(soup.select('.releasetime')[item].string.strip()[5:]),
            'area': get_release_area(soup.select('.releasetime')[item].string.strip()[5:]),
            'score': soup.select('.integer')[item].string + soup.select('.fraction')[item].string
        }

运行上述程序,结果同同前述方法一样。

2.5. Beautiful Soup + find_all 函数提取
Beautifulsoup 除了和 CSS 选择器搭配,还可以直接用它自带的 find_all 函数进行提取。
find_all,顾名思义,就是查询所有符合条件的元素,可以给它传入一些属性或文本来得到符合条件的元素,功能十分强大,API 接口如下:

find_all(name , attrs , recursive , text , **kwargs)
常用的语法规则有这几点

soup.find_all(name='ul'): 查找所有ul节点,ul 节点内还可以嵌套;
li.string 和 li.get_text():都是获取li节点的文本,但推荐使用后者;
soup.find_all(attrs={'id': 'list-1'})):传入 attrs 参数,参数的类型是字典类型,表示查询 id 为 list-1 的节点;
常用的属性比如 id、class 等,可以省略 attrs 采用更简洁的形式,例如:
soup.find_all(id='list-1')
soup.find_all(class_='element')

用这种方法提取内容,可以这样写:

def parse_one_page4(html):
    soup = BeautifulSoup(html,'lxml')
    items = range(10)
    for item in items:
        yield{
            'index': soup.find_all(class_='board-index')[item].string,
            'thumb': soup.find_all(class_ = 'board-img')[item].attrs['data-src'],
            # 用.get('data-src')获取图片 src 链接,或者用 attrs['data-src']
            'name': soup.find_all(name = 'p',attrs = {'class' : 'name'})[item].string,
            'star': soup.find_all(name = 'p',attrs = {'class':'star'})[item].string.strip()[3:],
            'time': get_release_time(soup.find_all(class_ ='releasetime')[item].string.strip()[5:]),
            'area': get_release_time(soup.find_all(class_ ='releasetime')[item].string.strip()[5:]),
            'score':soup.find_all(name = 'i',attrs = {'class':'integer'})[item].string.strip() +soup.find_all(name = 'i',attrs = {'class':'fraction'})[item].string.strip()
        }

提取结果仍然和前述方法一样。

以上,我们用了四种不同方法来解析提取信息,通过对比可以加深对每种方法的理解,接下来就要保存提取的内容。

  1. 数据存储
    上面输出的结果是字典格式,可利用 csv 包的 DictWriter 函数将字典格式数据存储到 csv 文件中。
 数据存储到 csv
def write_to_file3(item):
    with open('猫眼 top100.csv', 'a', encoding='utf_8_sig',newline='') as f:
        # 'a'为追加模式(添加)
        # utf_8_sig 格式导出 csv 不乱码 
        fieldnames = ['index', 'thumb', 'name', 'star', 'time', 'area', 'score']
        w = csv.DictWriter(f,fieldnames = fieldnames)
        # w.writeheader()
        w.writerow(item)

然后修改一下 main()方法:

def main():
    url = 'http://maoyan.com/board/4?offset=0'
    html = get_one_page(url)
    for item in parse_one_page(html):  
        # print(item)
        write_to_csv(item)
if __name__ == '__main__':
    main()

结果如下图:
64113-1rf32tg5ccoh.png

再把封面的图片下载下来:

def download_thumb(name, url,num):
    try:
        response = requests.get(url)
        with open('封面图/' + name + '.jpg', 'wb') as f:
            f.write(response.content)
            print('第%s 部电影封面下载完毕' %num)
            print('------')
    except RequestException as e:
        print(e)
        pass
     # 不能是 w,否则会报错,因为图片是二进制数据所以要用 wb

这样我们就完成了第一页信息爬取和存储。一共有十页信息,下面我们构造一个简单的循环,就可以爬取全部页数信息。

  1. 分页爬取
    剩下 9 页共 90 部电影的数据可以给网址传入一个 offset 参数,然后遍历 URL 重复执行上面的过程即可,代码修改如下:
def main(offset):
    url = 'http://maoyan.com/board/4?offset=' + str(offset)
    html = get_one_page(url)
    for item in parse_one_page(html):  
        write_to_csv(item)
if __name__ == '__main__':
    for i in range(10):
        main(offset = i*10)

这样我们就爬取了全部电影信息,结果如下:
95434-fmnv1urev0b.png
24345-h1uwpwuym9i.png

  1. 数据分析
    俗话说“文不如表,表不如图”。下面爬取的数据做简单的数据可视化分析。

5.1. 评分最高的十部电影
先来看一看评分最高的十部电影是哪些,代码编写如下:

import pandas as pd
import matplotlib.pyplot as plt
import pylab as pl  #用于修改 x 轴坐标
plt.style.use('ggplot')   #默认绘图风格很难看,替换为好看的 ggplot 风格
fig = plt.figure(figsize=(8,5))   #设置图片大小
colors1 = '#6D6D6D'  #设置图表 title、text 标注的颜色
columns = ['index', 'thumb', 'name', 'star', 'time', 'area', 'score']  #设置表头
df = pd.read_csv('maoyan_top100.csv',encoding = "utf-8",header = None,names =columns,index_col = 'index')  #打开表格
# index_col = 'index' 将索引设为 index
df_score = df.sort_values('score',ascending = False)  #按得分降序排列
name1 = df_score.name[:10]      #x 轴坐标
score1 = df_score.score[:10]    #y 轴坐标  
plt.bar(range(10),score1,tick_label = name1)  #绘制条形图,用 range()能搞保持 x 轴正确顺序
plt.ylim ((9,9.8))  #设置纵坐标轴范围
plt.title('电影评分最高 top10',color = colors1) #标题
plt.xlabel('电影名称')      #x 轴标题
plt.ylabel('评分')          #y 轴标题
# 为每个条形图添加数值标签
for x,y in enumerate(list(score1)):
    plt.text(x,y+0.01,'%s' %round(y,1),ha = 'center',color = colors1)
pl.xticks(rotation=270)   #x 轴名称太长发生重叠,旋转为纵向显示
plt.tight_layout()    #自动控制空白边缘,以全部显示 x 轴名称
# plt.savefig('电影评分最高 top10.png')   #保存图片
plt.show()

结果如下图:
81569-bn3hutbj99.png

可以看到,排名最高的分别是两部国产片《霸王别姬》和《大话西游》,其他还包括《肖申克的救赎》、《教父》等。

5.2. 各国电影数量对比
来了解一下这 100 部电影都是来自哪些国家,代码编写如下:

area_count = df.groupby(by = 'area').area.count().sort_values(ascending = False)
# 绘图方法 1
area_count.plot.bar(color = '#4652B1')  #设置为蓝紫色
pl.xticks(rotation=0)   #x 轴名称太长重叠,旋转为纵向
# 绘图方法 2
# plt.bar(range(11),area_count.values,tick_label = area_count.index)
for x,y in enumerate(list(area_count.values)):
    plt.text(x,y+0.5,'%s' %round(y,1),ha = 'center',color = colors1)
plt.title('各国/地区电影数量排名',color = colors1)
plt.xlabel('国家/地区')
plt.ylabel('数量(部)')
plt.show()
# plt.savefig('各国(地区)电影数量排名.png')

结果如下图:
65317-qwz8olup2a.png

可以看到,除去网站自身没有显示国家的电影以外,上榜电影被 10 个国家/地区"承包"了。其中,美国以 30 部电影的绝对优势占据第 1 名,其次是 8 部的日本,7 部的韩国。香港有 5 部,而内地一部都没有。

5.3. 电影大年
这些电影拍摄的年份时间跨度很大,统计一下各年的电影数量,看看是否存在"电影大年"。

# 从日期中提取年份
df['year'] = df['time'].map(lambda x:x.split('/')[0])
# print(df.info())
# print(df.head())
# 统计各年上映的电影数量
grouped_year = df.groupby('year')
grouped_year_amount = grouped_year.year.count()
top_year = grouped_year_amount.sort_values(ascending = False)
# 绘图
top_year.plot(kind = 'bar',color = 'orangered') #颜色设置为橙红色
for x,y in enumerate(list(top_year.values)):
    plt.text(x,y+0.1,'%s' %round(y,1),ha = 'center',color = colors1)
plt.title('电影数量年份排名',color = colors1)
plt.xlabel('年份(年)')
plt.ylabel('数量(部)')
plt.tight_layout()
# plt.savefig('电影数量年份排名.png')
plt.show()

结果如下图:

54925-1ltqnvv5f5c.png
可以看到,100 部电影来自 37 个年份。其中 2011 年上榜电影数量最多,达到 9 部;其次是 2010 年的 7 部。网上盛传的传" 1994 电影史奇迹年" 仅排名第 6,猫眼榜单的权威性有待考量。

另外,上世纪三四十年代也有电影上榜,那会儿还是黑白电影,反映了电影的口碑好坏跟外在技术没有绝对的关系,质量才是王道。

5.4. 电影作品最多的演员
最后,看看前 100 部电影中哪些演员的作品数量最多。

#表中的演员位于同一列,用逗号分割符隔开。需进行分割然后全部提取到 list 中
starlist = []
star_total = df.star
for i in df.star.str.replace(' ','').str.split(','):
    starlist.extend(i)  
# print(starlist)
# print(len(starlist))
# set 去除重复的演员名
starall = set(starlist)
# print(starall)
# print(len(starall))
starall2 = {}
for i in starall:
    if starlist.count(i)>1:
        # 筛选出电影数量超过 1 部的演员
        starall2[i] = starlist.count(i)
starall2 = sorted(starall2.items(),key = lambda starlist:starlist[1] ,reverse = True)
starall2 = dict(starall2[:10])  #将元组转为字典格式
# 绘图
x_star = list(starall2.keys())      #x 轴坐标
y_star = list(starall2.values())    #y 轴坐标
plt.bar(range(10),y_star,tick_label = x_star)
pl.xticks(rotation = 270)
for x,y in enumerate(y_star):
    plt.text(x,y+0.1,'%s' %round(y,1),ha = 'center',color = colors1)
plt.title('演员电影作品数量排名',color = colors1)
plt.xlabel('演员')
plt.ylabel('数量(部)')
plt.tight_layout()
plt.show()  
# plt.savefig('演员电影作品数量排名.png')

结果如下图:
84208-lvd6ujqh5cn.png

张国荣排在了第一位,觉得意外么?其次是梁朝伟和周星驰,再之后是布拉德·皮特。仔细数一下,前十名影星中,香港影星占了 6 位,这份榜单真是偏爱港星。

对张国荣以七部影片的巨大优势占据第一感到好奇,来看看是哪七部电影。

df['star1'] = df['star'].map(lambda x:x.split(',')[0])  #提取 1 号演员
df['star2'] = df['star'].map(lambda x:x.split(',')[1])  #提取 2 号演员
star_most = df[(df.star1 == '张国荣') | (df.star2 == '张国荣')][['star','name']].reset_index('index')
# |表示两个条件或查询,之后重置索引
print(star_most)

可以看到包括排名第一的《霸王别姬》、第 17 名的《春光乍泄》、第 27 名的《射雕英雄传之东成西就》等。这些电影你都看过么。

     index        star              name
0      1   张国荣,张丰毅,巩俐        霸王别姬
1     17   张国荣,梁朝伟,张震        春光乍泄
2     27  张国荣,梁朝伟,张学友  射雕英雄传之东成西就
3     37  张国荣,梁朝伟,刘嘉玲        东邪西毒
4     70   张国荣,王祖贤,午马        倩女幽魂
5     99  张国荣,张曼玉,刘德华        阿飞正传
6    100   狄龙,张国荣,周润发        英雄本色

以上,我们使用了多种方法爬取并分析了猫眼 TOP 100 电影,初步了解了爬虫的基本技法。

最后修改:2020 年 10 月 11 日
如果觉得我的文章对你有用,请随意赞赏