擺脫劇荒:教你用Python爬取豆瓣電影最新榜單

做者 | 吹牛Z
來源 | 數據不吹牛

【導讀】本文以豆瓣電影(非TOP250)爲例,從數據爬取、清洗與分析三個維度入手,詳解和還原數據爬取到分析的全鏈路。閱讀全文大概須要5分鐘,想直接看結果或下載源碼+數據集的旁友能夠空降到文末。html

旁友,暑假,已通過了一大半了。json

這個遙遠而炙熱的名詞,雖然和小Z這個上班狗已經沒有任何關係,但在房間穿着褲衩,吹着空調,吃着西瓜,看着電影,依然是假期最好的打開方式。如今褲衩、空調、西瓜都唾手可得,壓力全在電影這邊了。cookie

關於電影推薦和排行,豆瓣是個好地方,只是電影TOP250排名實在是太經典,經典到有點老套了。app

小Z想來點新花樣,因而按默認的「評分最高」來排序,Emmm,結果好像比較小衆:dom

image

又按年代進行篩選,發現返回的結果和預期差的更遠了。函數

怎麼辦捏?不如咱們本身對豆瓣電影進行更全面的爬取和分析,再DIY評分規則,結合電影上映年代作一個各年代TOP100電影排行榜。優化

數據爬取

一、網址規律探究url

據說看的人越多,評分越有說服力,因此咱們進入導航頁,選擇「標記最多」。(雖然標記的多並不徹底等於看的多,但也差很少了)spa

要找到網址變化規律,常規的套路就是先右鍵「審查元素」,而後經過不斷的點擊「加載更多」刷新頁面的方式來找規律。3d

網址規律異常的簡單,開頭URL不變,每翻一頁,start的數值增長20就OK了。

一頁是20部電影,開頭咱們立下的FLAG是要爬取9000部電影,也就是爬取450頁。

二、單頁解析+循環爬取

豆瓣灰常貼心,每一頁都是JSON格式存儲的規整數據,爬取和清洗都省了很多事兒:

這裏咱們只須要假裝一下headers裏面的user-agent就能夠愉快的爬取了:

headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'}

直接上單頁解析的代碼:

def parse_base_info(url,headers): html = requests.get(url,headers = headers) bs = json.loads(html.text) df = pd.DataFrame()for i in bs['data']: casts = i['casts'] #主演 cover = i['cover'] #海報 directors = i['directors'] #導演 m_id = i['id'] #ID rate = i['rate'] #評分 star = i['star'] #標記人數 title = i['title'] #片名 url = i['url'] #網址 cache = pd.DataFrame({'主演':[casts],'海報':[cover],'導演':[directors],'ID':[m_id],'評分':[rate],'標記':[star],'片名':[title],'網址':[url]}) df = pd.concat([df,cache])return df

而後咱們寫一個循環,構造所需的450個基礎網址:

你想爬取多少頁,其實這裏對應着加載多少次def format_url(num): urls = [] base_url = 'https://movie.douban.com/j/new_search_subjects?sort=T&range=0,10&tags=%E7%94%B5%E5%BD%B1&start={}'for i in range(0,20 * num,20): url = base_url.format(i) urls.append(url)return urlsurls = format_url(450)

兩個湊一塊兒,跑起來:

result = pd.DataFrame()#看爬取了多少頁count = 1for url in urls:df = parse_base_info(url,headers = headers)result = pd.concat([result,df])time.sleep(random.random() + 2)print('I had crawled page of:%d' % count)count += 1

一個大號的功夫,包含電影ID、電影名稱、主演、導演、評分、標記人數和具體網址的數據已經爬好了:

image

下面,咱們還想要批量訪問每一部電影,拿到有關電影各星級評分佔比等更豐富的信息,後續咱們想結合評分分佈來進行排序。

三、單部電影詳情爬取

咱們打開單部電影的網址,取巧作法是直接右鍵,查看源代碼,看看咱們想要的字段在不在源代碼中,畢竟,爬靜態的源代碼是最省力的。

電影名稱?在的!導演信息?在的!豆瓣評分?仍是在的!一通CTRL+F搜索發現,咱們全部須要的字段,所有在源代碼中。那爬取起來就太簡單了,這裏咱們用xpath來解析:

