用爬蟲分析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數據庫。
- 創建一個
imdb_movie
schema - 創建表
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`) )
- 創建表
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`) )
- 創建表
cast_in_movie
來保存演員出演電影的信息。
由於一個演員可以參演多部電影,而一個電影也有很多的演員,所以這裏我會創建一個cast_id
來標示每一個出演的關系,這個表中的每一行數據記錄了一個演員參演了一部電影。同時是分別使用actor_id
和movie_id
為Foreign Key與actors
和top_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 )
- 用類似的思路創建表
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. 將電影數據存入數據庫
- 首先建立數據庫連接
db =pymysql.connect("localhost","testuser01","111111","imdb_movie" ) cursor = db.cursor()
- 把電影數據存入數據庫
每次存入前需要檢查這條數據是否已經存在,避免出錯。
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
最終的答案是什麽呢?各位同學可以自己來揭曉。
用爬蟲分析IMDB TOP250電影數據