用爬蟲分析IMDB TOP250電影數據


起因

恰逢諾蘭導演的新片《敦刻爾克》即將在中國上映,作為諾蘭導演的鐵粉,印象中他的很多部電影都進入了IMDB TOP250的榜單,但是具體是多少部呢?他是不是IMDB TOP250 中作品最多的導演呢?哪些演員在這些電影中出鏡最多呢?在這些問題的啟發下,我准備寫一個簡單的爬蟲腳本來獲取我想要的數據。

分析

首先需要對工作的流程進行一個簡單的分析。我們的目標是獲取以下的數據:

  • IMDB TOP250 中導演根據作品數量的排名
  • IMDB TOP250 中演員根據作品數量的排名

要得到以上的數據,我們需要的原始數據包括:

  • IMDB TOP250 的電影數據: 名稱,評分
  • 電影導演
  • 電影演員

頁面HTML分析

讓我們先來看一下數據的來源,IMDB TOP250的網頁

HTML
HTML

 

可以看到在頁面HTML文件中,我們可以得到的數據有電影的評分,電影的名字,電影的年份。但是導演和演員的數據呢?可以發現在頁面上點擊電影的名字,可以到達電影的詳情頁,而這個link也在HTML文件中。
我們接着觀察電影的詳情頁。在HTML中我們可以獲取到導演的信息

Director
Director

 

同時在Cast 的表中還可以獲取到主要演員的信息

Actor
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. 創建表 actors 和 directors來保存演員和導演的信息。
    這個表的結構很簡單,就是演員的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

 

最終的答案是什么呢?各位同學可以自己來揭曉。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM