1. 背景介绍
哔哩哔哩(www.bilibili.com ,英文名称:bilibili,简称B站)现为中国年轻世代高度聚集的文化社区和视频平台,该网站于2009年6月26日创建。
B站早期是一个ACG(动画、漫画、游戏)内容创作与分享的视频网站。经过十年多的发展,围绕用户、创作者和内容,构建了一个源源不断产生优质内容的生态系统,B站已经涵盖7000多个兴趣圈层的多元文化社区。
哔哩哔哩作为目前国内最大的动画作品平台,已上线了3000多部来自日本、美国以及国内的动画作品,具有大量的播放、点赞、弹幕、评分等数据可供分析。
bangumi(bangumi番组计划,bangumi.tv )是专注于ACG领域的网站,是国内专业的动画评分网站。该网站可看作动画作品的数据库,拥有万余部动画作品的详细数据,包括集数、播放时间、监督以及评分、评分人数等信息等可供分析。
2. 需求目标
编写一键爬虫脚本获取两个网站的动画作品数据
对两网站的数据进行分析,其中对于评分进行相关性分析
可视化展示数据
3. 基于urllib的bangumi和bilibili一键爬虫脚本的编写
3.1 bangumi网站分析及爬虫脚本的编写
3.1.1 网站分析
首先打开bangumi首页,并登录。登录后刷新页面,并用fiddler抓包,获取请求头:
打开一个需要爬取的动画作品页面,需要爬取的信息有5部分:
作品原名与类型
作品详细信息
作品简介
作品tags
作品评分数据
检查源代码,找到各部分对应的标签区块:
part1
part2
part3
part4
part5
获得对应的源代码位置后,便可以用beautifulsoup包对网页html进行解析获取数据了。
目前的问题是如何获取尽量多的作品数据。
根据网页地址,访问某部作品的页面应为bangumi.tv/subject/…(后面的数字称为subject号 ),所以可以从1开始遍历所有的subject号,这理论上可行,但实际操作中发现了两个问题,一是subject号目前超过20万,全部遍历所需时间太长;二是并不是所有作品都是动画作品,还可能是书籍、音乐、游戏等:
例:漫画
例:专辑
所以必须找到其他方法。注意到bangumi作为评分网站具有排行榜 功能:
该排行榜收录了所有评分人数达到最低评分人数的动画,默认按照评分从高到低排序。截至2020年6月26日,共有5831部动画在榜。并且榜单分为243页,全部可以直接访问爬取subject号:
不需通过ajax请求获取某段排行的数据,这对于爬虫是非常友好的。
考虑到能上榜的作品都具有一定人气,并且只有评分人数达到一定数量评分才更有代表性,所以决定按照排行榜爬取这5800多部动画作品subject号,再访问各自的页面获取详细信息。
3.1.2 代码实现
模块的导入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import numpy as npimport pandas as pdfrom bs4 import BeautifulSoup as bsimport urllib.request as urimport urllib.parse as upimport urllib.error as ueimport http.cookiejar as hcimport reimport gzipimport jsonimport timeimport osimport socketos.chdir('...' ) socket.setdefaulttimeout(30 )
总榜subject号爬取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 headers={ 'Host' : 'bangumi.tv' , 'Connection' : 'keep-alive' , 'Cache-Control' : 'max-age=0' , 'Upgrade-Insecure-Requests' : '1' , 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36' , 'Sec-Fetch-Dest' : 'document' , 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' , 'Sec-Fetch-Site' : 'none' , 'Sec-Fetch-Mode' : 'navigate' , 'Sec-Fetch-User' : '?1' , 'Accept-Encoding' : 'gzip' , 'Accept-Language' : 'zh-CN,zh;q=0.9' , 'Cookie' : '...(此处略去)...__utmb=1.7.10.1593136256' } cj=hc.CookieJar() hl=ur.HTTPCookieProcessor(cj) opener=ur.build_opener(hl) pattern=re.compile (r'li id="item_(\d+)"' ) subjectlist=[] def update_cookie (cookie=str ( ) ): return cookie[:cookie.rfind('utmb=' )]+cookie[cookie.rfind('utmb=' ):].split('.' )[0 ]+'.' +str (int (cookie[cookie.rfind('utmb=' ):].split('.' )[1 ])+1 )+'.' +cookie[cookie.rfind('utmb=' ):].split('.' )[2 ]+'.' +cookie[cookie.rfind('utmb=' ):].split('.' )[3 ] def bangumidownload (start,end,subjectlist ): for i in range (start,end+1 ): url='https://bangumi.tv/anime/browser?sort=rank&page=%d' %(i) print ('downloading page' ,i,end='\r' ) try_time=0 while try_time<=5 : try : r=ur.Request(url=url,headers=headers) response=opener.open (r) break except Exception as e: try_time+=1 if try_time>1 : headers['Cookie' ]=update_cookie(headers['Cookie' ]) print ('retrying with cookie=' ,headers['Cookie' ][headers['Cookie' ].rfind('utmb=' ):],try_time) else : raise Exception('Download Failed!!' ) content=str (gzip.decompress(response.read()),'utf-8' ) response.close() thispage=re.findall(pattern,content) subjectlist.extend(thispage) bangumidownload(1 ,243 ,subjectlist) date=str (time.localtime().tm_mon)+'_' +str (time.localtime().tm_mday) with open (r'bangumi\subjectlist_' +date+'.json' ,'w' ) as fp: json.dump(subjectlist,fp)
详细信息爬取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 bgmdb=[] picpattern=re.compile (r'href="//(.*?)"' ) def bangumifulldownload (bgmsubjects ): for i in bgmsubjects: url='https://bangumi.tv/subject/' +i print ('downloading subject ' +i) try_time=0 while try_time<=5 : try : r=ur.Request(url=url,headers=headers) response=opener.open (r,timeout=30 ) break except Exception as e: try_time+=1 if try_time>2 : headers['Cookie' ]=update_cookie(headers['Cookie' ]) print ('retrying with cookie=' ,headers['Cookie' ][headers['Cookie' ].rfind('utmb=' ):],try_time) else : raise Exception('Download Failed!!' ) content=str (gzip.decompress(response.read()),'utf-8' ) response.close() soup=bs(content) mainWrapper=soup.find('div' ,class_='mainWrapper' ) name=soup.find('h1' ,class_='nameSingle' ) if mainWrapper==None or name==None : continue infobox=mainWrapper.find('ul' ,id ='infobox' ) if infobox==None : continue infodict=dict () infodict.update({'subject' :i,'原名' :name.find('a' ).text if name.find('a' )!=None else '' , '类型' :name.find('small' ).text if name.find('small' )!=None else '' }) summary=mainWrapper.find('div' ,id ='subject_summary' ) if summary is not None : infodict.update({'简介' :summary.text}) pic=mainWrapper.find('a' ,class_='thickbox cover' ) if pic is not None : pic=re.findall(picpattern,str (pic)) if len (pic): infodict.update({'封面' :'https://' +pic[0 ]}) info=infobox.find_all('li' ) for each_info in info: kv=each_info.text.split(':' ,maxsplit=1 ) infodict.update({kv[0 ].strip():kv[1 ].strip()}) tagWrapper=mainWrapper.find('div' ,class_='inner' ) if tagWrapper==None : continue tagtext=tagWrapper.select('.l span,a small' ) tags=[] for everytag in tagtext: tags.append(everytag.text) tags=' ' .join(tags) infodict.update({'tags' :tags}) chartWrapper=mainWrapper.find('div' ,id ='ChartWarpper' ) infodict.update({'votes' :chartWrapper.find('span' ,property ='v:votes' ).text}) rating_list=[] for each_rater in chartWrapper.find_all('span' ,{'class' :'count' }): rating_list.append(each_rater.text[1 :-1 ]) infodict.update({'ratings' :rating_list}) overall_score=0 overall_vote=0 for score in range (10 ,0 ,-1 ): overall_vote+=int (infodict['ratings' ][10 -score]) overall_score+=score*int (infodict['ratings' ][10 -score]) overall_score=overall_score/overall_vote infodict.update({'rating' :str ('%.3f' %(overall_score))}) print (infodict) bgmdb.append(infodict) bangumifulldownload(subjectlist) date=str (time.localtime().tm_mon)+'_' +str (time.localtime().tm_mday) with open (r'bangumi\bgmdb_' +date+'.json' ,'w' ) as fp: json.dump(bgmdb,fp)
数据初步清洗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bgmfulldb=pd.read_json(r'bangumi\bgmdb_' +date+'.json' ) indexs=bgmfulldb[~bgmfulldb.isna()].count().sort_values(ascending=False )[:60 ].index bgmdb2=[] for i in bgmdb: thisanime=[] for each_key in indexs: if each_key in i.keys(): thisanime.append(i[each_key]) else : thisanime.append('' ) bgmdb2.append(thisanime) bgmdb2=pd.DataFrame(bgmdb2,columns=indexs) bgmdb2.to_csv(r'bangumi\bgmdb_' +date+'.csv' ,index=False )
初步清洗后的数据格式如下:
votes
原名
类型
封面
tags
rating
ratings
话数
简介
中文名
...
主题歌作曲
主题歌作词
开始
结束
片长
主题歌编曲
第二原画
音响
特效
机械设定
subject
253
7016
カウボーイビバップ
TV
https://lain.bgm.tv/pic/cover/l/c2/4c/253_t3XW...
渡边信一郎 2293 菅野洋子 2196 星际牛仔 1529 经典 1139 SUNRISE...
9.143
['3325', '2252', '977', '287', '88', '37', '9'...
26
2021年,随着超光速航行技术的实现,人类得以在太阳系范围内方便的自由移动,但...
星际牛仔
...
菅野よう子
NaN
NaN
NaN
NaN
菅野よう子
NaN
NaN
長谷川敏生
山根公利
326
3968
攻殻機動隊 S.A.C. 2nd GIG
TV
https://lain.bgm.tv/pic/cover/l/a6/66/326_M9f1...
菅野洋子 957 攻殻機動隊 871 神山健治 762 攻壳机动队 668 科幻 619 押...
9.129
['1773', '1359', '623', '129', '44', '12', '3'...
26
这个世界距离我们并不遥远,你把它看作是现代社会的镜子亦为不可。\r\n也许:无论人类怎样发展...
攻壳机动队 S.A.C. 2nd GIG
...
菅野よう子
Origa
NaN
NaN
NaN
菅野よう子
NaN
NaN
村上正博
常木志伸、寺岡賢司
324
4896
攻殻機動隊 STAND ALONE COMPLEX
TV
https://lain.bgm.tv/pic/cover/l/f2/fc/324_psuX...
攻壳机动队 1606 菅野洋子 1215 科幻 926 神山健治 852 士郎正宗 775 ...
9.081
['2036', '1780', '790', '172', '61', '26', '5'...
26
公元2030的世界,改造人、生化人、机器人等等的存在已经非常普及。主人公草薙素子正是人类最高...
攻壳机动队 STAND ALONE COMPLEX
...
菅野よう子
Origa
NaN
NaN
NaN
菅野よう子
NaN
NaN
遠藤誠、村上正博
寺岡賢司、常木志伸
3.2 bilibili网站分析及爬虫脚本的编写
3.2.1 网站分析
bilibili的动画作品分处于番剧(国外作品)区与国创(国内作品)区,故主要对这两个区进行分析。
进入番剧区后,点击“番剧索引”,可以发现与bangumi类似的页面,在这个页面同样可以获取到所有上线的国外动画作品(国创区同理):
每个作品对应一个链接: https://www.bilibili.com/bangumi/play/ss...(ss后面的数字称为ss号 )
打开其中一个作品,进入播放页面,在这个页面上可以看到播放量、弹幕数、追番人数、作品类型、完结情况、集数、简介、评分与评分人数等信息:
对该网页进行抓包,尝试获取以上信息:
可以发现,响应中有一些信息,但是缺少播放数、弹幕数等信息,说明网页不是一次性加载出来的。在抓包界面可以看到很多data.bilibili.com的请求,估计是获取更多的页面数据,比如视频源信息等。
在所有抓包结果中搜索弹幕数1126,找到了对应的api接口:api.bilibili.com/pgc/web/season/stat?season_id=…,该接口返回一个json格式字符串,存有精确的播放量、弹幕数等信息(但没有评分信息):
请求中的season_id即为前述的ss号。
继续搜索评分9.2,发现另一个api:api.bilibili.com /pgc/review/user?media_id=…&ts=…
请求链接中的media_id下称为md号 。
该api提供了详尽的作品信息,包括地区、封面链接、评分、标题、类型,还包含一个ss链接。
由于在初始网页https://www.bilibili.com/bangumi/play/ss...中可以找到md号,故设计以下爬虫流程:
爬取ss号–访问ss页面获取作品简介和md号–根据ss号和md号访问相应的api获取详细信息。
最后,访问该json中的share_url,打开的页面为该作品的介绍界面,包含作品开播日期、完结情况、tags等:
找到对应的位置:
故可以访问该页面获取tags、日期、话数。
对于bilibili网站,总的爬虫流程设计如下:
从索引页面爬取ss号—访问ss页面获取作品简介和md号—根据ss号和md号访问相应的api获取详细信息—访问md页面获取作品开播日期和话数—爬取封面图。
3.2.2 代码实现
模块的导入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import numpy as npimport pandas as pdfrom bs4 import BeautifulSoup as bsimport urllib.request as urimport urllib.parse as upimport urllib.error as ueimport http.cookiejar as hcimport reimport gzipimport jsonimport timeimport osimport socketos.chdir(r'...' ) socket.setdefaulttimeout(30 )
配置爬虫条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 apiheaders={ 'Host' : 'api.bilibili.com' , 'Connection' : 'keep-alive' , 'Cache-Control' : 'max-age=0' , 'Upgrade-Insecure-Requests' : '1' , 'Accept' :' application/json, text/plain, */*' , 'Sec-Fetch-Dest' : 'empty' , 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36' , 'Origin' : 'https://www.bilibili.com' , 'Sec-Fetch-Site' : 'same-site' , 'Sec-Fetch-User' : '?1' , 'Sec-Fetch-Mode' : 'cors' , 'Sec-Fetch-Dest' : 'document' , 'Referer' : 'https://www.bilibili.com/anime/index/' , 'Accept-Encoding' : 'gzip, deflate, br' , 'Accept-Language' : 'zh-CN,zh;q=0.9' , 'Cookie' :cookie } wwwheaders={ 'Host' : 'www.bilibili.com' , 'Connection' : 'keep-alive' , 'Cache-Control' : 'max-age=0' , 'Upgrade-Insecure-Requests' : '1' , 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36' , 'Sec-Fetch-Dest' : 'document' , 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9' , 'Sec-Fetch-Site' : 'same-origin' , 'Sec-Fetch-Mode' : 'navigate' , 'Sec-Fetch-User' : '?1' , 'Referer' : 'https://www.bilibili.com/anime/index/' , 'Accept-Encoding' : 'gzip, deflate, br' , 'Accept-Language' : 'zh-CN,zh;q=0.9' , 'Cookie' :cookie } imageheaders={ 'Host' : 'i0.hdslb.com' , 'Connection' : 'keep-alive' , 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36' , 'Sec-Fetch-Dest' : 'image' , 'Accept' : 'image/webp,image/apng,image/*,*/*;q=0.8' , 'Sec-Fetch-Site' : 'cross-site' , 'Sec-Fetch-Mode' : 'no-cors' , 'Referer' : 'https://www.bilibili.com/bangumi/media/md1178/?from=search&seid=17806546061422186816' , 'Accept-Encoding' : 'gzip, deflate, br' , 'Accept-Language' : 'zh-CN,zh;q=0.9' } cj=hc.CookieJar() hl=ur.HTTPCookieProcessor(cj) opener=ur.build_opener(hl)
爬取ss号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 sslist=list () pattern=re.compile (r'https://www.bilibili.com/bangumi/play/ss\d+' ) pattern2=re.compile (r'"title":"(.*?)"' ) def ssdownload (): for i in range (1 ,1000 ): url='https://api.bilibili.com/pgc/season/index/result?season_version=-1&area=-1&is_finish=-1©right=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&order=5&st=1&sort=0&page=' +str (i)+'&season_type=1&pagesize=20&type=1' print ('downloading page=' +str (i)) try_time=0 while try_time<=5 : try : r=ur.Request(url=url,headers=headers) response=opener.open (r) break except ue.HTTPError as e: print ('Page Not found...skipped' ) break except Exception as e: try_time+=1 print ('retrying' ,try_time) else : raise Exception('Download Failed!!' ) try : content=str (gzip.decompress(response.read()),'utf-8' ) except Exception as e: break response.close() titles=re.findall(pattern2,content) ssurl=re.findall(pattern,content) for i in range (len (ssurl)): sslist.append({'title' :titles[i],'ssurl' :ssurl[i]}) def ssdownload2 (): for i in range (1 ,1000 ): url='https://api.bilibili.com/pgc/season/index/result?season_version=-1&is_finish=-1©right=-1&season_status=-1&year=-1&style_id=-1&order=5&st=4&sort=0&page=' +str (i)+'&season_type=4&pagesize=20&type=1' print ('downloading page=' +str (i)) try_time=0 while try_time<=5 : try : r=ur.Request(url=url,headers=headers) response=opener.open (r) break except ue.HTTPError as e: print ('Page Not found...skipped' ) break except Exception as e: try_time+=1 print ('retrying' ,try_time) else : raise Exception('Download Failed!!' ) try : content=str (gzip.decompress(response.read()),'utf-8' ) except Exception as e: break response.close() titles=re.findall(pattern2,content) ssurl=re.findall(pattern,content) for i in range (len (ssurl)): sslist.append({'title' :titles[i],'ssurl' :ssurl[i]}) ssdownload() ssdownload2()
访问ss链接获取md号与简介
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 mdpattern=re.compile ('\d+' ) mdlist=[] summary_dict={} def mddownload (): for each_ssurl in sslist: print ('downloading item=' +str (sslist.index(each_ssurl))) try_time=0 httperror=False while try_time<=5 : try : r=ur.Request(url=each_ssurl['ssurl' ],headers=wwwheaders) response=opener.open (r) break except ue.HTTPError as e: httperror=True print ('Page Not found...skipped' ) break except Exception as e: try_time+=1 print ('retrying' ,try_time) else : raise Exception('Download Failed!!' ) if httperror: continue content=str (gzip.decompress(response.read()),'utf-8' ) response.close() soup=bs(content) mdlist.append(re.search(mdpattern,str (soup.find('div' , id ='media_module' ).find('a' ))).group()) summary_dict.update({mdlist[-1 ]:soup.find('span' ,class_='absolute' ).text}) mddownload() mdlist=sorted (mdlist)
访问api
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bilidb=list () for i in mdlist: url='https://api.bilibili.com/pgc/review/user?media_id=' +str (i) r=ur.Request(url=url,headers=apiheaders) response=opener.open (r) content=str (response.read(),'utf-8' ) mdjson=json.loads(content) if 'result' in mdjson.keys(): if 'media' in mdjson['result' ].keys(): url='https://api.bilibili.com/pgc/web/season/stat?season_id=' +str (mdjson['result' ]['media' ]['season_id' ]) r=ur.Request(url=url,headers=headers) response=opener.open (r) content=str (response.read(),'utf-8' ) mdjson2=json.loads(content) mdjson['result' ].update(mdjson2['result' ]) bilidb.append(mdjson['result' ] if 'result' in mdjson.keys() else dict ()) print (mdlist.index(i),end='\r' )
访问md页面获取日期、集数、tags
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 tp_finished=re.compile (r'(\d+)年(\d+)月(\d+)日开播.*(\d+)' ) tp_not_finished=re.compile (r'(\d+)年(\d+)月(\d+)日开播' ) tp_movie=re.compile (r'(\d+)年(\d+)月(\d+)日上映' ) def download (): for i in range (len (bilidb)): print (i,end='\r' ) if 'media' not in bilidb[i].keys(): continue url=bilidb[i]['media' ]['share_url' ] try_time=0 httperror=False while try_time<=5 : try : r=ur.Request(url=url,headers=wwwheaders) response=opener.open (r) break except ue.HTTPError as e: httperror=True print ('Page Not found...skipped' ) break except Exception as e: try_time+=1 print ('retrying' ,try_time) else : raise Exception('Download Failed!!' ) if httperror: continue content=str (gzip.decompress(response.read()),'utf-8' ) response.close() soup=bs(content) tags=soup.find('div' ,class_='media-info-r' ).find_all('span' ,{'class' :'media-tag' }) tags=[each_tag.text for each_tag in tags] tags=' ' .join(tags) if len (tags) else '' time=soup.find('div' ,class_='media-info-r' ).find_all('div' ,{'class' :'media-info-time' })[0 ] res=re.search(tp_finished,time.text) finished=False if res is not None : finished=True res=res.groups() date='-' .join(res[0 :3 ]) episodes=res[3 ] else : res=re.search(tp_not_finished,time.text) if res is not None : res=res.groups() date='-' .join(res[0 :3 ]) episodes=0 else : res=re.search(tp_movie,time.text) if res is not None : res=res.groups() date='-' .join(res[0 :3 ]) episodes=1 else : date='' episodes=-1 bilidb[i].update({'tags' :tags,'date' :date,'episodes' :episodes}) print (i,{'tags' :tags,'date' :date,'episodes' :episodes}) download()
数据初步清洗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 bilidb2=[] for i in range (len (bilidb)): a=dict () if not len (bilidb[i]): continue a.update({ 'title' :bilidb[i]['media' ]['title' ], 'type_name' :bilidb[i]['media' ]['type_name' ], 'season_id' :bilidb[i]['media' ]['season_id' ], 'area' :bilidb[i]['media' ]['areas' ][0 ]['name' ] if len (bilidb[i]['media' ]['areas' ])>0 else '' , 'media_id' :bilidb[i]['media' ]['media_id' ], 'rating' :bilidb[i]['media' ]['rating' ]['score' ] if 'rating' in bilidb[i]['media' ].keys() else 0 , 'raters' :bilidb[i]['media' ]['rating' ]['count' ] if 'rating' in bilidb[i]['media' ].keys() else 0 , 'cover' :bilidb[i]['media' ]['cover' ], 'follow' :bilidb[i]['follow' ] if 'follow' in bilidb[i].keys() else 0 , 'series_follow' :bilidb[i]['series_follow' ] if 'series_follow' in bilidb[i].keys() else 0 , 'views' :bilidb[i]['views' ] if 'views' in bilidb[i].keys() else 0 , 'coins' :bilidb[i]['coins' ] if 'coins' in bilidb[i].keys() else 0 , 'danmakus' :bilidb[i]['danmakus' ] if 'danmakus' in bilidb[i].keys() else 0 , 'tags' :bilidb[i]['tags' ] if 'tags' in bilidb[i].keys() else '' , 'date' :bilidb[i]['date' ] if 'date' in bilidb[i].keys() else '' , 'episodes' :bilidb[i]['episodes' ] if 'episodes' in bilidb[i].keys() else 0 }) bilidb2.append(a) for each_item in bilidb2: summary=summary_dict.get(str (each_item['media_id' ])) each_item.update({'简介' :summary if summary is not None else '' }) date=str (time.localtime().tm_mon)+'_' +str (time.localtime().tm_mday) with open (r'bilibili\sslist_' +date+'.json' ,'w' ) as fp: json.dump(sslist,fp) with open (r'bilibili\mdlist_' +date+'.json' ,'w' ) as fp: json.dump(mdlist,fp) with open (r'bilibili\bilidb_' +date+'.json' ,'w' ) as fp: json.dump(bilidb2,fp) bilidb3=pd.DataFrame(bilidb2) bilidb3.to_csv(r'bilibili\bilidb_' +date+'.csv' ,index=False )
初步清洗后的数据格式如下:
title
type_name
season_id
area
media_id
rating
raters
cover
follow
series_follow
views
coins
danmakus
tags
date
episodes
简介
160
散华礼弥
番剧
710
日本
710
9.4
8016
http://i0.hdslb.com/bfs/bangumi/6dccd70827dd5f...
1124707
1124705
4384323
38564
255295
奇幻 日常 治愈
2012-4-5
4
毫无特色的少年降谷千纮,就读于县立紫阳高校一年级,是个非常喜爱僵尸的人。少女散华礼弥是散华家...
161
恋爱随意链接
番剧
713
日本
713
9.5
9269
http://i0.hdslb.com/bfs/bangumi/8274f1107032a6...
730312
730313
3206322
33427
219865
日常 少女 校园 小说改
2012-7-7
7
故事发生在私立山星高中,这所学校的文化研究部内,八重樫太一、永濑伊织、稻叶姬子、桐山唯、...
162
猫物语(黑)
番剧
723
日本
723
9.7
1926
http://i0.hdslb.com/bfs/bangumi/d24e532a91234b...
433136
433079
811255
4162
12484
奇幻 声控 小说改 神魔
2012-12-31
4
黄金周的第一天,阿良良木历和班长羽川翼一起埋葬了一只被车碾过,没有尾巴的猫。这本应是一件的普...
封面图下载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def imgdl (): for i in range (len (bilidb)): print (i,end='\r' ) url=bilidb.cover.values[i] md=bilidb.media_id.values[i] try_time=0 httperror=False while try_time<=5 : try : r=ur.Request(url=url,headers=headers) response=opener.open (r) break except ue.HTTPError as e: httperror=True print ('Page Not found...skipped' ) break except Exception as e: try_time+=1 print ('retrying' ,try_time) else : raise Exception('Download Failed!!' ) content=response.read() response.close() with open ('bilibili\\covers\\' + str (md) + '.jpg' ,'wb' ) as fp: fp.write(content)
4. 基于pandas的综合数据分析和基于matplotlib的数据可视化
4.1 导入依赖库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import numpy as npimport pandas as pdfrom scipy import statsimport matplotlib.pyplot as pltimport timeimport osfrom bs4 import BeautifulSoup as bsimport urllib.request as urimport urllib.parse as upimport urllib.error as ueimport http.cookiejar as hcimport reimport gzipimport json%matplotlib inline plt.rcParams['font.sans-serif' ]=['SimHei' ] plt.rcParams['axes.unicode_minus' ]=False os.chdir(r'...' )
4.2 数据清洗
4.2.1 日期型数据处理
对于bilibili的日期格式:
1 2 3 4 5 6 7 8 9 10 11 12 0 2013-1-3 1 2014-7-8 2 2014-7-2 3 2012-10-7 4 2013-7-7 ... 3703 NaN 3704 NaN 3705 NaN 3706 NaN 3707 NaN Name: date , Length: 3708, dtype: object
先将其转换为datetime格式:
1 2 bilidb.date=pd.to_datetime(bilidb.date,format ='%Y-%m-%d' ,errors='coerce' ).copy() bilidb.date
1 2 3 4 5 6 7 8 9 10 11 12 0 2013-01-03 1 2014-07-08 2 2014-07-02 3 2012-10-07 4 2013-07-07 ... 3703 NaT 3704 NaT 3705 NaT 3706 NaT 3707 NaT Name: date , Length: 3708, dtype: datetime64[ns]
再提取出年份,并转换为int格式,缺失值设为0:
1 2 3 bilidb['year' ]=bilidb.date.apply(lambda x: x.year) bilidb.year=bilidb.year.apply(lambda x: 0 if not pd.notna(x) else int (x)) bilidb.year
1 2 3 4 5 6 7 8 9 10 11 12 0 2013 1 2014 2 2014 3 2012 4 2013 ... 3703 0 3704 0 3705 0 3706 0 3707 0 Name: date , Length: 3708, dtype: int64
对于bangumi,由于多集动画和单集电影的播出时间分处于放送开始和上映年度两个键中,故需将其合并后处理。
放送开始
上映年度
0
1998年10月23日
NaN
1
2004年1月1日
NaN
2
2002年10月1日
NaN
3
2008年10月2日
NaN
4
1995年10月4日
NaN
5
NaN
1995年11月18日
6
NaN
1997年7月19日
7
NaN
NaN
8
2009年4月5日
NaN
9
2017年10月14日
NaN
1 2 3 4 5 6 7 8 9 10 11 12 13 14 bgmdb['日期' ]=pd.to_datetime(bgmdb['放送开始' ],format ='%Y年%m月%d日' ,errors='coerce' ) bgmdb['日期2' ]=pd.to_datetime(bgmdb['上映年度' ],format ='%Y年%m月%d日' ,errors='coerce' ) for i in range (len (bgmdb['日期' ])): if bgmdb['日期' ][i] is pd.NaT: bgmdb.loc[i,'日期' ]=bgmdb.loc[i,'日期2' ] bgmdb.drop('日期2' ,axis=1 ,inplace=True ) bgmdb['年度' ]=0 for i in range (len (bgmdb)): bgmdb.loc[i,'年度' ]=bgmdb.loc[i,'日期' ].year if bgmdb.loc[i,'日期' ] is not pd.NaT else 0 bgmdb[['放送开始' ,'上映年度' ,'日期' ,'年度' ]].head(10 )
放送开始
上映年度
日期
年度
0
1998年10月23日
NaN
1998-10-23
1998
1
2004年1月1日
NaN
2004-01-01
2004
2
2002年10月1日
NaN
2002-10-01
2002
3
2008年10月2日
NaN
2008-10-02
2008
4
1995年10月4日
NaN
1995-10-04
1995
5
NaN
1995年11月18日
1995-11-18
1995
6
NaN
1997年7月19日
1997-07-19
1997
7
NaN
NaN
NaT
0
8
2009年4月5日
NaN
2009-04-05
2009
9
2017年10月14日
NaN
2017-10-14
2017
4.2.2 bilibili评分缺失值处理
需要将评分人数不足的作品的评分及评分人数从0改为nan,以便后面进行剔除:
1 2 bilidb.rating=bilidb.rating.apply(lambda x: np.nan if x==0 else x) bilidb.raters=bilidb.raters.apply(lambda x: np.nan if x==0 else x)
4.3 基本描述统计
4.3.1 bilibili评分
1 2 3 (len (bilidb.rating), bilidb.rating.dropna().describe(), bilidb.rating[bilidb.rating==bilidb.rating.mode()[0 ]].count())
1 2 3 4 5 6 7 8 9 10 11 (3708, count 2581.00000 mean 9.12592 std 1.01527 min 2.20000 25% 9.10000 50% 9.50000 75% 9.70000 max 9.90000 Name: rating, dtype: float64, 431)
即在3708部作品中,有2581部有评分,且平均分为9.12分,中位数为9.5分(共431部作品)。
1 2 3 4 5 6 7 8 9 10 fig=plt.figure(num=100 ,figsize=(6 ,4 ),dpi=200 ) ax=fig.gca() nx=np.arange(2 ,11 ,0.1 ) ny=normfun(nx,bilidb.rating.dropna().mean(),bilidb.rating.dropna().std()) ax.plot(nx,ny) ax.hist(x=bilidb.rating.dropna(),bins=20 ,color='yellow' ,edgecolor='black' ,density=True ) ax.set_title('bilibili评分频数分布直方图与正态分布曲线' ) ax.text(2.5 ,0.8 ,'average=%.4f\nstd=%.4f' %(bilidb.rating.dropna().mean(),bilidb.rating.dropna().std()),fontsize=15 )
可以看出评分的分布与正态分布相差较大。
1 2 (bilidb.raters.dropna().sum (), (bilidb.rating.dropna()*bilidb.raters.dropna()).sum ()/bilidb.raters.sum ())
1 (14881442.0, 8.987174381353636)
求得总评分数超过1488万,票均评分为8.99分。
4.3.2 bangumi评分
1 2 3 4 5 6 7 8 9 count 5830.000000 mean 6.633856 std 0.880986 min 1.068000 25% 6.127250 50% 6.691500 75% 7.236750 max 9.143000 Name: rating, dtype: float64
即在5830部作品中,平均分为6.63分,中位数为6.69分。
1 2 3 4 5 6 7 8 9 10 fig=plt.figure(num=101 ,figsize=(6 ,4 ),dpi=200 ) ax=fig.gca() nx=np.arange(1 ,10 ,0.1 ) ny=normfun(nx,bgmdb.rating.mean(),bgmdb.rating.std()) ax.plot(nx,ny) ax.hist(x=bgmdb.rating,bins=32 ,color='yellow' ,edgecolor='black' ,density=True ) ax.set_title('bangumi评分频数分布直方图与正态分布曲线' ) ax.text(2.1 ,0.4 ,'average=%.4f\nstd=%.4f' %(bgmdb.rating.mean(),bgmdb.rating.std()),fontsize=15 )
可以看出评分的分布与正态分布相当吻合。
1 bgmdb.votes.sum (),(bgmdb.rating*bgmdb.votes).sum ()/bgmdb.votes.sum ()
1 (4525600, 7.185151881518473)
求得总评分数超过452万,票均评分为7.19分。
4.4 bangumi动画作品数据分析
4.4.1 每个动画公司各年度制作了多少动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 yr_prod=pd.crosstab(index=bgmdb['年度' ],columns=bgmdb['动画制作' ]) studios=bgmdb['动画制作' ].dropna().unique() studio_dict={} for each_studio in studios: studio_dict.update({str (each_studio): bgmdb['动画制作' ].dropna()[ bgmdb['动画制作' ].dropna().str .contains(each_studio) ].count()}) studio_dict=pd.Series(studio_dict) yr_prod.loc[:,studio_dict.sort_values(ascending=False ).index.tolist()[:20 ]][15 :]
动画制作
サンライズ
J.C.STAFF
東映アニメーション
MADHOUSE
Production I.G
スタジオディーン
A-1 Pictures
AIC
トムス・エンタテインメント
ぴえろ
BONES
シンエイ動画
XEBEC
GONZO
SHAFT
SILVER LINK.
京都アニメーション
オー・エル・エム
サテライト
Brain's Base
年度
1974
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1975
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1977
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1978
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
1979
1
0
1
0
0
0
0
0
1
0
0
1
0
0
0
0
0
0
0
0
1980
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
1981
2
0
2
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
1982
3
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
1983
0
0
0
0
0
0
0
0
1
1
0
1
0
0
0
0
0
0
0
0
1984
0
0
1
0
0
0
0
0
0
1
0
1
0
0
0
0
0
0
0
0
1985
2
0
0
1
0
1
0
0
0
0
0
1
0
0
0
0
0
0
0
0
1986
0
0
3
0
0
1
0
0
0
0
0
1
0
0
0
0
0
0
0
0
1987
1
0
3
1
0
0
0
0
1
1
0
1
0
0
0
0
0
0
0
0
1988
3
0
1
0
0
0
0
0
1
2
0
1
0
0
0
0
0
0
0
0
1989
3
0
2
1
0
3
0
0
0
0
0
1
0
0
0
0
0
0
0
0
1990
1
0
3
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
1991
2
0
2
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
1992
1
0
3
0
1
0
0
0
0
1
0
2
0
0
0
0
0
0
0
0
1993
1
0
5
0
1
0
0
0
0
1
0
2
0
0
0
0
0
0
0
0
1994
1
0
3
0
0
0
0
0
0
1
0
2
0
0
0
0
0
0
0
0
1995
1
1
4
0
1
0
0
1
2
1
0
3
0
0
1
0
0
1
0
0
1996
2
1
4
0
0
1
0
1
2
2
0
1
2
0
0
0
0
0
0
0
1997
2
2
2
0
0
0
0
3
1
2
0
2
2
0
0
0
0
0
0
0
1998
5
2
2
4
0
1
0
1
1
0
0
1
2
0
0
0
0
1
0
0
1999
5
2
5
5
1
2
0
1
1
1
0
3
1
0
0
0
0
2
0
0
2000
3
2
4
7
1
1
0
1
3
3
1
2
1
0
0
0
0
1
0
0
2001
3
3
5
4
1
3
0
0
1
3
2
3
2
1
0
0
0
2
0
0
2002
6
3
4
5
3
3
0
0
3
3
1
3
0
3
0
0
0
0
0
0
2003
2
6
2
6
1
1
0
1
4
2
4
2
2
2
0
0
1
0
0
0
2004
6
1
2
5
5
3
0
0
1
5
2
2
1
6
1
0
0
1
0
0
2005
5
7
6
4
5
3
0
0
1
3
2
2
3
4
0
0
3
1
2
0
2006
7
5
8
10
4
6
0
0
2
2
3
2
2
5
1
0
2
1
0
1
2007
3
7
5
12
2
4
1
1
1
4
3
3
2
7
4
0
2
0
3
1
2008
4
7
4
11
6
6
5
1
2
2
2
2
4
6
3
0
1
1
2
2
2009
3
6
6
10
8
5
2
3
2
2
2
2
1
3
5
1
6
0
4
0
2010
4
8
4
6
5
3
8
4
3
3
2
2
4
0
4
1
2
1
0
0
2011
4
7
7
7
9
4
6
1
3
2
11
2
3
3
3
2
2
1
2
3
2012
7
9
10
1
9
3
8
6
5
3
2
3
5
1
5
5
4
1
3
3
2013
8
5
7
6
7
6
11
2
5
1
2
2
5
5
3
3
5
3
5
4
2014
9
6
12
5
6
7
13
0
7
5
7
6
4
1
4
4
4
2
3
5
2015
5
6
9
4
10
3
13
0
1
4
5
4
3
1
4
5
4
1
3
3
2016
7
8
10
2
2
9
17
0
3
4
8
3
1
1
4
6
4
4
4
4
2017
8
10
5
4
4
6
13
0
2
5
3
3
4
3
4
9
2
2
2
2
2018
6
11
6
6
4
7
6
0
5
4
8
2
2
7
2
3
4
1
4
1
2019
4
11
3
5
8
2
2
0
3
2
5
2
0
2
1
5
2
3
3
1
2020
1
2
1
0
3
1
2
0
1
0
0
1
0
0
1
2
1
0
0
1
4.4.2 总的动画制作分布
1 2 3 4 5 6 7 8 9 10 fig=plt.figure(num=104 ,figsize=(6 ,4 ),dpi=200 ,facecolor='white' ) ax=fig.gca() y=studio_dict.sort_values(ascending=False ).index.tolist()[:17 ] x=studio_dict.sort_values(ascending=False ).values.tolist()[:17 ] ax.pie(x,labels=y,autopct='%.1f%%' ,pctdistance=0.5 ,labeldistance=1.1 , \ startangle=120 ,radius=1.2 ,counterclock=False ,wedgeprops={'linewidth' :1.5 ,'edgecolor' :'green' }, \ textprops={'fontsize' :10 ,'color' :'black' }) ax.set_title('动画制作分布(前%.0f%%作品)' %( 100 *studio_dict.sort_values(ascending=False )[:17 ].sum ()/studio_dict.sum ()),pad=30 )
4.4.3 每个动画公司制作的动画部数及平均评分
1 2 3 4 5 6 7 8 studios_ratings=pd.pivot_table(bgmdb,values='rating' ,index='动画制作' ,aggfunc=np.mean,margins=False ,dropna=True ) studios_counts=pd.pivot_table(bgmdb,values='subject' ,index='动画制作' ,aggfunc=len ,margins=False ,dropna=True ) studios_counts_ratings=pd.merge(studios_ratings,studios_counts,on='动画制作' ).sort_values( by=['subject' ,'rating' ],ascending=False ).head(30 ) studios_counts_ratings
rating
subject
动画制作
東映アニメーション
6.929124
186
サンライズ
7.106657
178
J.C.STAFF
6.810706
177
MADHOUSE
7.092288
156
Production I.G
7.176925
133
A-1 Pictures
6.776467
122
スタジオディーン
6.799629
116
BONES
7.021640
89
ぴえろ
6.644360
89
シンエイ動画
7.067851
87
トムス・エンタテインメント
6.786667
78
XEBEC
6.789254
67
GONZO
6.536925
67
京都アニメーション
7.367710
62
SHAFT
7.434050
60
SILVER LINK.
6.592797
59
オー・エル・エム
6.902265
49
動画工房
6.658000
48
サテライト
6.435106
47
Brain's Base
6.703870
46
セブン
6.138848
46
Milky
5.883156
45
AIC
6.785727
44
ZEXCS
6.272095
42
ティーレックス
5.959929
42
アームス
6.402683
41
ラルケ
6.555162
37
diomedéa
6.198838
37
ufotable
7.170139
36
Walt Disney Animation Studios
7.089556
36
选择其中平均评分最高的10个动画公司:
1 studios_counts_ratings.sort_values(by=['rating' ],ascending=False ).head(10 )
rating
subject
动画制作
SHAFT
7.434050
60
京都アニメーション
7.367710
62
Production I.G
7.176925
133
ufotable
7.170139
36
サンライズ
7.106657
178
MADHOUSE
7.092288
156
シンエイ動画
7.067851
87
BONES
7.021640
89
東映アニメーション
6.929124
186
オー・エル・エム
6.902265
49
可以看到,SHAFT、京都动画、Production I.G等动画公司出产的作品平均质量较高,这与许多动画爱好者的观点是一致的。
4.4.4 2000-2019年热门动画作品及趋势分析
热门作品
1 2 3 4 5 6 a=bgmdb[bgmdb.['年度' ]==2000 ].sort_values(by='votes' ,ascending=False ).loc[:,'中文名' ][:5 ].values a.resize(5 ,1 ) for i in range (2001 ,2020 ): b=bgmdb[bgmdb.['年度' ]==i].sort_values(by='votes' ,ascending=False ).loc[:,'中文名' ][:5 ].values b.resize(5 ,1 ) a=np.concatenate([a,b],axis=1 )
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
热度第1名
犬夜叉
千与千寻
攻壳机动队 STAND ALONE COMPLEX
钢之炼金术师
混沌武士
NaN
Code Geass 反叛的鲁路修
CLANNAD
NaN
化物语
NaN
魔法少女小圆
冰菓
进击的巨人
白箱
吹响!悠风号
你的名字。
小林家的龙女仆
紫罗兰永恒花园
辉夜大小姐想让我告白~天才们的恋爱头脑战~
热度第2名
名侦探柯南 瞳孔中的暗杀者
棋魂
全金属狂潮
全金属狂潮 校园篇
哈尔的移动城堡
虫师
凉宫春日的忧郁
秒速5厘米
龙与虎
钢之炼金术师 FULLMETAL ALCHEMIST
凉宫春日的消失
命运石之门
刀剑神域
我的青春恋爱物语果然有问题
月刊少女野崎君
一拳超人
Re:从零开始的异世界生活
来自深渊
青春笨蛋少年不做兔女郎学姐的梦
进击的巨人 第三季 Part.2
热度第3名
魔卡少女樱 被封印的卡片
星际牛仔 天国之扉
火影忍者
东京教父
攻壳机动队 S.A.C. 2nd GIG
灼眼的夏娜
死亡笔记
幸运星
Code Geass 反叛的鲁路修R2
轻音少女
无头骑士异闻录
NaN
男子高中生的日常
某科学的超电磁炮S
Fate/stay night [Unlimited Blade Works]
Fate/stay night [Unlimited Blade Works] 第二季
为美好的世界献上祝福!
情色漫画老师
比宇宙更远的地方
鬼灭之刃
热度第4名
游戏王-怪兽之决斗
名侦探柯南 通往天国的倒数计时
名侦探柯南 贝克街的亡灵
名侦探柯南 迷宫的十字路口
妖精的旋律
搞笑漫画日和
银魂
福音战士新剧场版:序
魔法禁书目录
某科学的超电磁炮
我的妹妹哪有这么可爱!
我们仍未知道那天所看见的花的名字。
中二病也要谈恋爱!
斩服少女
NO GAME NO LIFE 游戏人生
路人女主的养成方法
甲铁城的卡巴内利
少女终末旅行
DARLING in the FRANXX
灵能百分百 第二季
热度第5名
吸血鬼猎人D:妖杀行
热带雨林的爆笑生活
人形电脑天使心
奇诺之旅
攻壳机动队2 无罪
蜂蜜与四叶草
NaN
永生之酒
夏目友人帐
凉宫春日的忧郁 2009
轻音少女 第二季
日常
心理测量者
打工吧!魔王大人
四月是你的谎言
我的青春恋爱物语果然有问题 续
灵能百分百
进击的巨人 第二季
佐贺偶像是传奇
约定的梦幻岛
高分作品
1 2 3 4 5 6 7 a=bgmdb[bgmdb['年度' ]==2000 ].sort_values(by='rating' ,ascending=False ).loc[:,'中文名' ][:5 ].values a.resize(5 ,1 ) for i in range (2001 ,2020 ): b=bgmdb[bgmdb['年度' ]==i].sort_values(by='rating' ,ascending=False ).loc[:,'中文名' ][:5 ].values b.resize(5 ,1 ) a=np.concatenate([a,b],axis=1 ) pd.DataFrame(data=a,index=['评分第%d名' %i for i in range (1 ,6 )],columns=range (2000 ,2020 ))
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
评分第1名
第一神拳
千与千寻
攻壳机动队 STAND ALONE COMPLEX
百变之星
攻壳机动队 S.A.C. 2nd GIG
虫师
银魂
天元突破 红莲螺岩
NaN
钢之炼金术师 FULLMETAL ALCHEMIST
凉宫春日的消失
银魂'
银魂' 延长战
歌牌情缘2
白箱
水星领航员 The AVVENIRE
排球少年 乌野高校 VS 白鸟泽学园高校
3月的狮子 第二季
莉兹与青鸟
进击的巨人 第三季 Part.2
评分第2名
吸血鬼猎人D:妖杀行
星际牛仔 天国之扉
小魔女DoReMi 大合奏
钢之炼金术师
攻壳机动队2 无罪
蜂蜜与四叶草
蜂蜜与四叶草II
CLANNAD
水星领航员 第三季
福音战士新剧场版:破
四叠半神话大系
命运石之门
爆漫王。3
剧场版 魔法少女小圆 剧场版 [新篇] 叛逆的物语
虫师 续章 第2期
少女与战车 剧场版
3月的狮子
昭和元禄落语心中 -助六再临篇-
强风吹拂
瑞克和莫蒂 第四季
评分第3名
游戏王-怪兽之决斗
棋魂
十二国记
星空清理者
混沌武士
哆啦A梦
盗梦侦探
福音战士新剧场版:序
攻壳机动队2.0
化物语
Heart Catch 光之美少女!
魔法少女小圆
来自新世界
小马驹G4 第四季
虫师 续章
排球少年 第二季
吹响!悠风号 第二季
来自深渊
比宇宙更远的地方
高分少女 第二季
评分第4名
小魔女DoReMi ♯
蜡笔小新 呼风唤雨!大人帝国的反击
萩萩公主
东京教父
怪物
水星领航员
死亡笔记
物怪
剧场版 空之境界 第五章 矛盾螺旋
天元突破红莲螺岩 螺岩篇
王牌投手 振臂高挥~夏日大会篇~
日常
JOJO的奇妙冒险
辉夜姬物语
乒乓
JOJO的奇妙冒险 星尘斗士 埃及篇
昭和元禄落语心中
春宵苦短,少女前进吧!
摇曳露营△
海盗战记
评分第5名
魔卡少女樱 被封印的卡片
大~集合!小魔女DoReMi
阿滋漫画大王
全金属狂潮 校园篇
飞跃巅峰2!
交响诗篇
攻壳机动队 S.A.C. Solid State Society
永生之酒
Code Geass 反叛的鲁路修R2
剧场版 空之境界 第七章 杀人考察(后)
小马驹G4 第一季
夏目友人帐 参
冰菓
宇宙战舰大和号2199
怪诞小镇 第二季
虫师 续章 铃之雫
你的名字。
终物语(下)
JOJO的奇妙冒险 黄金之风
灵能百分百 第二季
1 2 3 4 5 6 7 8 9 10 11 12 13 14 bgmdb_by_year=bgmdb.groupby(by='年度' ) def count_rank (x ): count=0 for i in x: if i<100 : count+=1 return count TOP100_by_year=bgmdb_by_year.bgmrank.apply(count_rank)[-21 :-1 ] print (TOP100_by_year)fig = plt.figure(num=112 ,figsize=(6 ,4 ),dpi=200 ,facecolor='white' ) ax = plt.gca() ax.plot(TOP100_by_year.index,TOP100_by_year.values) ax.set (title='各年度TOP100进榜数量' ,xticks=range (2000 ,2019 ,2 ))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 年度 2000 0 2001 3 2002 3 2003 3 2004 4 2005 3 2006 7 2007 4 2008 4 2009 2 2010 3 2011 4 2012 2 2013 2 2014 7 2015 5 2016 1 2017 2 2018 1 2019 1
可以看到,在2006年和2014年,优质作品出现井喷现象,但近几年来优质作品减少趋势明显。
1 2 3 4 5 6 7 TOP10=pd.DataFrame(data=[ bgmdb[bgmdb['年度' ]==i].sort_values( by='rating' ,ascending=False ).loc[:,'rating' ][:10 ].mean() for i in range (2000 ,2020 ) ],columns=['TOP10均分' ],index=range (2000 ,2020 )) TOP10
TOP10均分
2000
7.7456
2001
8.0019
2002
8.0241
2003
8.1159
2004
8.2890
2005
8.1902
2006
8.3470
2007
8.2656
2008
8.3198
2009
8.2012
2010
8.1866
2011
8.3057
2012
8.1612
2013
8.0661
2014
8.3658
2015
8.2285
2016
8.0552
2017
8.1678
2018
8.1140
2019
8.1075
1 2 3 4 fig = plt.figure(num=111 ,figsize=(6 ,4 ),dpi=200 ,facecolor='white' ) ax = plt.gca() ax.plot(TOP10.index,TOP10.values) ax.set (title='TOP10均分走势' ,xticks=range (2000 ,2019 ,2 ))
4.5 两站动画匹配与数据库的合并
4.5.1 匹配策略
注意到bilibili的搜索功能,可以搜索到对应作品的md号:
故采取以下的匹配策略:
导入bangumi的数据
按照日文原名和中文名向search.bilibili.com发出查询请求
从查询结果中提取番剧链接(md号)
根据名称、年份等信息进行匹配,得到匹配接口(subject-md)
4.5.2 查询脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 url='https://search.bilibili.com/all?' pattern=re.compile (r'www.bilibili.com/bangumi/media/md(.*?)/' ) interface=dict () for i in range (0 ,len (bgmdb)): res=[] print ('item' ,i,'searching' ,bgmdb['原名' ][i]) querystring={'keyword' :bgmdb['原名' ][i]} querystring=up.urlencode(querystring) final_url=url+querystring try_succeed=False while not try_succeed: try : try_succeed=True r=ur.Request(url=final_url,headers=headers) response=opener.open (r) except Exception as e: try_succeed=False time.sleep(1 ) print ('URL timeout! retrying...' ) content=str (gzip.decompress(response.read()),'utf-8' ) res.extend(re.findall(pattern,content)) response.close() print ('item' ,i,'searching' ,bgmdb['中文名' ][i]) querystring={'keyword' :bgmdb['中文名' ][i]} querystring=up.urlencode(querystring) final_url=url+querystring try_succeed=False while not try_succeed: try : try_succeed=True r=ur.Request(url=final_url,headers=headers) response=opener.open (r) except Exception as e: try_succeed=False time.sleep(1 ) print ('URL timeout! retrying...' ) content=str (gzip.decompress(response.read()),'utf-8' ) res.extend(re.findall(pattern,content)) response.close() res=list (set (res)) interface.update({bgmdb['subject' ][i]:res}) print ({bgmdb['subject' ][i]:res}) with open ('bangumi\\interfaces\\origin-subject2md.json' ,'w' ) as fp: json.dump(interface,fp)
接口格式:
1 2 3 4 5 6 7 8 {253: ['8023271' , '3008' , '5383' ], 326: ['1714' , '1705' , '1565' ], 324: ['1564' , '1712' , '1705' ], 876: ['1178' , '1180' ], 265: ['10352' , '10372' , '10332' , '1635' ], 237: ['1714' , '1568' , '1566' , '28228268' ], 6049: ['10272' ], ...}
4.5.3 信息匹配
首先,过滤掉没有搜索结果的键值对:
1 2 3 4 5 interface2={} for k,v in interface.items(): if len (v)>0 : v=[int (i) for i in v if len (i)>0 ] interface2.update({int (k):v})
然后,将subject号对应的bangumi信息对接,同时将md号对应的bilibili信息对接,生成新的字典
1 2 3 4 5 6 7 8 9 10 interface3=list () for k,v in interface2.items(): bgm=bgmdb.loc[bgmdb.subject==k,['年度' ,'subject' ,'原名' ,'中文名' ,'别名' ,'日期' ]].values[0 ] bili=[bilidb.loc[bilidb.media_id==each_bilimd,['year' ,'media_id' ,'title' ,'date' ]].values for each_bilimd in v] bili=[each_bilimd[0 ] for each_bilimd in bili if len (each_bilimd)] interface3.append({'bgm' :bgm,'bili' :bili}) interface3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [{'bgm' : array([1998, 253, 'カウボーイビバップ' , '星际牛仔' , '恶男杰特' , Timestamp('1998-10-23 00:00:00' )], dtype=object), 'bili' : [array([2001, 3008, '星际牛仔 天国之扉' , Timestamp('2001-09-01 00:00:00' )], dtype=object), array([1998, 5383, '星际牛仔 SP' , Timestamp('1998-10-23 00:00:00' )], dtype=object)]}, {'bgm' : array([2004, 326, '攻殻機動隊 S.A.C. 2nd GIG' , '攻壳机动队 S.A.C. 2nd GIG' , nan, Timestamp('2004-01-01 00:00:00' )], dtype=object), 'bili' : [array([2006, 1714, '攻壳机动队 个别的十一人' , Timestamp('2006-01-27 00:00:00' )], dtype=object), array([2006, 1705, '攻壳机动队 S.A.C. Solid State Society' , Timestamp('2006-09-01 00:00:00' )], dtype=object), array([2004, 1565, '攻壳机动队 S.A.C. 2nd GIG' , Timestamp('2004-01-01 00:00:00' )], dtype=object)]}, ...]
接着,寻找名称和年份相同的作品,存入fullymatched列表:
1 2 3 4 5 6 7 8 9 10 11 matched=[] for searchres in interface3: bgm=searchres['bgm' ] bili=searchres['bili' ] for bilimd in bili: for bgmname in bgm[2 :5 ]: if bilimd[2 ]==bgmname and bilimd[0 ]==bgm[0 ]: matched.append({'subject' :bgm[1 ],'md' :bilimd[1 ]}) import copyfullymatched=copy.deepcopy(matched)
然后,寻找名称不同但开播日期相同的作品,进行人工匹配,结果保存在manuallymatched中:
1 2 3 4 5 6 7 8 matched=[] for searchres in interface3: bgm=searchres['bgm' ] bili=searchres['bili' ] for bilimd in bili: if bilimd[2 ] not in bgm and (bilimd[3 ] is not pd.NaT and bilimd[3 ]==bgm[5 ]): matched.append({'bgm' :bgm[:-1 ],'bili' :bilimd[:-1 ]}) ...
最后,将两列表转换成DataFrame格式并合并去重,得到最终匹配结果:
1 2 3 4 5 6 fullymatched=pd.DataFrame(fullymatched) manuallymatched=pd.DataFrame(manuallymatched) finalmatch=pd.concat([fullymatched,manuallymatched],axis=0 ) finalmatch.drop_duplicates(subset='subject' ,keep='first' ,inplace=True ) len (finalmatch),finalmatch.head(5 )
1 2 3 4 5 6 7 (1837, subject md 0 326 1565 1 324 1564 2 876 1178 3 1428 1089 4 211567 6445)
最终匹配到了1837部作品。
4.5.4 数据库合并
使用匹配接口,将两个DataFrame合并。首先将接口与bilidb按md号内连接,然后再用bilidb与bgmdb按subject号内连接,并进行一系列调整,最终获得合并后的数据库:
1 2 3 4 5 6 bilidb=pd.merge(left=finalmatch,right=bilidb,left_on='md' ,right_on='media_id' ,how='inner' ) db=pd.merge(left=bilidb,right=bgmdb,on='subject' ,how='inner' ) db.drop(columns=['md' ],inplace=True ) db.rename(columns={'rating_x' :'bilirating' ,'rating_y' :'bgmrating' },inplace=True ) db.head(5 )
subject
title
type_name
season_id
area
media_id
bilirating
raters
cover
follow
...
开始
结束
片长
主题歌编曲
第二原画
音响
特效
机械设定
日期
年度
0
326
攻壳机动队 S.A.C. 2nd GIG
番剧
1565
日本
1565
9.8
2045.0
http://i0.hdslb.com/bfs/bangumi/00ee95c464defb...
152857
...
NaN
NaN
NaN
菅野よう子
NaN
NaN
村上正博
常木志伸、寺岡賢司
2004-01-01
2004
1
324
攻壳机动队 STAND ALONE COMPLEX
番剧
1564
日本
1564
9.8
3511.0
http://i0.hdslb.com/bfs/bangumi/6ebd07ba376115...
516939
...
NaN
NaN
NaN
菅野よう子
NaN
NaN
遠藤誠、村上正博
寺岡賢司、常木志伸
2002-10-01
2002
2
876
CLANNAD ~AFTER STORY~
番剧
1178
日本
1178
9.9
42904.0
http://i0.hdslb.com/bfs/bangumi/54003a09e72f0d...
917778
...
NaN
NaN
NaN
ANANT-GARDE EYES
NaN
NaN
NaN
NaN
2008-10-02
2008
3
1428
钢之炼金术师 FULLMETAL ALCHEMIST
番剧
1089
日本
1089
9.9
80126.0
http://i0.hdslb.com/bfs/bangumi/401f84cadca354...
1990896
...
NaN
NaN
NaN
大橋卓弥、常田真太郎
関本美穂
テクノサウンド
龍角里美、池上真崇
鈴木雅久
2009-04-05
2009
4
211567
3月的狮子 第二季
番剧
6445
日本
6445
9.8
15230.0
http://i0.hdslb.com/bfs/bangumi/14cf90e4ea9a05...
480677
...
NaN
NaN
NaN
NaN
谷口工作、吉澤翠
NaN
NaN
NaN
2017-10-14
2017
4.6 两站评分综合分析
4.6.1 相关性分析与散点图
1 2 res=stats.linregress(x=db.bgmrating,y=db.bilirating) res,res.rvalue**2
1 2 (LinregressResult(slope=0.8046754433744006, intercept=3.709341364364734, rvalue=0.736131603271121, pvalue=7.026688311545783e-200, stderr=0.02166711159881075), 0.5418897373345112)
拟合得到bili=0.80bgm+3.70,相关系数为0.74,决定系数为0.54,即两站评分呈现正相关关系,且bilibili分数的变化的一半可用bangumi分数变化来解释。
按原始数据作散点图和趋势线:
1 2 3 4 5 6 7 8 fig=plt.figure(num=105 ,figsize=(8 ,4 ),dpi=300 ,facecolor='white' ) ax=fig.gca() ax.scatter(x=db.bgmrating,y=db.bilirating,color='red' ,marker='.' ,s=0.1 ) linx=np.arange(2 ,9 ,0.1 ) liny=res.slope*linx+res.intercept ax.plot(linx,liny,ls='--' ,lw=0.5 ) ax.set (title='两站评分关系图' ,xlabel='bangumi' ,ylabel='bilibili' ) ax.text(2 ,9 ,'bili=%.4fbgm+%.4f\nR=%.2f' %(res.slope,res.intercept,res.rvalue),fontsize=10 )
但无论从图上看,还是从相关系数上看,两者的相关性存在,但不是很高。
4.6.2 气泡图、二维频次直方图与三维柱状图
由于bangumi的评分精确到小数点后三位,相同评分的作品很少,普通的散点图对分布情况的展示效果不佳。故尝试作气泡图、二维频次直方图与三维柱状图增强数据直观性。
作气泡图首先要将bangumi的评分的分辨率降至0.1分,然后建立数据交叉表:
1 2 3 4 5 db['bgmrating_2digit' ]=db['bgmrating' ].copy() db['bgmrating_2digit' ]=db['bgmrating_2digit' ].apply(lambda x : ((x*10 )//1 )/10 ) bb=pd.crosstab(index=db['bgmrating_2digit' ],columns=db['bilirating' ]) bb.iloc[20 :,40 :]
bilirating
8.4
8.5
8.6
8.7
8.8
8.9
9.0
9.1
9.2
9.3
9.4
9.5
9.6
9.7
9.8
9.9
bgmrating_2digit
5.4
1
0
0
0
1
0
0
2
1
1
0
0
0
1
0
0
5.5
1
1
1
2
0
2
2
0
0
1
1
0
1
0
0
0
5.6
1
0
1
2
1
0
0
1
0
0
0
1
0
1
0
0
5.7
2
2
1
1
2
1
2
0
0
2
0
2
1
0
0
0
5.8
0
4
2
1
0
0
2
1
2
1
2
1
1
0
0
0
5.9
0
1
0
2
2
2
2
2
2
2
1
0
0
0
0
0
6.0
1
1
2
2
2
4
2
4
1
6
0
1
1
0
0
0
6.1
1
1
3
2
3
1
3
5
3
2
6
5
1
0
0
0
6.2
0
1
1
5
1
4
7
4
5
5
4
4
5
0
0
0
6.3
1
0
1
0
4
2
5
4
7
3
5
6
5
2
1
0
6.4
0
1
1
0
0
1
7
1
8
2
5
4
4
1
0
0
6.5
0
1
1
2
1
2
1
5
10
7
14
7
4
6
0
0
6.6
0
0
1
1
1
4
3
3
7
11
10
7
13
5
1
0
6.7
0
0
0
0
2
0
0
2
2
5
6
11
13
5
1
0
6.8
0
0
0
0
2
1
1
3
4
3
8
9
4
7
4
0
6.9
1
0
0
0
1
1
0
2
3
3
6
10
3
13
6
0
7.0
0
0
1
0
1
1
0
1
3
5
7
6
13
21
3
0
7.1
0
0
0
0
0
1
0
2
1
4
8
3
10
11
8
1
7.2
1
0
0
0
0
1
0
1
3
5
7
16
13
13
8
0
7.3
0
0
0
1
0
0
1
1
1
2
4
10
10
15
7
1
7.4
0
0
0
1
0
0
0
1
1
2
5
2
12
17
10
0
7.5
0
0
0
0
0
0
0
1
2
0
2
4
10
11
9
0
7.6
0
0
0
0
0
0
1
0
1
2
3
0
5
15
7
2
7.7
0
0
0
0
0
1
0
0
0
0
3
4
2
10
10
0
7.8
0
0
0
0
0
0
0
0
0
1
0
5
3
9
10
2
7.9
0
0
0
0
0
0
0
0
1
0
0
3
1
10
7
3
8.0
0
0
0
0
0
0
0
0
0
2
0
1
0
5
10
1
8.1
0
0
0
0
0
0
0
0
0
0
0
0
1
2
5
0
8.2
0
0
0
0
0
0
0
0
0
0
0
0
2
6
8
2
8.3
0
1
0
0
0
0
0
0
0
0
0
2
1
0
1
1
8.4
0
0
0
0
0
0
0
0
0
0
0
1
0
0
4
0
8.5
0
0
0
0
0
0
0
0
0
0
0
0
0
2
0
0
8.6
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
8.7
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
8.8
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
1
9.0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
1
9.1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
接着对每一个点分别作图,实现气泡图的效果:
1 2 3 4 5 6 7 8 fig=plt.figure(num=106 ,figsize=(8 ,4 ),dpi=200 ,facecolor='white' ) ax=fig.gca() for eachbgmrating in bb.index.tolist(): for eachbilirating in bb.columns.tolist(): ax.scatter(eachbgmrating,eachbilirating, s=bb.loc[eachbgmrating,eachbilirating]**1.1 , marker='.' ,c='red' ) ax.set (title='两站评分关系图' ,xlabel='bangumi' ,ylabel='bilibili' )
还可直接使用hist2d函数构造二维频次直方图,附带标尺:
1 2 3 4 5 6 fig=plt.figure(num=107 ,figsize=(8 ,4 ),dpi=200 ,facecolor='white' ) plt.hist2d(db.bgmrating,db.bilirating,bins=50 ,cmap='Reds' ) ax=fig.gca() ax.set (xlim=(4 ,9.1 ),ylim=(7 ,9.9 ),title='两站评分二维频次直方图' ,xlabel='bangumi' ,ylabel='bilibili' ) cb=plt.colorbar() cb.set_label('counts' )
如果画出三维柱状分布图,柱高度代表作品数量,可以更明显地看出b站评分相对于bangumi更为集中,且绝大多数分布在9分以上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from mpl_toolkits.mplot3d import Axes3Dimport matplotlib as mplfig = plt.figure(num=108 ,figsize=(8 ,4 ),dpi=300 ,facecolor='white' ) ax=plt.subplot(111 ,projection='3d' ) x = bb.index.to_list() x_index=range (len (x)) y = bb.columns.to_list() y_index=range (len (y)) for i in x_index: z = bb.iloc[i,y_index] ax.bar(y, z, x[i],zdir='x' ,width=0.2 ) ax.set (title='三维分布图' ,xlabel='bangumi' ,ylabel='bilibili' )
4.6.3 箱线图
比较两网站的片均评分和中位数,可以看到bangumi两者差距很小,而b站平均分明显小于中位数。
bilibili
bangumi
片均评分
9.12592
6.633856
中位数
9.50000
6.691500
平均数小于中位数,意味着存在许多低分作品,且没有与之数量相当的高分作品。
我们可以通过箱线图更直观地展示两站评分的这种差异。
1 2 3 4 5 6 fig=plt.figure(num=102 ,figsize=(4 ,3 ),dpi=200 ,facecolor='white' ) ax=fig.gca() ax.boxplot(x=[bgmdb.rating,bilidb.rating.dropna()],showmeans=True ,meanline=True ,sym='.' ,widths=0.3 ) ax.set_xticklabels(['bangumi' ,'bilibili' ]) ax.set_ylabel('分数' ) ax.set_title('bangumi与bilibili评分箱线图' ,fontsize=10 )
可以看到,b站的异常值均出现在下边缘以下,并且数量比bangumi的多。而从直方图上也能看出,b站评分产生严重的拖尾,导致其片均评分明显小于中位数。
4.6.4 两网站评分特征及原因推测
通过bilibili的评分频数分布直方图 ,可以看到作品评分在高分段扎堆,呈现的趋势基本上是分数越高,作品越多。9.7分就有378部,占到了全部有评分动画的五分之一,严重地丧失了区分度。并且,从箱线图 中也能看出b站低评分很多导致片均评分低于中位数。
一般来说,作品评分极高和极低的作品数都应该很少,绝大多数作品评分应当集中在平均值左右(即正态分布)。很显然,b站的评分分布严重偏离了正态分布。
相比于bilibili,bangumi的频数分布直方图 呈现出两头低,中间高,左右对称的特点,相应的正态分布曲线与实际分布高度吻合。而且bangumi的分数集中度也较低,在6.6-6.7区间也只有266部动画,占比只有二十分之一多一点。所以至少从统计学规律上说,bangumi这个网站的评分更有参考意义。
与bangumi的对比告诉我们,b站的评分数据存在诸多异常之处。
既然分布很异常,那么b站评分到底代表了什么?产生这种分布的原因是什么?笔者尝试通过数据给出一些合理推断。
我们需要回来关注另一组数据,那就是片均评分和人均评分。
bilibili
bangumi
片均评分
9.211588
6.633856
票均评分
9.062114
7.185152
b站的片均评分高于票均评分,而bangumi的片均评分高于票均评分。
这说明了什么呢?首先我们知道,点评数与热度成正比。那么
当片均高于票均时,意味着高评分动画的评分人数较少,出现了好番不火的情况;
当片均低于票均时,意味着低评分动画的评分人数较少,出现了烂番没人看的情况。
显然,后者更符合常理。
现在,我们用数据说话,用具体数字表达“好番不火”或“烂番没人看”的程度。
好番是要和烂番作对比的,所以我们定义一个函数,称为相对人气指数 ,在给出百分比累积排名x的情况下,
相对人气指数的定义为:
该比值表示好番热度与同等程度的烂番热度之比。
并且,累积排名越高,则表示排名越靠前,而且如果好番和热度成正比,这个相对人气指数应当随累积排名增大而增大,是一个单调递增函数。
我们将相对人气指数对累积排名作图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 bgm_ninki=[ bgmdb[bgmdb.rating>=bgmdb.rating.quantile(i)].votes.mean()/ bgmdb[bgmdb.rating<=bgmdb.rating.quantile(1 -i)].votes.mean() for i in np.linspace(0.5 ,1 ,51 ) ] bili_ninki=[ bilidb[bilidb.rating>=bilidb.rating.quantile(i)].raters.mean()/ bilidb[bilidb.rating<=bilidb.rating.quantile(1 -i)].raters.mean() for i in np.linspace(0.5 ,1 ,51 ) ] fig=plt.figure(num=109 ,figsize=(4 ,3 ),dpi=200 ,facecolor='white' ) ax=fig.gca() x=np.linspace(0.5 ,1 ,51 ) ax.plot(x,bgm_ninki,label='bangumi' ) ax.plot(x,bili_ninki,label='bilibili' ) ax.set (xlim=(0.5 ,1 ),ylim=(0 ,7 ),xticks=[0.5 ,0.6 ,0.7 ,0.8 ,0.9 ,1 ],xticklabels=[50 ,60 ,70 ,80 ,90 ,100 ], xlabel='百分比排名' ,title="相对人气指数" ) ax.legend() ax.axhline(y=2.5 ,c='black' ,ls='--' ,lw=0.5 ) ax.axhspan(ymin=1 ,ymax=2.5 ,facecolor='yellow' ,alpha=0.2 )
可以看到,bgm明显出现了好番很火,烂番没人看的情况,而b站的曲线基本徘徊在1-2.5之间,这意味着有与看好番差不多人数的人也看烂番。
总的来说就是b站好番不火的程度比bangumi严重得多。
我们来分析好番不火出现的原因,而刚刚提到对“好”字的理解,我们就来谈一谈好番的评价标准。
评价一部番其实是蛮困难的事情,需要考虑故事情节、人物、画面、音乐、表达的思想内涵等等。
而现在看来,B站小伙伴们对于好番的评价标准可能出现了偏差:
现在有一种观点,认为人们只想看到他们想看到的东西,我想这也适用于评分者们。
这种倾向的一个集中表现就是合自己口味就打高分,不合自己口味就打压。
分数的高低代表自己接受不接受这部作品。
这种模糊片面,且带有强烈主观性的倾向会导致某些剧情或者设定晦涩难懂的作品难以得到多数人理解,遭冷门和打低分的概率增加,
这在高分段尤为明显。很多真正有思想有深度的番在热度和评分上均不敌所谓的季度霸权番。
总的来说就是:
B站的评分中含有更多的“观众接受度”的成分。
其实这种现象很常见。
以钉钉作为例子。
钉钉软件质量不错,能提供强大的团队协作支持,极大地方便了远程办公,
然而由于你知道的原因,广大同学并不接受这个软件,所以惨遭分期付款。
钉钉的评分很明显可以由两部分解释:5分是评软件功能的,而1分则表现接受程度。
下表是2017年以来b站评分9.8分及以上并且播放量超过1000万的作品(由于匹配不完全原因,列表不全),可以看到很多“霸权番”的身影,这些番热度和接受度都很高。
中文名
b站评分
bgm评分
bgm排名
播放量
放送开始
77
辉夜大小姐想让我告白?~天才们的恋爱头脑战~
9.9
7.966
4.61
117788191
2020年4月11日
150
擅长捉弄的高木同学 第二季
9.9
7.669
10.02
43794382
2019年7月7日
31
强风吹拂
9.9
8.241
1.87
24923712
2018年10月2日
101
妖精森林的小不点
9.9
7.852
6.55
19621709
2018年1月12日
208
鬼灭之刃
9.8
7.515
14.25
475758252
2019年4月6日
39
JOJO的奇妙冒险 黄金之风
9.8
8.106
2.93
288360998
2018年10月5日
105
辉夜大小姐想让我告白~天才们的恋爱头脑战~
9.8
7.817
7.15
194717353
2019年1月12日
274
刺客伍六七
9.8
7.386
19.04
249105011
2018年4月25日
254
青春笨蛋少年不做兔女郎学姐的梦
9.8
7.416
17.96
139628305
2018年10月3日
187
某科学的超电磁炮T
9.8
7.583
12.13
96256223
2020年1月10日
398
碧蓝之海
9.8
7.205
26.50
66390730
2018年7月13日
174
女高中生的无所事事
9.8
7.606
11.63
48538811
2019年7月5日
159
约定的梦幻岛
9.8
7.654
10.45
49401260
2019年1月10日
60
少女终末旅行
9.8
8.001
4.00
29103828
2017年10月6日
230
Megalo Box
9.8
7.457
16.35
39774790
2018年4月5日
108
宝石之国
9.8
7.809
7.38
56474833
2017年10月7日
488
魔卡少女樱 透明牌篇
9.8
7.043
33.93
51432035
2018年1月7日
203
齐木楠雄的灾难 第二季
9.8
7.518
14.15
81010663
2018年1月16日
29
比宇宙更远的地方
9.8
8.234
1.99
17111562
2018年1月2日
97
月色真美
9.8
7.852
6.55
49090357
2017年4月6日
422
剑网3·侠肝义胆沈剑心
9.8
7.129
30.36
74053515
2018年9月21日
340
街角魔族
9.8
7.282
23.12
25037979
2019年7月11日
121
终将成为你
9.8
7.757
8.23
17766674
2018年10月5日
316
非人哉
9.8
7.336
21.03
266252297
2018年3月29日
394
风灵玉秀
9.8
7.211
26.31
14756338
2017年4月1日
91
少女☆歌剧 Revue Starlight
9.8
7.871
6.14
12244034
2018年7月12日
612
索玛丽与森林之神
9.8
6.818
43.60
14488526
2020年1月9日
335
神推偶像登上武道馆我就死而无憾
9.8
7.298
22.44
10146270
2020年1月10日
376
请吃红小豆吧!
9.8
7.254
24.19
53938485
2018年7月5日
4
3月的狮子 第二季
9.8
8.816
0.19
10668551
2017年10月14日
485
邻家的吸血鬼小妹
9.8
7.037
34.20
20233778
2018年10月5日
206
夏目友人帐 陆
9.8
7.528
13.81
37366405
2017年4月11日
当然,它们在专业评分网站的评分也不会低,在b站拿到评分前100名的番剧,在bangumi平均排前13%,但是相比而言b站评分过于集中,缺乏区分度。
相比而言,在bangumi拿到前100名的番剧,在b站平均只能排在前30%。很多老番在各个方面和新番有的一拼,却没有新番的排面,热度低倒是正常,可是评分都排不上第一梯队。
这不但是好番不火的体现,同时也反映了另一个问题。
我们作出两个网站不同年份动画平均排名折线图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 db['biliprank' ]=db.bilirating.rank(ascending=False ).values db.biliprank=db.biliprank.apply(lambda x: x/len (db)) db['bgmprank' ]=db.bgmrating.rank(ascending=False ).values db.bgmprank=db.bgmprank.apply(lambda x: x/len (db)) fig = plt.figure(num=110 ,figsize=(6 ,4 ),dpi=200 ,facecolor='white' ) ax = plt.gca() ax.invert_yaxis() db.groupby(by='年度' ).bgmprank.mean()[22 :].plot(label='bangumi' ) db.groupby(by='年度' ).biliprank.mean()[22 :].plot(label='bilibili' ) ax.axhline(y=0.45 ,c='red' ,ls='--' ,lw=0.3 ) ax.axvline(x=2012.3 ,c='green' ,ls=':' ,lw=1 ) ax.set (title='两站各年度动画平均评分变化' ,ylabel='排名' ,yticks=[0.2 ,0.3 ,0.4 ,0.5 ,0.6 ] ,yticklabels=['前20%' ,'前30%' ,'前40%' ,'前50%' ,'前60%' ],xticks=np.arange(2001 ,2020 ,3 )) ax.legend()
可以提取出两个特征:
总体来说两站基本趋势相同,说明动画业界衰退态势明显,近十年来作品平均排名在后半部分徘徊;
b站对于2012年之后的动画作品排名始终高于bgm,尤其是近几年的新番排名明显偏高,相对来说,老番的排名则偏低,这充分印证了上文提到的新番压制老番的情况。
就这些情况,推测如下:
作品年龄与其观众的年龄是成正比的
而且是观众年龄越大,评价质量就越高,在一定程度上也就意味这给出5星的概率越低
不同年龄段评价标准的差异影响了新番和旧番评分情况,同时也与好番不火情况有关。
4.6.5 总结
基于上述分析和事实,我总结了B站评分不正常分布产生的原因:
评分缺乏基本的指导
在bangumi评分时,会从1星到10星分别提示
不忍直视-很差-差-较差-不过不失-还行-推荐-力荐-神作和超神作,并且还会提示评分者谨慎评价。
虽然只有这几个字的建议,但这能够在很大程度上促使评分者谨慎思考。
而回过头看b站的评分环境,除了令人迷惑的“发表五星评价需扣除一枚硬币”之外别无他物。
评分没有限制
bangumi在评分时首先要点击“看过”才能评分。虽然说这种形式上的限制可能没什么作用,但相比之下,B站作为一个提供视频源的网站,居然不用看番就可以评分,这极大降低了评分的门槛,严重降低了评分的可信度,而且我认为b站对于投五星需扣除1硬币这种操作荒谬至极,如果b站希望通过评分扣硬币这种方式促使点评者谨慎评价,那么应当是投任何分数都需要硬币,而且至少2个。
评分标准存在问题
首先对于平台来说,评分机制缺乏指导,过于模糊,而对于用户而言,发表的评价质量也不高,往往非常片面,并且用户接受度的影响较大。但是另一方面,由于所有作品的评分和点评都是公开可见的,在评分时固然会受到已有评价的影响。有些人看起来很有主见,实际上很容易被带节奏,改变自己的想法。这一方面表示对一部作品没有自己的理解,没有形成明确的观点,另一方面也是从众心理的体现。
所以我提出两条建议:
对平台而言希望b站能提供基本的评分指导;
对用户来说,希望在评分时能够做到冷静、谨慎。
5 结语
本文从爬虫入手,爬取bilibili和bangumi网站的动画作品数据,对动画作品进行了一些数据分析,了解了近年来动画行业的发展趋势,并且通过分析b站评分数据并将其与专业评分网站bangumi比较,发现
与专业评分网站相比,b站评分的参考作用存在但有限
b站评分分布异常,区分度不大,佳作被埋没
点评者们对评分标准把握出现偏差,过度追捧新番
b站评分机制不完善,缺乏限制和指导
出于时间和能力原因,很多分析并不全面,甚至可能导致结论错误。接下来的工作便是优化代码,并对数据进行更深入的分析。