用爬虫分析IMDB TOP250电影数据

起因

恰逢诺兰导演的新片《敦刻尔克》即将在中国上映,作为诺兰导演的铁粉,印象中他的很多部电影都进入了IMDB TOP250的榜单,但是具体是多少部呢?他是不是IMDB TOP250 中作品最多的导演呢?哪些演员在这些电影中出镜最多呢?在这些问题的启发下,我准备写一个简单的爬虫脚本来获取我想要的数据。

分析

首先需要对工作的流程进行一个简单的分析。我们的目标是获取以下的数据:

  • IMDB TOP250 中导演根据作品数量的排名
  • IMDB TOP250 中演员根据作品数量的排名

要得到以上的数据,我们需要的原始数据包括:

  • IMDB TOP250 的电影数据: 名称,评分
  • 电影导演
  • 电影演员

页面HTML分析

让我们先来看一下数据的来源,IMDB TOP250的网页

HTML

可以看到在页面HTML文件中,我们可以得到的数据有电影的评分,电影的名字,电影的年份。但是导演和演员的数据呢?可以发现在页面上点击电影的名字,可以到达电影的详情页,而这个link也在HTML文件中。
我们接着观察电影的详情页。在HTML中我们可以获取到导演的信息

Director

同时在Cast 的表中还可以获取到主要演员的信息

Actor

这样一来我们需要的数据就都有了。

数据库设计

要实现这种类型数据的排名和统计,关系型数据库更加合适。在这里,我的设计是用5个不同的表来记录不同的数据。同时我使用的是开源的MySQL数据库。

  1. 创建一个 imdb_movie schema
  2. 创建表 top_250_movies 用于存储电影的信息:电影名称 name, 电影的发行年份 year, 电影的评分 rate.
    这里还有一个电影的ID, 这个值如何来生成呢?是自动增加呢还是用一个其他的值?在前面的HTML文件中,我观察到电影的链接中有一个tt0111161的部分,所以我猜测0111161就是这部电影在IMDB中的UUID,所以我决定用这个值作为这个表的id值。
CREATE TABLE `top_250_movies` (
`id` int(11) NOT NULL,
`name` varchar(45) NOT NULL,
`year` int(11) DEFAULT NULL,
`rate` float NOT NULL,
PRIMARY KEY (`id`)
)
  1. 创建表 actorsdirectors来保存演员和导演的信息。
    这个表的结构很简单,就是演员的id 和演员的 name. 而演员/导演的ID和前面的电影ID的思路类似,通过演员详情页链接中的ID来设置。
CREATE TABLE `actors` (
  `id` int(11) NOT NULL,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
)
REATE TABLE `directors` (
  `id` int(11) NOT NULL,
  `name` varchar(45) NOT NULL,
  PRIMARY KEY (`id`)
)
  1. 创建表 cast_in_movie来保存演员出演电影的信息。
    由于一个演员可以参演多部电影,而一个电影也有很多的演员,所以这里我会创建一个cast_id来标示每一个出演的关系,这个表中的每一行数据记录了一个演员参演了一部电影。同时是分别使用actor_idmovie_id为Foreign Key与actorstop_250_movies关联。
CREATE TABLE `cast_in_movie` (
  `cast_id` int(11) NOT NULL AUTO_INCREMENT,
  `actor_id` int(11) NOT NULL,
  `movie_id` int(11) NOT NULL,
  PRIMARY KEY (`cast_id`),
  KEY `actor_id_idx` (`actor_id`),
  KEY `movie_id_idx` (`movie_id`),
  CONSTRAINT `actor_id` FOREIGN KEY (`actor_id`) REFERENCES `actors` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
  CONSTRAINT `movie_id` FOREIGN KEY (`movie_id`) REFERENCES `top_250_movies` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
)
  1. 用类似的思路创建表 direct_movie