defparse_movie_info(url,headers = headers,ip = ''):if ip == '': html = requests.get(url,headers = headers)else: html = requests.get(url,headers = headers,proxies = ip) bs = etree.HTML(html.text)#片名 title = bs.xpath('//div[@id = "wrapper"]/div/h1/span')[0].text #上映時間 year = bs.xpath('//div[@id = "wrapper"]/div/h1/span')[1].text #電影類型 m_type = []for t in bs.xpath('//span[@property = "v:genre"]'): m_type.append(t.text) a = bs.xpath('//div[@id= "info"]')[0].xpath('string()')#片長 m_time =a[a.find('片長: ') + 4:a.find('分鐘n')] #時長#地區 area = a[a.find('製片國家/地區:') + 9:a.find('n 語言')] #地區#評分人數try: people = bs.xpath('//a[@class = "rating_people"]/span')[0].text#評分分佈 rating = {} rate_count = bs.xpath('//div[@class = "ratings-on-weight"]/div')for rate in rate_count: rating[rate.xpath('span/@title')[0]] = rate.xpath('span[@class = "rating_per"]')[0].textexcept: people = 'None' rating = {}#簡介try: brief = bs.xpath('//span[@property = "v:summary"]')[0].text.strip('n u3000u3000')except: brief = 'None'try: hot_comment = bs.xpath('//div[@id = "hot-comments"]/div/div/p/span')[0].textexcept: hot_comment = 'None' cache = pd.DataFrame({'片名':[title],'上映時間':[year],'電影類型':[m_type],'片長':[m_time],'地區':[area],'評分人數':[people],'評分分佈':[rating],'簡介':[brief],'熱評':[hot_comment],'網址':[url]})return cache

第二步咱們已經拿到了9000部電影全部的網址,只需寫個循環,批量訪問就能夠了。然鵝,儘管設置了訪問時間間隔,爬取上千個頁面咱們就會發現,豆娘仍是會把咱們給BAN(禁)掉。

回憶一下,咱們沒有登陸,不須要cookies驗證,只是由於頻繁的訪問騷擾到了豆娘。那這個問題仍是比較好解決的,此處不留爺,換個IP就留爺。細心的朋友已經發現了,上面針對單部電影的頁面解析,有一個默認IP參數,咱們只須要在舊IP被禁後,傳入新的IP就能夠了。

PS:代理IP若是展開講篇幅太長,網上有許多免費的IP代理(缺點是可用時間短,不穩定)和付費的IP代理(缺點是難免費)。另外,要強調一下這裏咱們傳入的IP長這樣:{'https':'https://115.219.79.103:0000'}

movie_result = pd.DataFrame()ip = ''#這裏構建本身的IP池count2 = 1cw = 1for url,name in zip(result['網址'].values[6000:],result['片名'].values[6000:]):#for name,url in wrongs.items():try: cache = parse_movie_info(url,headers = headers,ip = ip) movie_result = pd.concat([movie_result,cache])#time.sleep(random.random()) print('咱們爬取了第:%d部電影-------%s' % (count2,name)) count2 += 1except: print('滴滴滴滴滴,第{}次報錯'.format(cw)) print('ip is:{}'.format(ip)) cw += 1 time.sleep(2)continue

電影頁面數據爬取結果以下:


數據清洗

一、基本信息表和電影內容表合併

base_info表裏面是咱們批量抓取的電影基本信息,movie_info則是咱們進入每一部電影,獲取到的感興趣字段彙總,後面的分析是須要依賴兩張表進行的,因此咱們合併之:

二、電影年份數據清洗

咱們發現以前爬取的上映時間數據不夠規整,前面都帶了一個「-」:

要把前面多餘的符號去掉,但發現不管怎麼用str.replace返回的都是Nan,原來這裏pandas把全部數字默認成負的,因此只須要把這一列全部數字乘-1便可:

三、評分分佈規整

最終咱們是但願可以把電影總體評分(如某電影8.9分)和不一樣評分等級(5星的佔比70%)結合起來分析的。而剛纔爬取評分數據的時候,爲了偷懶,用的是一個字典把各評分等級和對應的佔比給包起來了,然鵝,pandas默認把他當成了字符串,不能直接當作字典處理:

靈光一閃?這種字典形式的字符串,用JSON解析一下不就變字典了?HAVE A TRY:

結果,瘋狂報錯:

報錯貌似在提示咱們是最外圍的引號錯誤致使了問題,目前咱們用的是雙引號("{'a':1}")難道只能用單引號('{'a':1}')?先試試吧:

報錯解決了。接下來,咱們把字典形式的評分拆成多列,例如每一個星級對應一列,且百分比的格式變成數值型的,寫個循環函數,用apply應用一下便可:

把單列字典的評分分佈轉化成分開的5列,且每一列是數值型的def get_rate(x,types):try:return float(x[types].strip('%'))except:passmovie_combine['5星'] = movie_combine['format_評分'].apply(get_rate,types = '力薦')movie_combine['4星'] = movie_combine['format_評分'].apply(get_rate,types = '推薦')movie_combine['3星'] = movie_combine['format_評分'].apply(get_rate,types = '還行')movie_combine['2星'] = movie_combine['format_評分'].apply(get_rate,types = '較差')movie_combine['1星'] = movie_combine['format_評分'].apply(get_rate,types = '不好')

