起因
恰逢諾蘭導(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文件中免绿,我們可以得到的數(shù)據(jù)有電影的評分,電影的名字擦盾,電影的年份嘲驾。但是導(dǎo)演和演員的數(shù)據(jù)呢淌哟?可以發(fā)現(xiàn)在頁面上點擊電影的名字,可以到達電影的詳情頁辽故,而這個link也在HTML文件中徒仓。
我們接著觀察電影的詳情頁。在HTML中我們可以獲取到導(dǎo)演的信息
同時在Cast 的表中還可以獲取到主要演員的信息
這樣一來我們需要的數(shù)據(jù)就都有了誊垢。
數(shù)據(jù)庫設(shè)計
要實現(xiàn)這種類型數(shù)據(jù)的排名和統(tǒng)計掉弛,關(guān)系型數(shù)據(jù)庫更加合適。在這里喂走,我的設(shè)計是用5個不同的表來記錄不同的數(shù)據(jù)殃饿。同時我使用的是開源的MySQL數(shù)據(jù)庫。
- 創(chuàng)建一個
imdb_movie
schema - 創(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`)
)
- 創(chuàng)建表
actors
和directors
來保存演員和導(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`)
)
- 創(chuàng)建表
cast_in_movie
來保存演員出演電影的信息晤斩。
由于一個演員可以參演多部電影,而一個電影也有很多的演員姆坚,所以這里我會創(chuàng)建一個cast_id
來標(biāo)示每一個出演的關(guān)系澳泵,這個表中的每一行數(shù)據(jù)記錄了一個演員參演了一部電影。同時是分別使用actor_id
和movie_id
為Foreign Key與actors
和top_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
)
- 用類似的思路創(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ù)庫
- 首先建立數(shù)據(jù)庫連接
db = pymysql.connect("localhost","testuser01","111111","imdb_movie" )
cursor = db.cursor()
- 把電影數(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é)可以自己來揭曉姥闪。