当前位置: 首页 > 后端技术 > Python

摆脱剧荒:教你用Python爬取豆瓣电影最新榜单

时间:2023-03-26 19:29:59 Python

作者|吹牛Z源|从一个维度出发,细化还原从数据爬取到分析的全链路。全文阅读大约需要5分钟,想直接看结果或者下载源码+数据集的朋友可以跳伞到文末。朋友们,暑假已经过半了。虽然这个遥远而火热的名词与小Z无关,但作为工作犬,在房间里穿着短裤,吹着空调,吃着西瓜,看着电影,依然是假期最好的开启方式。现在内裤、空调、西瓜都唾手可得,压力都在电影这边。关于电影推荐和排名,豆瓣是个好地方,但是电影TOP250排名太经典了,经典到有点老套。小Z想搞点新东西,于是按默认的“最高分”排序,Emmm,结果好像比较少:他还按年龄筛选,结果发现返回的结果远不如预期。我应该怎么办?何不自己对豆瓣电影进行更全面的爬取和分析,然后DIY评分规则,根据电影上映年份做一个各时代TOP100电影的榜单。数据爬取1.探索网站的规律。听说看的人越多,评分越有说服力,于是我们进入导航页面,选择“最受好评”。(虽然更多的标签不完全等于更多的浏览量,但也差不多。)要找到URL的变化规律,常规的套路是先右击“InspectElement”,再点击“LoadMore”不断刷新页面。找到一个模式。URL规则很简单,开头的URL不变,每翻一页,start的值增加20,就OK了。一个页面有20部电影,我们一开始设置的FLAG是爬取9000部电影,也就是爬取450个页面。2.单页解析+循环爬取豆瓣灰很贴心。每个页面都是以JSON格式存储的常规数据。抓取清理省事:这里我们只需要在headers中伪装user-agent就可以愉快的抓取了:headers={'User-Agent':'Mozilla/5.0(WindowsNT6.1;WOW64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/63.0.3239.132Safari/537.36'}页面解析直接命令代码:defparse_base_info(url,headers):html=requests.get(url,headers=headers)bs=json.loads(html.text)df=pd.DataFrame()foriinbs['data']:casts=i['casts']#starringcover=i['cover']#posterdirectors=i['directors']#directorm_id=i['id']#IDrate=i['rate']#评分star=i['star']#标签人数title=i['title']#片名url=i['url']#URLcache=pd.DataFrame({'starring':[casts],'poster':[cover],'director':[directors],'ID':[m_id],'rating':[rate],'mark':[star],'title':[title],'url':[url]})df=pd.concat([df,cache])returndf然后我们写一个循环构造required450basicurls:你要爬多少个页面,其实这个对应加载多少次defformat_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={}'foriinrange(0,20*num,20):url=base_url.format(i)urls.append(url)returnurlsurls=format_url(450)将两者放在一起运行:result=pd.DataFrame()#查看爬取了多少页count=1forurlinurls:df=parse_base_info(url,headers=headers)result=pd.concat([result,df])time.sleep(random.random()+2)print('我抓取了页面:%d'%count)count+=1费了九牛二虎之力,电影ID,电影名字,男主角,导演,评分,打标签人数,具体网址都爬取了:接下来,我们还要批量访问每部电影,获取相关的电影。对于星级占比等更丰富的信息,我们以后想结合评分的分布进行排序。3、爬取单部电影的详情,我们打开单部电影的URL。必填字段不在源代码中。毕竟爬取静态源码是最省力的。电影名?我在这里!导演资料?我在这里!豆瓣评分?还在那儿!CTRL+F一搜,发现我们需要的字段都在源码里了。爬取太简单了,这里用xpath解析:defparse_movie_info(url,headers=headers,ip=''):ifip=='':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=[]fort在bs.xpath('//span[@property="v:genre"]'):m_type.append(t.text)a=bs.xpath('//div[@id="info"]')[0].xpath('string()')#lengthm_time=a[a.find('length:')+4:a.find('minuten')]#timelength#areaarea=a[a.find('Country/regionofproduction:')+9:a.find('nlanguage')]#area#numberofratingstry:people=bs.xpath('//a[@class="rating_people"]/span')[0].text#Rating分布rating={}rate_count=bs.xpath('//div[@class="ratings-on-weight"]/div')forrateinrate_count:rating[rate.xpath('span/@title')[0]]=rate.xpath('span[@class="rating_per"]')[0].textexcept:people='None'rating={}#Introductiontry:brief=bs.xpath('//span[@property="v:summary"]')[0].text.strip('nu3000u3000')除了:brief='None'尝试:hot_comment=bs.xpath('//div[@id="hot-comments"]/div/div/p/span')[0].textexcept:hot_comment='None'cache=pd.DataFrame({'片名':[title],'上映时间':[year],'电影类型':[m_type],'length':[m_time],'area':[area],'评分人数':[people],'分数分布':[rating],'简介':[brief],'热评':[hot_comment],'url':[url]})returncache第二步,我们已经获取了9000部电影的所有url,只需要写一个循环,批量访问即可,虽然设置了访问间隔,爬了几千页之后,我们会发现豆娘还是会把我们ban掉。回想一下,我们没有登录,也不需要cookie验证,只是因为频繁访问骚扰豆娘。那么这个问题还是比较好解决的.如果不留在这里,换个IP就好了,细心的朋友已经发现,单m的页面分析是有默认IP参数的上面的电影。我们只需要在旧IP被封禁后通过该IP即可。只需输入一个新IP。PS:如果代理IP太长扩展不了,网上有很多免费IP代理(缺点是可用时间短,不稳定)和付费IP代理(缺点是不免费)。另外,我要强调一下,我们这里传入的IP是这样的:{'https':'https://115.219.79.103:0000'}movie_result=pd.DataFrame()ip=''#BuildyourownIPpoolcount2here=1cw=1forurl,nameinzip(result['URL'].values[6000:],result['filmname'].values[6000:]):#forname,urlinwrongs.items():try:cache=parse_movie_info(url,headers=headers,ip=ip)movie_result=pd.concat([movie_result,cache])#time.sleep(random.random())print('我们抓取了第一个:%dMovie--------%s'%(count2,name))count2+=1except:print('滴滴滴滴,第{}次报错'.format(cw))print('ipis:{}'.format(ip))cw+=1time.sleep(2)continue电影页面数据爬取结果如下:数据清洗1、基本信息表和电影内容表合并base_info表就是我们爬进去的batches电影基本信息,movie_info是我们进入每部电影时得到的感兴趣字段的汇总。后面的分析需要依赖两张表,所以合并:2.电影年数据清洗我们爬了才发现上映时间的数据不够规整,前面有个“-”:去掉前面多余的符号,但是发现不管怎么用str.replace返回Nan,原来pandas默认所有的数都是负数,所以只要把这一列的所有数都乘以-1:3即可。分数分布是有规律的。最后,我们希望结合电影的总体评分(比如一部8.9分的电影)和不同的评分等级(5星占70%)。的。刚才爬取评分数据的时候,为了偷懒,我用字典把每个评分等级和对应的比例都包起来了。但是pandas默认把它当成字符串,不能直接当成字典:灵光闪现?字典形式的字符串用json解析后不就变成字典了吗?HAVEATRY:结果报了一个疯狂的错误:错误似乎在提醒我们是最外面的引号错误导致了问题。目前我们使用双引号("{'a':1}")。能不能只用单引号('{'a':1}')?先试一下:错误已解决。接下来,我们将字典形式的分数拆分成多列,比如每颗星对应一列,百分比的格式变成数字,写一个循环函数,用apply应用:单-的分数分布columndictionary转换成5个独立的列,每列都是数字defget_rate(x,types):try:returnfloat(x[types].strip('%'))except:passmovie_combine['5stars']=movie_combine['format_score'].apply(get_rate,types='recommended')movie_combine['4stars']=movie_combine['format_score'].apply(get_rate,types='recommended')movie_combine['3stars']=movie_combine['format_rating'].apply(get_rate,types='OK')movie_combine['2stars']=movie_combine['format_rating'].apply(get_rate,types='poor')movie_combine['1star']=movie_combine['format_score'].apply(get_rate,types='verybad')现在我们的数据看起来是这样的:OK,清洗到此结束。数据分析还记得开头的FLAG吗?我们想列出每个时代的TOP100电影。所以只要按照年龄划分电影,然后按照评分对电影进行排序,就大功告成了!不过,这听起来有些粗糙粗暴。如果您仅根据电影的总体评分对电影进行排名,则内部评分细节的差异将被忽略。例如,《搏击俱乐部》:总评分为9.0,其中60.9%的电影为5星,30.5%的电影为4星。同样是9分的优秀作品,56.0%的人给《美丽心灵》打5星,比《搏击俱乐部》少4.9%,比4星多6%。我可以做一个不负责任的概括:都是9分经典,但观众往往给搏击俱乐部5星多于美丽心灵。GET到这里,我们可以制定一个简单的电影评分排序规则:先按照总评分排序,然后比较5星的人数比例,如果相同就比较4星,以此类推在。这个评分排序逻辑用PYTHON做起来并不太简单,一行代码就可以了:根据总分,5星评分的比例,4星,3星的比例..等等movie_combine.sort_values(['rating','5星','4星','3星','2星','1星'],ascending=False,inplace=True)但是仔细看排序结果,我们会发现这个排序有些小瑕疵,一些高分片其实比较小众,比如《歌剧魅影:25周年纪念秀》和《悲惨世界:25周年演唱会》。而我们要找的是老百姓喜闻乐见的电影排行。在这里,只有评分的人数才能代表人数。我们来看看所有电影的评分人数分布:评分人数非常多。为了减少极值对平均值的影响,让中位数衡量人们是否喜欢它,所以我们只留下大于中位数的分数。接下来看历年电影数量分布:直到2000年初,上映后每年上映的电影数量才接近200部,而更早时期的电影数量似乎加起来20年内减少到100个以下。为了让结果更直观,我们按年份统计电影的上映时间。这涉及到对每部电影的上映时间进行分类,有点tricky。。。绞尽脑汁终于找到了一个比较讨人喜欢的方式,先构造年龄标签,然后借用cut函数在十点分割上映-yearintervals时间,最后将标签传入参数。删除!数据直观的反应了每个时代的发行量,而在1980年代之前,真是少得可怜。看到这里,我不禁想起当初我们立下的“制作年份TOP100榜单”的FLAG。因为没有早期的电影,完全站不住脚。别慌,一个优秀的数据分析师一定要本着具体问题具体分析的精神来调整FLAG:根据时代的发布数据,我们从1930年代开始做排名;为了避免某些时代电影太少,优化成了每个时代电影推荐的TOP10%;同时,为了避免近几年的电影太多,每个时代的推荐上限不超过100部。看到这三个条件,连一向骄傲的潘师傅都忍不住了不禁感叹良久。不过,鹅大师之所以是大师,是因为在他的眼里,没有什么是不可能的。想了1分钟,决定采用灵活筛选的套路:final_rank=pd.DataFrame()forcentury,countinzip(century_f.index,century_f.values):f1=movie_f2.loc[movie_f['Year']==century,:]#1000以下取TOP10%ifcount<1000:return_num=int(count*0.1)#1000以上取前100else:return_num=100f2=f1.iloc[:return_num,:]final_rank=pd.concat([final_rank,f2])根据上一步构造的century_f变量,结合每个时代上映的电影数量,放映前10%不到1000,且1000多只筛选前100名。结果,直接叫出来就好了。附上代码和榜单之前,我预感大部分朋友和我一样懒惰(不会仔细看榜单),所以我先整理了各个时代的TOP5电影(有的低于TOP5),并做了一波三折结局让人呐喊的精版《控方证人》,到辩白无罪的真相的《十二怒汉》,到家人不怒自威的《教父》系列,以及重新《肖申克的救赎》诠释希望与坚韧,《阿甘正传》将灵感提升到新的高度(小Z只看过这几部电影,我也只在榜单上看过)。每一部好电影都像一颗从高空落下的石头,总能在人们的心中激起阵阵涟漪,激起人们对人生、对社会、对人性的思考。糟糕的电影是一个从高空坠落的空矿泉水瓶。它猛烈下落,但最终只会浮在水面上,让看过的人怨恨不已,觉得自己的灵魂都被污染了。有了新的电影排行榜,你再也不用担心没有戏了。