用爬蟲分析IMDB TOP250電影數(shù)據(jù)

起因

恰逢諾蘭導(dǎo)演的新片《敦刻爾克》即將在中國上映问顷,作為諾蘭導(dǎo)演的鐵粉趟畏,印象中他的很多部電影都進入了IMDB TOP250的榜單修己,但是具體是多少部呢?他是不是IMDB TOP250 中作品最多的導(dǎo)演呢牡肉?哪些演員在這些電影中出鏡最多呢?在這些問題的啟發(fā)下丑罪,我準(zhǔn)備寫一個簡單的爬蟲腳本來獲取我想要的數(shù)據(jù)荚板。

分析

首先需要對工作的流程進行一個簡單的分析。我們的目標(biāo)是獲取以下的數(shù)據(jù):

  • IMDB TOP250 中導(dǎo)演根據(jù)作品數(shù)量的排名
  • IMDB TOP250 中演員根據(jù)作品數(shù)量的排名

要得到以上的數(shù)據(jù)吩屹,我們需要的原始數(shù)據(jù)包括:

  • IMDB TOP250 的電影數(shù)據(jù): 名稱跪另,評分
  • 電影導(dǎo)演
  • 電影演員

頁面HTML分析

讓我們先來看一下數(shù)據(jù)的來源,IMDB TOP250的網(wǎng)頁煤搜。

HTML

可以看到在頁面HTML文件中免绿,我們可以得到的數(shù)據(jù)有電影的評分,電影的名字擦盾,電影的年份嘲驾。但是導(dǎo)演和演員的數(shù)據(jù)呢淌哟?可以發(fā)現(xiàn)在頁面上點擊電影的名字,可以到達電影的詳情頁辽故,而這個link也在HTML文件中徒仓。
我們接著觀察電影的詳情頁。在HTML中我們可以獲取到導(dǎo)演的信息

Director

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

Actor

這樣一來我們需要的數(shù)據(jù)就都有了誊垢。

數(shù)據(jù)庫設(shè)計

要實現(xiàn)這種類型數(shù)據(jù)的排名和統(tǒng)計掉弛,關(guān)系型數(shù)據(jù)庫更加合適。在這里喂走,我的設(shè)計是用5個不同的表來記錄不同的數(shù)據(jù)殃饿。同時我使用的是開源的MySQL數(shù)據(jù)庫。

  1. 創(chuàng)建一個 imdb_movie schema
  2. 創(chuàng)建表 top_250_movies 用于存儲電影的信息:電影名稱 name芋肠, 電影的發(fā)行年份 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. 創(chuàng)建表 actorsdirectors來保存演員和導(dǎo)演的信息帮孔。
    這個表的結(jié)構(gòu)很簡單雷滋,就是演員的id 和演員的 name. 而演員/導(dǎo)演的ID和前面的電影ID的思路類似,通過演員詳情頁鏈接中的ID來設(shè)置文兢。
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. 創(chuàng)建表 cast_in_movie來保存演員出演電影的信息晤斩。
    由于一個演員可以參演多部電影,而一個電影也有很多的演員姆坚,所以這里我會創(chuàng)建一個cast_id來標(biāo)示每一個出演的關(guān)系澳泵,這個表中的每一行數(shù)據(jù)記錄了一個演員參演了一部電影。同時是分別使用actor_idmovie_id為Foreign Key與actorstop_250_movies關(guān)聯(lián)兼呵。
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. 用類似的思路創(chuàng)建表 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
)

腳本實現(xiàn)

在理清了工作流程之后,可以開始實現(xiàn)腳本了击喂。

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

代碼中主要使用BeautifulSoup對HTML文件進行解析和搜索维苔,獲取需要的數(shù)據(jù)。
另外需要注意的是使用正則表達式來獲取電影的ID
id_pattern = re.compile(r'(?<=tt)\d+(?=/?)')
獲取的是tt開頭 /結(jié)尾的字符串懂昂,但是不包含tt/介时,這部分?jǐn)?shù)字就是我們想要的ID。
這個方法是一個生成器,每次的返回是一部電影的數(shù)據(jù)沸柔。

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. 將電影數(shù)據(jù)存入數(shù)據(jù)庫
  1. 首先建立數(shù)據(jù)庫連接
db = pymysql.connect("localhost","testuser01","111111","imdb_movie" )
cursor = db.cursor()
  1. 把電影數(shù)據(jù)存入數(shù)據(jù)庫
    每次存入前需要檢查這條數(shù)據(jù)是否已經(jīng)存在循衰,避免出錯。
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:
            # 發(fā)生錯誤時回滾
            db.rollback()
    else:
        print("This movie ALREADY EXISTED!!!")
3. 獲取電影詳細信息

