我們曾經抓取過貓眼電影TOP100,並進行了簡單的分析。但是眾所周知,豆瓣的用戶比較小眾、比較獨特,那麼豆瓣的TOP250又會是哪些電影呢?
我在整理代碼的時候突然發現一年多以前的爬蟲代碼竟然還能使用……那今天就用它來演示下,如何通過urllib+BeautifulSoup來快速抓取解析豆瓣電影TOP250。
一、觀察網頁地址
首先我們觀察url地址,連續點擊幾頁之後我們發現,豆瓣電影TOP250一共分10頁,每頁有25部電影,每頁的url地址的格式為https://movie.douban.com/top250?start={0}&filter=,大括號中的部分用這一頁的第一部電影的編號代替,編號從0開始一直到249。
因此我們可以通過格式化字符串來生成所有的url地址:
url_init = ' urls = [url_init.format(x * 25) for x in range(10)]print(urls)輸出為:
['https://movie.douban.com/top250?start=0&filter=', 'https://movie.douban.com/top250?start=25&filter=', 'https://movie.douban.com/top250?start=50&filter=', 'https://movie.douban.com/top250?start=75&filter=', 'https://movie.douban.com/top250?start=100&filter=', 'https://movie.douban.com/top250?start=125&filter=', 'https://movie.douban.com/top250?start=150&filter=', 'https://movie.douban.com/top250?start=175&filter=', 'https://movie.douban.com/top250?start=200&filter=', 'https://movie.douban.com/top250?start=225&filter=']當然,這個地址未必就是我們需要請求的地址。我們先打開TOP250的第一頁,右鍵檢查,單擊進入網絡(Network)選項卡,刷新一下,可以看到出現了一大堆請求的返回結果,我們先打開第一個document類型的請求。
我們切換到Headers標籤,可以看到,在General下,Request URL的取值與網頁地址一樣。也就是說,我們上邊生成的10條url,正是我們需要請求的地址。
二、定位信息位置
接下來我們看一下電影的信息藏在哪裡。我們切換到Response標籤下,搜索網頁中的內容,比如我們先搜一下榜首的名稱:肖申克的救贖。
可以看到,所有的電影信息都在一個class="grid_view"的<ol>(有序列表)標籤下,每一部電影是一個<li>標籤。
在每個<li>下:
標題藏在一個class="title"的<span>標籤下;導演、主演、上映年份、地區、類型藏在一個<div>的子標籤<p>中;得分和評分人數在<div>下
好,接下來我們就開始抓取並解析這些內容。
三、抓取並解析
首先我們定義一個函數,用來打開url並返回BeautifulSoup對象。
# -*- coding:utf-8 -*-from urllib.request import urlopenfrom bs4 import BeautifulSoupfrom collections import defaultdictimport pandas as pdimport timeimport reclass DoubanMovieTop(): def __init__(self): self.top_urls = ['https://movie.douban.com/top250?start={0}&filter='.format(x*25) for x in range(10)] self.data = defaultdict(list) self.columns = ['title', 'link', 'score', 'score_cnt', 'top_no', 'director', 'writers', 'actors', 'types', 'edit_location', 'language', 'dates', 'play_location', 'length', 'rating_per', 'betters', 'had_seen', 'want_see', 'tags', 'short_review', 'review', 'ask', 'discussion'] self.df = None def get_bsobj(self, url): html = urlopen(url).read().decode('utf-8') bsobj = BeautifulSoup(html, 'lxml') return bsobj在這個函數中,我們使用urllib.requests.urlopen來發起請求,對返回的響應結果,我們使用.read()方法來讀取內容。但是這裡讀取到的內容是字節(bytes),我們要將其轉化為字符串,所以我們要再使用字節對象的.decode('utf-8')方法進行轉化;然後我們使用bs4.BeautifulSoup()從字符串中生成BeautifulSoup對象。
這裡我們為什麼選擇utf-8編碼進行解碼呢?這是因為這個網頁的編碼正是utf-8。一般情況下,我們可以從網頁的<head>中找到編碼信息,這樣我們就可以對於不同編碼的網頁進行針對性的解碼了。
接下來我們開始解析電影的信息。我們定義一個函數,以上邊get_bsobj(url)函數輸出的BeautifulSoup對象為輸入,以數據列表為輸出。
def get_info(self): for url in self.top_urls: bsobj = self.get_bsobj(url) main = bsobj.find('ol', {'class': 'grid_view'}) # 標題及連結信息 title_objs = main.findAll('div', {'class': 'hd'}) titles = [i.find('span').text for i in title_objs] links = [i.find('a')['href'] for i in title_objs] # 評分信息 score_objs = main.findAll('div', {'class': 'star'}) scores = [i.find('span', {'class': 'rating_num'}).text for i in score_objs] score_cnts = [i.findAll('span')[-1].text for i in score_objs] for title, link, score, score_cnt in zip(titles, links, scores, score_cnts): self.data[title].extend([title, link, score, score_cnt]) bsobj_more = self.get_bsobj(link) more_data = self.get_more_info(bsobj_more) self.data[title].extend(more_data) print(self.data[title]) print(len(self.data)) time.sleep(1)我們在榜單列表頁面直接獲取了標題、電影詳情頁面地址、評分、評分人數信息。
接下來,可以看到我對所有的電影詳情頁面進行了一個循環抓取解析,這是因為在榜單頁面中信息展示不全,且這裡的信息不夠豐富,在詳情頁中,我們可以獲取非常豐富的數據,包括導演、編劇、演員、上映時間和地區、語言、別名、短評數、影評數、多少人想看、多少人看過……
獲得了這麼多信息之後,我們可以進行更加深入的分析,因此我們選擇進入詳情頁進一步抓取更多信息。因此我們需要定義一個函數,用來解析詳情頁。
豆瓣的頁面抓取難度較小,不過我們這裡定義了較多的欄位,因此這個函數會顯得比較長。這個函數裡我們使用了兩個try...except...來應對異常,這是因為有些電影沒有編劇或者主演,這會導致抓取異常,針對這種情況,我們直接將該欄位留空即可。
每個欄位抓取的表達式都是根據返回的源碼得到的,BeautifulSoup的使用非常簡單,幾分鐘就可以上手,半小時就可以入門。事實上我現在更喜歡使用XPath表達式,更靈活、更強大,對XPath的使用不了解的同學可以去看我的另一篇抓取網易雲音樂的文章。在這個例子中,有些BeautifulSoup不太容易實現的部分,我們結合了re正則表達式來完成。
關於BeautifulSoup的使用,可以參考這份教程:https://docs.pythontab.com/beautifulsoup4/。至於學習程度,仍然是按照我們的二八法則,不需深究,學習最少的內容,覆蓋最多的應用即可,剩下的在實戰中遇到了再去檢索學習即可。
def get_more_info(self, bsobj): # 榜單排名 top_no = bsobj.find('span', {'class': 'top250-no'}).text.split('.')[1] # 更多信息 main = bsobj.find('div', {'id': 'info'}) # 導演 dire_obj = main.findAll('a', {'rel': 'v:directedBy'}) director = [i.text for i in dire_obj] # 編劇 try: writer_obj = main.findAll('span', {'class': 'attrs'})[1] writers = [i.text for i in writer_obj.findAll('a')] except Exception as e: writers = [] print(e) # 主演 try: actor_obj = main.findAll('a', {'rel': 'v:starring'}) actors = [i.text for i in actor_obj] except Exception as e: actors = [] print(e) # 類型 type_obj = main.findAll('span', {'property': 'v:genre'}) types = [i.text for i in type_obj] # 製片地區 pattern = re.compile('地區: (.*?)\n語言', re.S) edit_location = re.findall(pattern, main.text)[0] # 語言 pattern2 = re.compile('語言: (.*?)\n上映日期') language = re.findall(pattern2, main.text)[0] # 上映日期/地區 date_obj = main.findAll('span', {'property': 'v:initialReleaseDate'}) dates = [i.text.split('(')[0][:4] for i in date_obj] play_location = [i.text.split('(')[1][:-1] for i in date_obj] # 片長 length = main.find('span', {'property': 'v:runtime'})['content'] # 5星到1星比例 rating_obj = bsobj.findAll('span', {'class': 'rating_per'}) rating_per = [i.text for i in rating_obj] # 好於 better_obj = bsobj.find('div', {'class': 'rating_betterthan'}) betters = [i.text for i in better_obj.findAll('a')] # 想看/看過 watch_obj = bsobj.find('div', {'class': 'subject-others-interests-ft'}) had_seen = watch_obj.find('a').text[:-3] want_see = watch_obj.findAll('a')[-1].text[:-3] # 標籤 tag_obj = bsobj.find('div', {'class': 'tags-body'}).findAll('a') tags = [i.text for i in tag_obj] # 短評 short_obj = bsobj.find('div', {'id': 'comments-section'}) short_review = short_obj.find('div').find('span', {'class': 'pl'}).find('a').text.split(' ')[1] # 影評 review = bsobj.find('a', {'href': 'reviews'}).text.split(' ')[1] # 問題 ask_obj = bsobj.find('div', {'id': 'askmatrix'}) ask = ask_obj.find('h2').find('a').text.strip()[2:-1] # 討論 discuss_obj = bsobj.find('p', {'class': 'pl', 'align': 'right'}).find('a') discussion = discuss_obj.text.strip().split('(')[1][2:-2] more_data = [top_no, director, writers, actors, types, edit_location, language, dates, play_location, length, rating_per, betters, had_seen, want_see, tags, short_review, review, ask, discussion] return more_data成功抓取之後,我們還需要定義一個函數,用來將數據緩存到本地或其他途徑(比如資料庫),用於後續分析。
def dump_data(self): data = [] for title, value in self.data.items(): data.append(value) self.df = pd.DataFrame(data, columns=self.columns) self.df.to_csv('douban_top250.csv', index=False)好了,一個針對豆瓣電影TOP250的爬蟲就寫完了,接下來我們執行抓取。
if __name__ == '__main__': douban = DoubanMovieTop() douban.get_info() douban.dump_data()抓取完成後,我們就可以看到我們的數據了。
那麼有人可能會問,我們抓取到數據就結束了嗎?
當然沒有,在下一篇文章中,我們會實戰演練如何對我們得到的數據進行數據清洗和分析挖掘。