CREATE TABLE `direct_movie` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `director_id` int(11) NOT NULL,
  `movie_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `director_id_idx` (`director_id`),
  KEY `movie_id_idx` (`movie_id`),
  CONSTRAINT `director_id` FOREIGN KEY (`director_id`) REFERENCES `directors` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
)

脚本实现

在理清了工作流程之后,可以开始实现脚本了。

需要使用的扩展包
import re
import pymysql
import requests
from bs4 import BeautifulSoup
from requests.exceptions import RequestException
1. 解析IMDBTOP250 页面的HTML

代码中主要使用BeautifulSoup对HTML文件进行解析和搜索,获取需要的数据。
另外需要注意的是使用正则表达式来获取电影的ID
id_pattern = re.compile(r'(?<=tt)\d+(?=/?)')
获取的是tt开头 /结尾的字符串,但是不包含tt/,这部分数字就是我们想要的ID。
这个方法是一个生成器,每次的返回是一部电影的数据。

def get_top250_movies_list():
    url = "http://www.imdb.com/chart/top"
    try:
        response = requests.get(url)
        if response.status_code == 200:
            html = response.text
            soup = BeautifulSoup(html, 'lxml')
            movies = soup.select('tbody tr')
            for movie in movies:
                poster = movie.select_one('.posterColumn')
                score = poster.select_one('span[name="ir"]')['data-value']
                movie_link = movie.select_one('.titleColumn').select_one('a')['href']
                year_str = movie.select_one('.titleColumn').select_one('span').get_text()
                year_pattern = re.compile('\d{4}')
                year = int(year_pattern.search(year_str).group())
                id_pattern = re.compile(r'(?<=tt)\d+(?=/?)')
                movie_id = int(id_pattern.search(movie_link).group())
                movie_name = movie.select_one('.titleColumn').select_one('a').string

                yield {
                    'movie_id': movie_id,
                    'movie_name': movie_name,
                    'year': year,
                    'movie_link': movie_link,
                    'movie_rate': float(score)
                }
        else:
            print("Error when request URL")
    except RequestException:
        print("Request Failed")
        return None
2. 将电影数据存入数据库
  1. 首先建立数据库连接
db = pymysql.connect("localhost","testuser01","111111","imdb_movie" )
cursor = db.cursor()
  1. 把电影数据存入数据库
    每次存入前需要检查这条数据是否已经存在,避免出错。
def store_movie_data_to_db(movie_data):
    print(movie_data)
    sel_sql =  "SELECT * FROM top_250_movies \
       WHERE id =  %d" % (movie_data['movie_id'])

    try:
        cursor.execute(sel_sql)
        result = cursor.fetchall()
    except:
        print("Failed to fetch data")
    if result.__len__() == 0:
        sql = "INSERT INTO top_250_movies \
                    (id, name, year, rate) \
                 VALUES ('%d', '%s', '%d', '%f')" % \
              (movie_data['movie_id'], movie_data['movie_name'], movie_data['year'], movie_data['movie_rate'])
        try:
            cursor.execute(sql)
            db.commit()
            print("movie data ADDED to DB table top_250_movies!")
        except:
            # 发生错误时回滚
            db.rollback()
    else:
        print("This movie ALREADY EXISTED!!!")
3. 获取电影详细信息

接着利用上面的得到的movie_data 来获取电影详情页的信息。包括导演信息和演员信息。

def get_movie_detail_data(movie_data):
    url = "http://www.imdb.com" + movie_data['movie_link']
    try:
        response = requests.get(url)
        if response.status_code == 200:
            soup = BeautifulSoup(response.text, 'lxml')
            # Parse Director's info
            director = soup.select_one('span[itemprop="director"]')
            person_link = director.select_one('a')['href']
            director_name = director.select_one('span[itemprop="name"]')
            id_pattern = re.compile(r'(?<=nm)\d+(?=/?)')
            person_id = int(id_pattern.search(person_link).group())
            movie_data['director_id'] = person_id
            movie_data['director_name'] = director_name.string
            store_director_data_in_db(movie_data)
            #parse Cast's data
            cast = soup.select('table.cast_list tr[class!="castlist_label"]')
            for actor in get_cast_data(cast):
                store_actor_data_to_db(actor, movie_data)
        else:
            print("GET url of movie Do Not 200 OK!")
    except RequestException:
        print("Get Movie URL failed!")
        return None

获取演员信息的方法:

def get_cast_data(cast):
    for actor in cast:
        actor_data = actor.select_one('td[itemprop="actor"] a')
        person_link = actor_data['href']
        id_pattern = re.compile(r'(?<=nm)\d+(?=/)')
        person_id = int(id_pattern.search(person_link).group())
        actor_name = actor_data.get_text().strip()
        yield {
            'actor_id': person_id,
            'actor_name': actor_name
        }
4. 把导演信息存入数据库

这里需要在两个table中插入数据。首先在directors中插入导演的数据,同样检查记录是否已经存在。接着在 direct_movie插入数据,插入前也检查是否已经存在相同的数据。

def store_director_data_in_db(movie):
    sel_sql = "SELECT * FROM directors \
               WHERE id =  %d" % (movie['director_id'])

    try:
        # 执行sql语句
        cursor.execute(sel_sql)
        # 执行sql语句
        result = cursor.fetchall()

    except:
        print("Failed to fetch data")

    if result.__len__() == 0:
        sql = "INSERT INTO directors \
                            (id, name) \
                         VALUES ('%d', '%s')" % \
              (movie['director_id'], movie['director_name'])
        try:
            # 执行sql语句
            cursor.execute(sql)
            # 执行sql语句
            db.commit()
            print("Director data ADDED to DB table directors!", movie['director_name'] )
        except:
            # 发生错误时回滚
            db.rollback()
    else:
        print("This Director ALREADY EXISTED!!")

    sel_sql = "SELECT * FROM direct_movie \
                   WHERE director_id =  %d AND movie_id = %d" % (movie['director_id'], movie['movie_id'])

    try:
        # 执行sql语句
        cursor.execute(sel_sql)
        # 执行sql语句
        result = cursor.fetchall()

    except:
        print("Failed to fetch data")

    if result.__len__() == 0:
        sql = "INSERT INTO direct_movie \
                                (director_id, movie_id) \
                             VALUES ('%d', '%d')" % \
              (movie['director_id'], movie['movie_id'])
        try:
            # 执行sql语句
            cursor.execute(sql)
            # 执行sql语句
            db.commit()
            print("Director direct movie data ADD to DB table direct_movie!")
        except:
            # 发生错误时回滚
            db.rollback()
    else:
        print("This Director direct movie ALREADY EXISTED!!!")
5. 把演员信息存入数据库

这里需要在两个table中插入数据。首先在actors中插入演员的数据,同样检查记录是否已经存在。接着在cast_in_movie插入数据,插入前也检查是否已经存在相同的数据。

def store_actor_data_to_db(actor, movie):
    sel_sql = "SELECT * FROM actors \
           WHERE id =  %d" % (actor['actor_id'])

    try:
        # 执行sql语句
        cursor.execute(sel_sql)
        # 执行sql语句
        result = cursor.fetchall()

    except:
        print("Failed to fetch data")

    if result.__len__() == 0:
        sql = "INSERT INTO actors \
                        (id, name) \
                     VALUES ('%d', '%s')" % \
              (actor['actor_id'], actor['actor_name'])

        try:
            # 执行sql语句
            cursor.execute(sql)
            # 执行sql语句
            db.commit()
            print("actor data ADDED to DB table actors!")
        except:
            # 发生错误时回滚
            db.rollback()
    else:
        print("This actor has been saved already")

    sel_sql = "SELECT * FROM cast_in_movie \
               WHERE actor_id =  %d AND movie_id = %d" % (actor['actor_id'], movie['movie_id'])
    try:
        # 执行sql语句
        cursor.execute(sel_sql)
        # 执行sql语句
        result = cursor.fetchall()

    except:
        print("Failed to fetch data")

    if result.__len__() == 0:
        sql = "INSERT INTO cast_in_movie \
                        (actor_id, movie_id) \
                     VALUES ('%d', '%d')" % \
              (actor['actor_id'], movie['movie_id'])

        try:
            # 执行sql语句
            cursor.execute(sql)
            # 执行sql语句
            db.commit()
            print("actor casted in movie data ADDED to DB table cast_in_movie!")
        except:
            # 发生错误时回滚
            db.rollback()
    else:
        print("This actor casted in movie data ALREADY EXISTED")
6. 完成代码

这里需要注意的是在操作完成或者出错的情况下都要关闭数据库连接。

def main():
    try:
        for movie in get_top250_movies_list():
            store_movie_data_to_db(movie)
            get_movie_detail_data(movie)
    finally:
        db.close()


if __name__ == '__main__':
    main()

数据库查询分析

运行脚本完成数据获取之后,我们通过SQL语句来获取我们最终想要的数据

IMDB TOP250导演排名
SELECT dm.director_id, d.name, count(dm.id) as direct_count
FROM imdb_movie.direct_movie as dm
JOIN imdb_movie.directors as d ON d.id = dm.director_id
group by dm.director_id
order by direct_count desc
IMDB TOP250演员排名
SELECT cm.actor_id, a.name, count(cm.actor_id) as count_of_act
FROM imdb_movie.cast_in_movie as cm
JOIN imdb_movie.actors as a ON a.id = cm.actor_id
group by cm.actor_id
order by count_of_act desc

最终的答案是什么呢?各位同学可以自己来揭晓。

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

推荐阅读更多精彩内容