接著利用上面的得到的movie_data 來獲取電影詳情頁的信息褐澎。包括導(dǎo)演信息和演員信息会钝。

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. 把導(dǎo)演信息存入數(shù)據(jù)庫

這里需要在兩個table中插入數(shù)據(jù)。首先在directors中插入導(dǎo)演的數(shù)據(jù)工三,同樣檢查記錄是否已經(jīng)存在顽素。接著在 direct_movie插入數(shù)據(jù),插入前也檢查是否已經(jīng)存在相同的數(shù)據(jù)徒蟆。

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

    try:
        # 執(zhí)行sql語句
        cursor.execute(sel_sql)
        # 執(zhí)行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:
            # 執(zhí)行sql語句
            cursor.execute(sql)
            # 執(zhí)行sql語句
            db.commit()
            print("Director data ADDED to DB table directors!", movie['director_name'] )
        except:
            # 發(fā)生錯誤時回滾
            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:
        # 執(zhí)行sql語句
        cursor.execute(sel_sql)
        # 執(zhí)行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:
            # 執(zhí)行sql語句
            cursor.execute(sql)
            # 執(zhí)行sql語句
            db.commit()
            print("Director direct movie data ADD to DB table direct_movie!")
        except:
            # 發(fā)生錯誤時回滾
            db.rollback()
    else:
        print("This Director direct movie ALREADY EXISTED!!!")
5. 把演員信息存入數(shù)據(jù)庫

這里需要在兩個table中插入數(shù)據(jù)。首先在actors中插入演員的數(shù)據(jù)型型,同樣檢查記錄是否已經(jīng)存在段审。接著在cast_in_movie插入數(shù)據(jù),插入前也檢查是否已經(jīng)存在相同的數(shù)據(jù)闹蒜。

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

    try:
        # 執(zhí)行sql語句
        cursor.execute(sel_sql)
        # 執(zhí)行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:
            # 執(zhí)行sql語句
            cursor.execute(sql)
            # 執(zhí)行sql語句
            db.commit()
            print("actor data ADDED to DB table actors!")
        except:
            # 發(fā)生錯誤時回滾
            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:
        # 執(zhí)行sql語句
        cursor.execute(sel_sql)
        # 執(zhí)行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:
            # 執(zhí)行sql語句
            cursor.execute(sql)
            # 執(zhí)行sql語句
            db.commit()
            print("actor casted in movie data ADDED to DB table cast_in_movie!")
        except:
            # 發(fā)生錯誤時回滾
            db.rollback()
    else:
        print("This actor casted in movie data ALREADY EXISTED")
6. 完成代碼

這里需要注意的是在操作完成或者出錯的情況下都要關(guān)閉數(shù)據(jù)庫連接寺枉。

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

數(shù)據(jù)庫查詢分析

運行腳本完成數(shù)據(jù)獲取之后,我們通過SQL語句來獲取我們最終想要的數(shù)據(jù)

IMDB TOP250導(dǎo)演排名
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

最終的答案是什么呢绷落?各位同學(xué)可以自己來揭曉姥闪。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市砌烁,隨后出現(xiàn)的幾起案子筐喳,更是在濱河造成了極大的恐慌,老刑警劉巖函喉,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件避归,死亡現(xiàn)場離奇詭異,居然都是意外死亡管呵,警方通過查閱死者的電腦和手機梳毙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捐下,“玉大人账锹,你說我怎么就攤上這事】澜螅” “怎么了奸柬?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長啤握。 經(jīng)常有香客問我鸟缕,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任懂从,我火速辦了婚禮授段,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘番甩。我一直安慰自己侵贵,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布缘薛。 她就那樣靜靜地躺著窍育,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宴胧。 梳的紋絲不亂的頭發(fā)上漱抓,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機與錄音恕齐,去河邊找鬼乞娄。 笑死,一個胖子當(dāng)著我的面吹牛显歧,可吹牛的內(nèi)容都是我干的仪或。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼士骤,長吁一口氣:“原來是場噩夢啊……” “哼范删!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拷肌,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤到旦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后廓块,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厢绝,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年带猴,在試婚紗的時候發(fā)現(xiàn)自己被綠了昔汉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡拴清,死狀恐怖靶病,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情口予,我是刑警寧澤娄周,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站沪停,受9級特大地震影響煤辨,放射性物質(zhì)發(fā)生泄漏裳涛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一众辨、第九天 我趴在偏房一處隱蔽的房頂上張望端三。 院中可真熱鬧,春花似錦鹃彻、人聲如沸郊闯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽团赁。三九已至,卻和暖如春谨履,著一層夾襖步出監(jiān)牢的瞬間欢摄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工笋粟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留剧浸,地道東北人。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓矗钟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親嫌变。 傳聞我的和親對象是個殘疾皇子吨艇,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容