如今咱們的數據長這樣的:

OK,清洗到此告一段落。

數據分析

你們還記得開頭的FLAG嗎?咱們要製做各年代TOP100電影排行榜。因此直接按照年代劃分電影,而後按照電影評分排個序不就完事了!

然鵝這聽起來有點話糙理也糙。若是隻按照電影的總的評分來排序,會忽視掉內部評分細節的差別性,舉個例子,搏擊俱樂部:

總評分9.0分,打出5星好評的佔比60.9%,4星的有30.5%。

同爲9分佳做,給美麗心靈打出5星好評的有56.0%,和搏擊俱樂部相比少了4.9%,而4星的人數則高出了6%。能夠不負責任的作一個歸納:兩部都是9分經典,但觀衆給搏擊俱樂部的5星傾向要高於美麗心靈。

GET到這個點,咱們就能夠對電影評分排序制定一個簡單的規則:先按照總評分排序,而後再對比5星人數佔比,若是同樣就對比4星,以此類推。這個評分排序邏輯用PYTHON作起來不要太簡單,一行代碼就搞定:

按照總評分,5星評分人數佔比,4星佔比,3星..依次類推movie_combine.sort_values(['評分','5星','4星','3星','2星','1星'],ascending = False,inplace = True)

可是仔細看排序結果,咱們會發現這樣排序的一些小瑕疵,一些高分電影實際上是比較小衆的,好比「劇院魅影:25週年記念演出」和「悲慘世界:25週年記念演唱會」等。

而咱們想要找的,是人民羣衆所喜聞樂見的電影排名,這裏只有經過評分人數來表明人民的數量,咱們先看一看全部電影的評分人數分佈:

評分人數跨度極大,爲了減小極值對於平均的影響,就讓中位數來衡量人民羣衆是否喜聞樂見,因此咱們只留下大於中位數的評分。

接着,看看歷年電影數量分佈狀況:

直到2000年初,篩選後的電影年上映數才逼近200,更早時期的電影好像20年加起來還不到100部。爲了讓結果更加直觀,咱們來按年代統計電影的上映時間。這裏涉及到給每部電影上映時間進行歸類,有點棘手啊...

絞盡腦細胞,終於找到了一個比較討巧的辦法,先構造年代標籤,再借用cut函數按十年的間隔切分上映時間,最後把標籤傳入參數。

得勒!數據直觀的反映出各年代上映量,20世紀80年代前真的是少得可憐。看到這裏,不禁想到咱們最開始立的那個「製做年代TOP100榜單」的FLAG,由於早期電影量的貧乏,是徹底站不住腳的了。

不慌,一個優秀的數據分析師,必定是本着具體問題具體分析的精神來調整FLAG的:

基於年代上映量數據,咱們從20世紀30年代開始製做排名;爲了不有些年代電影過少,優化成各年代TOP 10%的電影推薦;同時,爲了不近年電影過多,每一個年代推薦的上限數不超過100部。

看到這三個條件,連一貫自傲的潘大師(pandas)都不由長嘆了口氣。然鵝大師之因此是大師,就是由於在他眼裏沒有什麼是不可能的。思考1分鐘後,肯定了靈活篩選的套路:

final_rank = pd.DataFrame()for century,count in zip(century_f.index,century_f.values): f1 = movie_f2.loc[movie_f['年代'] == century,:] #1000部如下的,取TOP10% if count < 1000: return_num = int(count * 0.1) #1000部以上的,取前100部 else: return_num = 100 f2 = f1.iloc[:return_num,:] final_rank = pd.concat([final_rank,f2])

根據上一步構造的century_f變量,結合每一個年代上映電影量,不足1000部的篩選前10%,超過1000部的只篩選前100部,結果,就呼之而出了。

在附上代碼和榜單以前,我預感到大部分旁友是和我同樣懶的(不會仔細看榜單),因此先整理出各年代TOP5電影(有些年代不足TOP5),作一個精華版的歷史電影排行榜奉上:

從峯迴路轉、結尾讓人大呼牛逼的《控方證人》,到爲無罪真理而辯的《十二怒漢》,再到家庭爲重不怒自威的《教父》系列、從新詮釋但願和堅韌的《肖申克的救贖》以及將勵志提高到新高度的《阿甘正傳》(小Z閱片尚淺,榜單上只看過這些)。

每一部好的電影,都是一塊從高空墜落的石頭,它總能在人們的心湖上激起水花和漣漪,引發人們對生活、社會以及人性的思考。而爛片,就是從高空墜落的空礦泉水瓶,它墜勢洶洶,但最終只會浮在水面,讓看過的人心存芥蒂,感受靈魂受到污染。

有了新的電影排名榜單,不再用擔憂劇荒了。

相關文章
相關標籤/搜索