哔哩哔哩(www.bilibili.com,英文名称:bilibili,简称B站)现为中国年轻世代高度聚集的文化社区和视频平台,该网站于2009年6月26日创建。
B站早期是一个ACG(动画、漫画、游戏)内容创作与分享的视频网站。经过十年多的发展,围绕用户、创作者和内容,构建了一个源源不断产生优质内容的生态系统,B站已经涵盖7000多个兴趣圈层的多元文化社区。 哔哩哔哩作为目前国内最大的动画作品平台,已上线了3000多部来自日本、美国以及国内的动画作品,具有大量的播放、点赞、弹幕、评分等数据可供分析。
bangumi(bangumi番组计划,bangumi.tv)是专注于ACG领域的网站,是国内专业的动画评分网站。该网站可看作动画作品的数据库,拥有万余部动画作品的详细数据,包括集数、播放时间、监督以及评分、评分人数等信息等可供分析。
首先打开bangumi首页,并登录。登录后刷新页面,并用fiddler抓包,获取请求头: 打开一个需要爬取的动画作品页面,需要爬取的信息有5部分:
作品原名与类型作品详细信息作品简介作品tags作品评分数据检查源代码,找到各部分对应的标签区块:
part1part2
part3
part4
part5
获得对应的源代码位置后,便可以用beautifulsoup包对网页html进行解析获取数据了。
目前的问题是如何获取尽量多的作品数据。
根据网页地址,访问某部作品的页面应为bangumi.tv/subject/…(后面的数字称为subject号),所以可以从1开始遍历所有的subject号,这理论上可行,但实际操作中发现了两个问题,一是subject号目前超过20万,全部遍历所需时间太长;二是并不是所有作品都是动画作品,还可能是书籍、音乐、游戏等:
例:漫画
例:专辑
所以必须找到其他方法。注意到bangumi作为评分网站具有排行榜功能:
该排行榜收录了所有评分人数达到最低评分人数的动画,默认按照评分从高到低排序。截至2020年6月26日,共有5831部动画在榜。并且榜单分为243页,全部可以直接访问爬取subject号:
不需通过ajax请求获取某段排行的数据,这对于爬虫是非常友好的。
考虑到能上榜的作品都具有一定人气,并且只有评分人数达到一定数量评分才更有代表性,所以决定按照排行榜爬取这5800多部动画作品subject号,再访问各自的页面获取详细信息。
初步清洗后的数据格式如下:
votes原名类型封面tagsratingratings话数简介中文名...主题歌作曲主题歌作词开始结束片长主题歌编曲第二原画音响特效机械设定subject2537016カウボーイビバップTVhttps://lain.bgm.tv/pic/cover/l/c2/4c/253_t3XW...渡边信一郎 2293 菅野洋子 2196 星际牛仔 1529 经典 1139 SUNRISE...9.143['3325', '2252', '977', '287', '88', '37', '9'...262021年,随着超光速航行技术的实现,人类得以在太阳系范围内方便的自由移动,但...星际牛仔...菅野よう子NaNNaNNaNNaN菅野よう子NaNNaN長谷川敏生山根公利3263968攻殻機動隊 S.A.C. 2nd GIGTVhttps://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...菅野よう子OrigaNaNNaNNaN菅野よう子NaNNaN村上正博常木志伸、寺岡賢司3244896攻殻機動隊 STAND ALONE COMPLEXTVhttps://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...菅野よう子OrigaNaNNaNNaN菅野よう子NaNNaN遠藤誠、村上正博寺岡賢司、常木志伸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页面获取作品开播日期和话数—爬取封面图。
初步清洗后的数据格式如下:
titletype_nameseason_idareamedia_idratingraterscoverfollowseries_followviewscoinsdanmakustagsdateepisodes简介160散华礼弥番剧710日本7109.48016http://i0.hdslb.com/bfs/bangumi/6dccd70827dd5f...11247071124705438432338564255295奇幻 日常 治愈2012-4-54毫无特色的少年降谷千纮,就读于县立紫阳高校一年级,是个非常喜爱僵尸的人。少女散华礼弥是散华家...161恋爱随意链接番剧713日本7139.59269http://i0.hdslb.com/bfs/bangumi/8274f1107032a6...730312730313320632233427219865日常 少女 校园 小说改2012-7-77故事发生在私立山星高中,这所学校的文化研究部内,八重樫太一、永濑伊织、稻叶姬子、桐山唯、...162猫物语(黑)番剧723日本7239.71926http://i0.hdslb.com/bfs/bangumi/d24e532a91234b...433136433079811255416212484奇幻 声控 小说改 神魔2012-12-314黄金周的第一天,阿良良木历和班长羽川翼一起埋葬了一只被车碾过,没有尾巴的猫。这本应是一件的普... 封面图下载 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)对于bilibili的日期格式:
bilidb.date 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格式:
bilidb.date=pd.to_datetime(bilidb.date,format='%Y-%m-%d',errors='coerce').copy() bilidb.date 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:
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 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,由于多集动画和单集电影的播出时间分处于放送开始和上映年度两个键中,故需将其合并后处理。
放送开始上映年度01998年10月23日NaN12004年1月1日NaN22002年10月1日NaN32008年10月2日NaN41995年10月4日NaN5NaN1995年11月18日6NaN1997年7月19日7NaNNaN82009年4月5日NaN92017年10月14日NaN # 新建一列,将上映年度和放送开始看做同一类型 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) 放送开始上映年度日期年度01998年10月23日NaN1998-10-23199812004年1月1日NaN2004-01-01200422002年10月1日NaN2002-10-01200232008年10月2日NaN2008-10-02200841995年10月4日NaN1995-10-0419955NaN1995年11月18日1995-11-1819956NaN1997年7月19日1997-07-1919977NaNNaNNaT082009年4月5日NaN2009-04-05200992017年10月14日NaN2017-10-142017需要将评分人数不足的作品的评分及评分人数从0改为nan,以便后面进行剔除:
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)即在3708部作品中,有2581部有评分,且平均分为9.12分,中位数为9.5分(共431部作品)。
频数分布直方图、正态分布曲线 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)可以看出评分的分布与正态分布相差较大。
票均平均分 (bilidb.raters.dropna().sum(), (bilidb.rating.dropna()*bilidb.raters.dropna()).sum()/bilidb.raters.sum()) (14881442.0, 8.987174381353636)求得总评分数超过1488万,票均评分为8.99分。
即在5830部作品中,平均分为6.63分,中位数为6.69分。
频数分布直方图、正态分布曲线 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)可以看出评分的分布与正态分布相当吻合。
票均平均分 bgmdb.votes.sum(),(bgmdb.rating*bgmdb.votes).sum()/bgmdb.votes.sum() (4525600, 7.185151881518473)求得总评分数超过452万,票均评分为7.19分。
选择其中平均评分最高的10个动画公司:
studios_counts_ratings.sort_values(by=['rating'],ascending=False).head(10) ratingsubject动画制作SHAFT7.43405060京都アニメーション7.36771062Production I.G7.176925133ufotable7.17013936サンライズ7.106657178MADHOUSE7.092288156シンエイ動画7.06785187BONES7.02164089東映アニメーション6.929124186オー・エル・エム6.90226549可以看到,SHAFT、京都动画、Production I.G等动画公司出产的作品平均质量较高,这与许多动画爱好者的观点是一致的。
热门作品
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) 20002001200220032004200520062007200820092010201120122013201420152016201720182019热度第1名犬夜叉千与千寻攻壳机动队 STAND ALONE COMPLEX钢之炼金术师混沌武士NaNCode Geass 反叛的鲁路修CLANNADNaN化物语NaN魔法少女小圆冰菓进击的巨人白箱吹响!悠风号你的名字。小林家的龙女仆紫罗兰永恒花园辉夜大小姐想让我告白~天才们的恋爱头脑战~热度第2名名侦探柯南 瞳孔中的暗杀者棋魂全金属狂潮全金属狂潮 校园篇哈尔的移动城堡虫师凉宫春日的忧郁秒速5厘米龙与虎钢之炼金术师 FULLMETAL ALCHEMIST凉宫春日的消失命运石之门刀剑神域我的青春恋爱物语果然有问题月刊少女野崎君一拳超人Re:从零开始的异世界生活来自深渊青春笨蛋少年不做兔女郎学姐的梦进击的巨人 第三季 Part.2热度第3名魔卡少女樱 被封印的卡片星际牛仔 天国之扉火影忍者东京教父攻壳机动队 S.A.C. 2nd GIG灼眼的夏娜死亡笔记幸运星Code Geass 反叛的鲁路修R2轻音少女无头骑士异闻录NaN男子高中生的日常某科学的超电磁炮SFate/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轻音少女 第二季日常心理测量者打工吧!魔王大人四月是你的谎言我的青春恋爱物语果然有问题 续灵能百分百进击的巨人 第二季佐贺偶像是传奇约定的梦幻岛高分作品
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)) 20002001200220032004200520062007200820092010201120122013201420152016201720182019评分第1名第一神拳千与千寻攻壳机动队 STAND ALONE COMPLEX百变之星攻壳机动队 S.A.C. 2nd GIG虫师银魂天元突破 红莲螺岩NaN钢之炼金术师 FULLMETAL ALCHEMIST凉宫春日的消失银魂'银魂' 延长战歌牌情缘2白箱水星领航员 The AVVENIRE排球少年 乌野高校 VS 白鸟泽学园高校3月的狮子 第二季莉兹与青鸟进击的巨人 第三季 Part.2评分第2名吸血鬼猎人D:妖杀行星际牛仔 天国之扉小魔女DoReMi 大合奏钢之炼金术师攻壳机动队2 无罪蜂蜜与四叶草蜂蜜与四叶草IICLANNAD水星领航员 第三季福音战士新剧场版:破四叠半神话大系命运石之门爆漫王。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的奇妙冒险 黄金之风灵能百分百 第二季 各年度动画作品进入排行榜前100的数量 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)) 年度 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年,优质作品出现井喷现象,但近几年来优质作品减少趋势明显。
各年度动画作品前10名评分走势 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均分20007.745620018.001920028.024120038.115920048.289020058.190220068.347020078.265620088.319820098.201220108.186620118.305720128.161220138.066120148.365820158.228520168.055220178.167820188.114020198.1075 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))注意到bilibili的搜索功能,可以搜索到对应作品的md号:
故采取以下的匹配策略:
导入bangumi的数据按照日文原名和中文名向search.bilibili.com发出查询请求从查询结果中提取番剧链接(md号)根据名称、年份等信息进行匹配,得到匹配接口(subject-md)接口格式:
{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'], ...}首先,过滤掉没有搜索结果的键值对:
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信息对接,生成新的字典
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 [{'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列表:
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 copy fullymatched=copy.deepcopy(matched)然后,寻找名称不同但开播日期相同的作品,进行人工匹配,结果保存在manuallymatched中:
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格式并合并去重,得到最终匹配结果:
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) (1837, subject md 0 326 1565 1 324 1564 2 876 1178 3 1428 1089 4 211567 6445)最终匹配到了1837部作品。
使用匹配接口,将两个DataFrame合并。首先将接口与bilidb按md号内连接,然后再用bilidb与bgmdb按subject号内连接,并进行一系列调整,最终获得合并后的数据库:
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) subjecttitletype_nameseason_idareamedia_idbiliratingraterscoverfollow...开始结束片长主题歌编曲第二原画音响特效机械设定日期年度0326攻壳机动队 S.A.C. 2nd GIG番剧1565日本15659.82045.0http://i0.hdslb.com/bfs/bangumi/00ee95c464defb...152857...NaNNaNNaN菅野よう子NaNNaN村上正博常木志伸、寺岡賢司2004-01-0120041324攻壳机动队 STAND ALONE COMPLEX番剧1564日本15649.83511.0http://i0.hdslb.com/bfs/bangumi/6ebd07ba376115...516939...NaNNaNNaN菅野よう子NaNNaN遠藤誠、村上正博寺岡賢司、常木志伸2002-10-0120022876CLANNAD ~AFTER STORY~番剧1178日本11789.942904.0http://i0.hdslb.com/bfs/bangumi/54003a09e72f0d...917778...NaNNaNNaNANANT-GARDE EYESNaNNaNNaNNaN2008-10-02200831428钢之炼金术师 FULLMETAL ALCHEMIST番剧1089日本10899.980126.0http://i0.hdslb.com/bfs/bangumi/401f84cadca354...1990896...NaNNaNNaN大橋卓弥、常田真太郎関本美穂テクノサウンド龍角里美、池上真崇鈴木雅久2009-04-05200942115673月的狮子 第二季番剧6445日本64459.815230.0http://i0.hdslb.com/bfs/bangumi/14cf90e4ea9a05...480677...NaNNaNNaNNaN谷口工作、吉澤翠NaNNaNNaN2017-10-142017拟合得到bili=0.80bgm+3.70,相关系数为0.74,决定系数为0.54,即两站评分呈现正相关关系,且bilibili分数的变化的一半可用bangumi分数变化来解释。
普通散点图按原始数据作散点图和趋势线:
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)但无论从图上看,还是从相关系数上看,两者的相关性存在,但不是很高。
由于bangumi的评分精确到小数点后三位,相同评分的作品很少,普通的散点图对分布情况的展示效果不佳。故尝试作气泡图、二维频次直方图与三维柱状图增强数据直观性。
作气泡图首先要将bangumi的评分的分辨率降至0.1分,然后建立数据交叉表:
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:] bilirating8.48.58.68.78.88.99.09.19.29.39.49.59.69.79.89.9bgmrating_2digit5.410001002110001005.511120220011010005.610121001000101005.722112120020210005.804210021212110005.901022222221000006.011222424160110006.111323135326510006.201151474554450006.310104254735652106.401100171825441006.50112121510714746006.600111433711107135106.70000200225611135106.800002113438947406.91000110233610313607.00010110135761321307.10000010214831011817.210000101357161313807.300010011124101015717.400010001125212171007.50000000120241011907.6000000101230515727.70000010000342101007.8000000000105391027.9000000001003110738.0000000000201051018.100000000000012508.200000000000026828.301000000000210118.400000000000100408.500000000000002008.600000000000000108.700000000000000108.800000000000000119.000000000000000119.10000000000000010接着对每一个点分别作图,实现气泡图的效果:
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函数构造二维频次直方图,附带标尺:
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分以上。
from mpl_toolkits.mplot3d import Axes3D import matplotlib as mpl fig = plt.figure(num=108,figsize=(8,4),dpi=300,facecolor='white') ax=plt.subplot(111,projection='3d') x = bb.index.to_list() #bilibili评分 x_index=range(len(x)) y = bb.columns.to_list()#该bilibili评分对应的bangumi评分序列 y_index=range(len(y)) for i in x_index:#对于每个bilibili评分 z = bb.iloc[i,y_index] ax.bar(y, z, x[i],zdir='x',width=0.2)#以bangumi评分序列为底轴作图 ax.set(title='三维分布图',xlabel='bangumi',ylabel='bilibili')比较两网站的片均评分和中位数,可以看到bangumi两者差距很小,而b站平均分明显小于中位数。
bilibilibangumi片均评分9.125926.633856中位数9.500006.691500平均数小于中位数,意味着存在许多低分作品,且没有与之数量相当的高分作品。 我们可以通过箱线图更直观地展示两站评分的这种差异。
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站评分产生严重的拖尾,导致其片均评分明显小于中位数。
通过bilibili的评分频数分布直方图,可以看到作品评分在高分段扎堆,呈现的趋势基本上是分数越高,作品越多。9.7分就有378部,占到了全部有评分动画的五分之一,严重地丧失了区分度。并且,从箱线图中也能看出b站低评分很多导致片均评分低于中位数。
一般来说,作品评分极高和极低的作品数都应该很少,绝大多数作品评分应当集中在平均值左右(即正态分布)。很显然,b站的评分分布严重偏离了正态分布。
相比于bilibili,bangumi的频数分布直方图呈现出两头低,中间高,左右对称的特点,相应的正态分布曲线与实际分布高度吻合。而且bangumi的分数集中度也较低,在6.6-6.7区间也只有266部动画,占比只有二十分之一多一点。所以至少从统计学规律上说,bangumi这个网站的评分更有参考意义。
与bangumi的对比告诉我们,b站的评分数据存在诸多异常之处。
既然分布很异常,那么b站评分到底代表了什么?产生这种分布的原因是什么?笔者尝试通过数据给出一些合理推断。
相对人气指数我们需要回来关注另一组数据,那就是片均评分和人均评分。
bilibilibangumi片均评分9.2115886.633856票均评分9.0621147.185152b站的片均评分高于票均评分,而bangumi的片均评分高于票均评分。
这说明了什么呢?首先我们知道,点评数与热度成正比。那么
当片均高于票均时,意味着高评分动画的评分人数较少,出现了好番不火的情况;当片均低于票均时,意味着低评分动画的评分人数较少,出现了烂番没人看的情况。显然,后者更符合常理。
现在,我们用数据说话,用具体数字表达“好番不火”或“烂番没人看”的程度。
好番是要和烂番作对比的,所以我们定义一个函数,称为相对人气指数,在给出百分比累积排名x的情况下, 相对人气指数的定义为:
该比值表示好番热度与同等程度的烂番热度之比。
并且,累积排名越高,则表示排名越靠前,而且如果好番和热度成正比,这个相对人气指数应当随累积排名增大而增大,是一个单调递增函数。
我们将相对人气指数对累积排名作图:
# 分别计算两网站的相对人气指数 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.97.9664.611177881912020年4月11日150擅长捉弄的高木同学 第二季9.97.66910.02437943822019年7月7日31强风吹拂9.98.2411.87249237122018年10月2日101妖精森林的小不点9.97.8526.55196217092018年1月12日208鬼灭之刃9.87.51514.254757582522019年4月6日39JOJO的奇妙冒险 黄金之风9.88.1062.932883609982018年10月5日105辉夜大小姐想让我告白~天才们的恋爱头脑战~9.87.8177.151947173532019年1月12日274刺客伍六七9.87.38619.042491050112018年4月25日254青春笨蛋少年不做兔女郎学姐的梦9.87.41617.961396283052018年10月3日187某科学的超电磁炮T9.87.58312.13962562232020年1月10日398碧蓝之海9.87.20526.50663907302018年7月13日174女高中生的无所事事9.87.60611.63485388112019年7月5日159约定的梦幻岛9.87.65410.45494012602019年1月10日60少女终末旅行9.88.0014.00291038282017年10月6日230Megalo Box9.87.45716.35397747902018年4月5日108宝石之国9.87.8097.38564748332017年10月7日488魔卡少女樱 透明牌篇9.87.04333.93514320352018年1月7日203齐木楠雄的灾难 第二季9.87.51814.15810106632018年1月16日29比宇宙更远的地方9.88.2341.99171115622018年1月2日97月色真美9.87.8526.55490903572017年4月6日422剑网3·侠肝义胆沈剑心9.87.12930.36740535152018年9月21日340街角魔族9.87.28223.12250379792019年7月11日121终将成为你9.87.7578.23177666742018年10月5日316非人哉9.87.33621.032662522972018年3月29日394风灵玉秀9.87.21126.31147563382017年4月1日91少女☆歌剧 Revue Starlight9.87.8716.14122440342018年7月12日612索玛丽与森林之神9.86.81843.60144885262020年1月9日335神推偶像登上武道馆我就死而无憾9.87.29822.44101462702020年1月10日376请吃红小豆吧!9.87.25424.19539384852018年7月5日43月的狮子 第二季9.88.8160.19106685512017年10月14日485邻家的吸血鬼小妹9.87.03734.20202337782018年10月5日206夏目友人帐 陆9.87.52813.81373664052017年4月11日当然,它们在专业评分网站的评分也不会低,在b站拿到评分前100名的番剧,在bangumi平均排前13%,但是相比而言b站评分过于集中,缺乏区分度。
相比而言,在bangumi拿到前100名的番剧,在b站平均只能排在前30%。很多老番在各个方面和新番有的一拼,却没有新番的排面,热度低倒是正常,可是评分都排不上第一梯队。
这不但是好番不火的体现,同时也反映了另一个问题。
不同年份动画平均排名我们作出两个网站不同年份动画平均排名折线图:
# 增加bili百分比排名和bgm百分比排名列 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()# 将y轴反向 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星的概率越低
不同年龄段评价标准的差异影响了新番和旧番评分情况,同时也与好番不火情况有关。
基于上述分析和事实,我总结了B站评分不正常分布产生的原因:
评分缺乏基本的指导在bangumi评分时,会从1星到10星分别提示 不忍直视-很差-差-较差-不过不失-还行-推荐-力荐-神作和超神作,并且还会提示评分者谨慎评价。
虽然只有这几个字的建议,但这能够在很大程度上促使评分者谨慎思考。
而回过头看b站的评分环境,除了令人迷惑的“发表五星评价需扣除一枚硬币”之外别无他物。
评分没有限制bangumi在评分时首先要点击“看过”才能评分。虽然说这种形式上的限制可能没什么作用,但相比之下,B站作为一个提供视频源的网站,居然不用看番就可以评分,这极大降低了评分的门槛,严重降低了评分的可信度,而且我认为b站对于投五星需扣除1硬币这种操作荒谬至极,如果b站希望通过评分扣硬币这种方式促使点评者谨慎评价,那么应当是投任何分数都需要硬币,而且至少2个。
评分标准存在问题首先对于平台来说,评分机制缺乏指导,过于模糊,而对于用户而言,发表的评价质量也不高,往往非常片面,并且用户接受度的影响较大。但是另一方面,由于所有作品的评分和点评都是公开可见的,在评分时固然会受到已有评价的影响。有些人看起来很有主见,实际上很容易被带节奏,改变自己的想法。这一方面表示对一部作品没有自己的理解,没有形成明确的观点,另一方面也是从众心理的体现。
所以我提出两条建议:
对平台而言希望b站能提供基本的评分指导;对用户来说,希望在评分时能够做到冷静、谨慎。本文从爬虫入手,爬取bilibili和bangumi网站的动画作品数据,对动画作品进行了一些数据分析,了解了近年来动画行业的发展趋势,并且通过分析b站评分数据并将其与专业评分网站bangumi比较,发现
与专业评分网站相比,b站评分的参考作用存在但有限
b站评分分布异常,区分度不大,佳作被埋没
点评者们对评分标准把握出现偏差,过度追捧新番
b站评分机制不完善,缺乏限制和指导
出于时间和能力原因,很多分析并不全面,甚至可能导致结论错误。接下来的工作便是优化代码,并对数据进行更深入的分析。