精华内容
下载资源
问答
  • BiLiBiLi爬虫

    2020-11-06 19:52:00
    BiLiBiLi Time: 2020年11月6日19:44:58 Author: Yblackd @目录BiLiBiLi介绍软件架构安装教程使用说明源码下载 BiLiBiLi 介绍 b站视频详情数据抓取,自动打包并发送到指定邮箱(单个或者群发) 软件架构 定时任务:...
    • BiLiBiLi
    • Time: 2020年11月6日19:44:58
    • Author: Yblackd

    @


    BiLiBiLi

    介绍

    b站视频详情数据抓取,自动打包并发送到指定邮箱(单个或者群发)

    软件架构

    • 定时任务:采用win自带或者time.sleep()
    • 采用python自动化测试(selenium),获取b站动态生成源码,
    • 采用selenium 和 bs4 对page_source进行规则匹配清洗。
    • 清洗完数据写入json和Excel文件(有json数据转换Excel函数)--按照日期保存;
    • 按日期将文件夹压缩zip
    • 将压缩后的zip发送给指定的 邮箱或者邮箱群组

    安装教程

    1. 更新浏览器版本(Chrome, Firefox, Edge等,方便找对应的webDriver); 自行百度(浏览器名 webdriver),第一个推介点进去搜索对应的 浏览器版本下载(相同最后,没有尽量选择相近)
    2. 安装python,和pip
    3. pip install -r requirments.txt,安装依赖,如果安装失败,就手敲吧,一般问题不大
    4. 按照使用说明更改必要参数

    使用说明

    1. run.py:

      • 修改up_user_name = "立体设计师峥嵘" # up主名: 更改你自己要抓取的up主名称

      • 发送邮件参数

        mail_cfg = {
            # 邮箱登录设置,使用SMTP登录
            'server_username': "xxx@qq.com",  # '你的邮箱'
            'server_pwd': "xxxxx",  # QQ和163邮箱需要:'16位随机码', QQ企业邮箱你的登录密码
        
            # 邮件内容设置
            'msg_to': ['xxx@qq.com', 'xxx@163.com'],  # 可以在此添加收件人单个,多个群发
            'msg_subject': u'日期:' + num_ct,
            'msg_date': email.utils.formatdate(),
            'msg_content': u"正文: BiLiBiLi视屏详情抓取--数据文件, 抓取时间:" + timestr,
        
            # 附件
            'attach_file': target
        }
    2. 如果只是修改上面说明参数,运行应该问题不大,关键就是webdriver的配置:下载好后不用添加环境变量,只要记录对应位置,代码里面声明就好;邮箱发送功能记得开启SMTP/POP

    3. 如果还是有问题,留言评论好了

    源码下载

    下载链接:

    展开全文
  • bilibili爬虫

    2019-05-24 08:49:14
    https://m.bilibili.com/video/av39053212.html ,通过正则匹配获取所有数据,该页面基本可以获取需要的数据,但是有几个数据有些问题 弹幕数:该视频为合集的情况下,获取的弹幕数是所有子集的弹幕数,而不是...

    一、背景需求

    1、抓取内容:给定关键词,并指定分区,获取该分区下的所有视频,同时,该视频如果是一个合集中的子集视频,则抓取该合集的所有视频
    2、抓取字段:作者、标题、分区、详情页链接、发布时间、封面图、下载链接、标签、播放量、点赞量、评论量、弹幕量、收藏量

    二、需求分析

    1、app端分析

    抓包后发现,搜索页的接口没有进行加密,将指定的分区映射成对应的分区id,结合关键词,可以构建请求url,获取数据
    详情页数据,请求参数中有加密字段,pass

    2、手机端H5页面

    通过app端搜索页获取的数据,可以构造手机端H5页面的详情页请求链接,如 https://m.bilibili.com/video/av39053212.html ,通过正则匹配获取所有数据,该页面基本可以获取需要的数据,但是有几个数据有些问题

    • 弹幕数:该视频为合集的情况下,获取的弹幕数是所有子集的弹幕数,而不是每个子集的弹幕数
    • 封面图:合集时,所有子集封面图都一样
    • 下载链接:合集时,需要另外请求其它子集的下载链接

    通过点击其它子集视频,可以找到获取下载链接的ajax请求,获取下载链接

    • 下载链接无法在电脑端H5页面打开,手机端H5页面有时打不开,但是将链接发到微信上,是都可以打开的
    • 下载链接有时效性,大概2小时就会过期
    • 通过手机端H5获取的下载链接,清晰度过低,无法满足需求

    3、电脑端H5页面

    • 防盗链:通过抓包同样找到下载链接的ajax请求,但是获取的下载链接无论在H5页面,还是微信上,都是403错误,一开始以为获取的下载链接有问题,最后通过代码下载该视频时,发现headers中必须加上referer字段才能请求成功
    • 音频视频分离:获取下载链接后,下载视频,打开后有些没有声音,观察接口返回数据,发现有两个下载链接的列表,一个是video, 一个是audio,也就是音频和视频分别是两个链接,解决办法:
      • 1、过滤音视频分离的视频:该接口返回的下载链接有两种形式,一种是音视频分离的,另一种是正常的视频,最终过滤了大概65%左右的视频
      • 2、将音视频合并:ffmpeg -i 55.mp3 -i 55.mp4 output2.mp4, 这种合成太占用cpu,且目前下载部分不支持传递两个链接进行下载拼接,故pass

    三、整体流程

    采用scrapy-redis实现分布式,构造两个爬虫

    1、搜索页爬虫:

    • 目的:获取所有符合要求的视频ID
    • 流程:通过关键词+分区,构造请求,将获取的视频id存入详情页爬虫的消费队列

    2、详情页爬虫:

    • 目的:获取视频的信息
    • 流程:从redis中获取视频id,构造请求,获取数据,并通过视频id及子集视频对应的id,构造下载链接的请求,获取下载链接

    数据存储在mongo数据中,并通过数据库进行数据去重

    四、总结

    • 下载链接:时效性,低清晰度视频只能在微信中打开,高清晰度视频存在防盗链,音视频分离问题
    • 抓取流程:app端搜索页数据->手机端H5详情页数据->电脑端H5下载链接
    展开全文
  • SAE 部署 bilibili 爬虫

    2018-04-17 15:54:00
    title: sae部署bilibili爬虫 categories: python tags: spider sae 前端效果 sae准备工作 从本地上传的爬虫到sae一直被提示没有requests模块,在requirements.txt里声明了也不行。起初我以为是这个第三方包被屏蔽...

    layout: post
    title: sae部署bilibili爬虫
    categories: python
    tags: spider sae

    前端效果

    pic1

    sae准备工作

    从本地上传的爬虫到sae一直被提示没有requests模块,在requirements.txt里声明了也不行。起初我以为是这个第三方包被屏蔽了,直到看到sae支持中心-Python共享服务器-运行环境才直到原因:requirements.txt 只在容器云app里面才会生效,同理 runtime.txt 也是。因此需要在共享服务器上面使用第三方包的话,只能自己上传。我通过 ubantu python 2.7.6 使用pip install -t vendor requests安装上 requests 包,再将这个文件夹上传到 sae 根目录就可以使用了。因为涉及到我自己账号的 cookie,这里我使用的是码云私有仓库保存我的代码,不再贴出。

    bilibili-Getcoin

    B站只要登录一下就可以获得当日的一硬币,在爬虫界这算是非常简单的了。我之前尝试用国外的某ae来爬B站,得到503错误(403?),国外访问B站是要梯子的。
    code:

    # -*- coding: utf-8 -*-
    """
    requests学习实战
    """
    import requests
    url = 'https://account.bilibili.com/site/getCoin'
    
    headers = {}
    cookies = {}
    with requests.Session() as s:
    r = s.get(url,headers=headers,cookies=cookies)
    print r.status_code
    bjson = r.json()
    #print bjson.keys()
    print bjson[u'data'] 
    
    print 'over!'

    以上。

    2018/9/3 更新

    cookies 需要每月更新。本月更新后无法获取硬币了,对比之前的cookies,发现本次更新后多出一个_jct键。现在删除后等待观察明天的结果。

    结果仍然是不能获取。

    转载于:https://www.cnblogs.com/aubucuo/p/spider6.html

    展开全文
  • bilibili爬虫+数据分析

    千次阅读 2020-07-05 08:43:34
    3. 基于urllib的bangumi和bilibili一键爬虫脚本的编写 3.1 bangumi网站分析及爬虫脚本的编写 3.1.1 网站分析 3.1.2 代码实现 3.2 bilibili网站分析及爬虫脚本的编写 3.2.1 网站分析 3.2.2 代码实现 4. 基于...

    Python爬虫+数据分析+数据可视化实战

    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部分:

    1. 作品原名与类型
    2. 作品详细信息
    3. 作品简介
    4. 作品tags
    5. 作品评分数据


    检查源代码,找到各部分对应的标签区块:

    • 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. 模块的导入
    import numpy as np
    import pandas as pd
    
    from bs4 import BeautifulSoup as bs
    import urllib.request as ur
    import urllib.parse as up
    import urllib.error as ue
    import http.cookiejar as hc
    
    import re
    import gzip
    import json
    
    import time
    import os
    import socket
    
    os.chdir('...')
    socket.setdefaulttimeout(30)
    
    
    1. 总榜subject号爬取
    # 设置请求头
    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'
    }
    # 创建cookiejar对象
    cj=hc.CookieJar()
    # 根据cookiejar创建handler对象
    hl=ur.HTTPCookieProcessor(cj)
    # 根据handler创建opener对象
    opener=ur.build_opener(hl)
    
    # 设定字符匹配模式将subject号匹配
    pattern=re.compile(r'li id="item_(\d+)"')
    # 储存subject号的列表
    subjectlist=[]
    
    # 更新cookie的函数(反爬机制之一)
    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]
    
    # 爬取subject号的函数
    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. 详细信息爬取
    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. 数据初步清洗
    # 读取获得的原始数据
    bgmfulldb=pd.read_json(r'bangumi\bgmdb_'+date+'.json')
    # 选取非空值数最少的60个键,将键值对复制到新的列表中(进行了初步清洗)
    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)
    # 转化成DataFrame格式并转存为csv格式文件
    bgmdb2=pd.DataFrame(bgmdb2,columns=indexs)
    bgmdb2.to_csv(r'bangumi\bgmdb_'+date+'.csv',index=False)
    

    初步清洗后的数据格式如下:

    votes原名类型封面tagsratingratings话数简介中文名...主题歌作曲主题歌作词开始结束片长主题歌编曲第二原画音响特效机械设定
    subject
    2537016カウボーイビバップ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遠藤誠、村上正博寺岡賢司、常木志伸

    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. 模块的导入
    import numpy as np
    import pandas as pd
    
    from bs4 import BeautifulSoup as bs
    import urllib.request as ur
    import urllib.parse as up
    import urllib.error as ue
    import http.cookiejar as hc
    
    import re
    import gzip
    import json
    
    import time
    import os
    import socket
    
    os.chdir(r'...')
    socket.setdefaulttimeout(30)
    
    1. 配置爬虫条件
    # 设置请求头
    # api请求头
    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'
    }
    # 创建cookiejar对象
    cj=hc.CookieJar()
    # 根据cookiejar创建handler对象
    hl=ur.HTTPCookieProcessor(cj)
    # 根据handler创建opener对象
    opener=ur.build_opener(hl)
    
    1. 爬取ss号
    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&copyright=-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&copyright=-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()
    
    1. 访问ss链接获取md号与简介
    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)
    
    1. 访问api
    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')
    
    1. 访问md页面获取日期、集数、tags
    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
                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: # not_finished
                    res=res.groups()
                    date='-'.join(res[0:3])
                    episodes=0
                else:
                    res=re.search(tp_movie,time.text)
                    if res is not None: # a_movie
                        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. 数据初步清洗
    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)
    

    初步清洗后的数据格式如下:

    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黄金周的第一天,阿良良木历和班长羽川翼一起埋葬了一只被车碾过,没有尾巴的猫。这本应是一件的普...
    1. 封面图下载
    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 导入依赖库

    import numpy as np
    import pandas as pd
    from scipy import stats
    import matplotlib.pyplot as plt
    import time
    import os
    from bs4 import BeautifulSoup as bs
    import urllib.request as ur
    import urllib.parse as up
    import urllib.error as ue
    import http.cookiejar as hc
    
    import re
    import gzip
    
    import json
    %matplotlib inline
    plt.rcParams['font.sans-serif']=['SimHei'] #解决符号乱码问题
    plt.rcParams['axes.unicode_minus']=False #解决中文乱码问题
    os.chdir(r'...')
    

    4.2 数据清洗

    4.2.1 日期型数据处理

    对于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日NaN
    12004年1月1日NaN
    22002年10月1日NaN
    32008年10月2日NaN
    41995年10月4日NaN
    5NaN1995年11月18日
    6NaN1997年7月19日
    7NaNNaN
    82009年4月5日NaN
    92017年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-231998
    12004年1月1日NaN2004-01-012004
    22002年10月1日NaN2002-10-012002
    32008年10月2日NaN2008-10-022008
    41995年10月4日NaN1995-10-041995
    5NaN1995年11月18日1995-11-181995
    6NaN1997年7月19日1997-07-191997
    7NaNNaNNaT0
    82009年4月5日NaN2009-04-052009
    92017年10月14日NaN2017-10-142017

    4.2.2 bilibili评分缺失值处理

    需要将评分人数不足的作品的评分及评分人数从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)
    

    4.3 基本描述统计

    4.3.1 bilibili评分

    • 平均数、中位数
    (len(bilidb.rating),
     bilidb.rating.dropna().describe(),
     bilidb.rating[bilidb.rating==bilidb.rating.mode()[0]].count())
    
    (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部作品)。

    • 频数分布直方图、正态分布曲线
    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分。

    4.3.2 bangumi评分

    • 平均数、中位数
    bgmdb.rating.describe()
    
    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分。

    • 频数分布直方图、正态分布曲线
    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分。

    4.4 bangumi动画作品数据分析

    4.4.1 每个动画公司各年度制作了多少动画

    # 生成年度与动画制作公司的数据交叉表
    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()})
    # 转化为series格式
    studio_dict=pd.Series(studio_dict)
    # 选取1974年以后,总制作部数排名前20的动画公司的数据展示
    yr_prod.loc[:,studio_dict.sort_values(ascending=False).index.tolist()[:20]][15:]
    
    动画制作サンライズJ.C.STAFF東映アニメーションMADHOUSEProduction I.GスタジオディーンA-1 PicturesAICトムス・エンタテインメントぴえろBONESシンエイ動画XEBECGONZOSHAFTSILVER LINK.京都アニメーションオー・エル・エムサテライトBrain's Base
    年度
    197400100000000000000000
    197500100000000000000000
    197700000000000000000000
    197800000000100000000000
    197910100000100100000000
    198000000000000100000000
    198120200000000100000000
    198230000000000100000000
    198300000000110100000000
    198400100000010100000000
    198520010100000100000000
    198600300100000100000000
    198710310000110100000000
    198830100000120100000000
    198930210300000100000000
    199010300000000100000000
    199120200000000100000000
    199210301000010200000000
    199310501000010200000000
    199410300000010200000000
    199511401001210300100100
    199621400101220120000000
    199722200003120220000000
    199852240101100120000100
    199952551201110310000200
    200032471101331210000100
    200133541300132321000200
    200263453300331303000000
    200326261101424222001000
    200461255300152216100100
    200557645300132234003120
    2006758104600223225102101
    2007375122411143327402031
    2008474116651222246301122
    2009366108523222213516040
    201048465384332240412100
    2011477794613211233322123
    2012791019386532351554133
    2013857676112512255335354
    20149612567130757641444235
    20155694103130145431454133
    20167810229170348311464444
    20178105446130253343492222
    2018611664760548227234141
    2019411358220325202152331
    202012103120100100121001

    4.4.2 总的动画制作分布

    fig=plt.figure(num=104,figsize=(6,4),dpi=200,facecolor='white')
    ax=fig.gca()
    # 选取制作部数排名前17的动画公司,这些动画公司制作的作品数占总数的36%。
    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 每个动画公司制作的动画部数及平均评分

    # 动画公司与其片均评分的数据交叉表
    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)
    # 两表合并,按制作部数取前30位
    studios_counts_ratings=pd.merge(studios_ratings,studios_counts,on='动画制作').sort_values(
        by=['subject','rating'],ascending=False).head(30)
    studios_counts_ratings
    
    ratingsubject
    动画制作
    東映アニメーション6.929124186
    サンライズ7.106657178
    J.C.STAFF6.810706177
    MADHOUSE7.092288156
    Production I.G7.176925133
    A-1 Pictures6.776467122
    スタジオディーン6.799629116
    BONES7.02164089
    ぴえろ6.64436089
    シンエイ動画7.06785187
    トムス・エンタテインメント6.78666778
    XEBEC6.78925467
    GONZO6.53692567
    京都アニメーション7.36771062
    SHAFT7.43405060
    SILVER LINK.6.59279759
    オー・エル・エム6.90226549
    動画工房6.65800048
    サテライト6.43510647
    Brain's Base6.70387046
    セブン6.13884846
    Milky5.88315645
    AIC6.78572744
    ZEXCS6.27209542
    ティーレックス5.95992942
    アームス6.40268341
    ラルケ6.55516237
    diomedéa6.19883837
    ufotable7.17013936
    Walt Disney Animation Studios7.08955636

    选择其中平均评分最高的10个动画公司:

    studios_counts_ratings.sort_values(by=['rating'],ascending=False).head(10)
    
    ratingsubject
    动画制作
    SHAFT7.43405060
    京都アニメーション7.36771062
    Production I.G7.176925133
    ufotable7.17013936
    サンライズ7.106657178
    MADHOUSE7.092288156
    シンエイ動画7.06785187
    BONES7.02164089
    東映アニメーション6.929124186
    オー・エル・エム6.90226549

    可以看到,SHAFT、京都动画、Production I.G等动画公司出产的作品平均质量较高,这与许多动画爱好者的观点是一致的。

    4.4.4 2000-2019年热门动画作品及趋势分析

    • 各年度代表作(热门作品与高分作品)

    热门作品

    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.7456
    20018.0019
    20028.0241
    20038.1159
    20048.2890
    20058.1902
    20068.3470
    20078.2656
    20088.3198
    20098.2012
    20108.1866
    20118.3057
    20128.1612
    20138.0661
    20148.3658
    20158.2285
    20168.0552
    20178.1678
    20188.1140
    20198.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))
    

    4.5 两站动画匹配与数据库的合并

    4.5.1 匹配策略

    注意到bilibili的搜索功能,可以搜索到对应作品的md号:

    故采取以下的匹配策略:

    1. 导入bangumi的数据
    2. 按照日文原名和中文名向search.bilibili.com发出查询请求
    3. 从查询结果中提取番剧链接(md号)
    4. 根据名称、年份等信息进行匹配,得到匹配接口(subject-md)

    4.5.2 查询脚本

    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)
    

    接口格式:

    {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 信息匹配

    首先,过滤掉没有搜索结果的键值对:

    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部作品。

    4.5.4 数据库合并

    使用匹配接口,将两个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-012004
    1324攻壳机动队 STAND ALONE COMPLEX番剧1564日本15649.83511.0http://i0.hdslb.com/bfs/bangumi/6ebd07ba376115...516939...NaNNaNNaN菅野よう子NaNNaN遠藤誠、村上正博寺岡賢司、常木志伸2002-10-012002
    2876CLANNAD ~AFTER STORY~番剧1178日本11789.942904.0http://i0.hdslb.com/bfs/bangumi/54003a09e72f0d...917778...NaNNaNNaNANANT-GARDE EYESNaNNaNNaNNaN2008-10-022008
    31428钢之炼金术师 FULLMETAL ALCHEMIST番剧1089日本10899.980126.0http://i0.hdslb.com/bfs/bangumi/401f84cadca354...1990896...NaNNaNNaN大橋卓弥、常田真太郎関本美穂テクノサウンド龍角里美、池上真崇鈴木雅久2009-04-052009
    42115673月的狮子 第二季番剧6445日本64459.815230.0http://i0.hdslb.com/bfs/bangumi/14cf90e4ea9a05...480677...NaNNaNNaNNaN谷口工作、吉澤翠NaNNaNNaN2017-10-142017

    4.6 两站评分综合分析

    4.6.1 相关性分析与散点图

    • 线性回归
    res=stats.linregress(x=db.bgmrating,y=db.bilirating)
    res,res.rvalue**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分数变化来解释。

    • 普通散点图

    按原始数据作散点图和趋势线:

    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分,然后建立数据交叉表:

    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.9
    bgmrating_2digit
    5.41000100211000100
    5.51112022001101000
    5.61012100100010100
    5.72211212002021000
    5.80421002121211000
    5.90102222222100000
    6.01122242416011000
    6.11132313532651000
    6.20115147455445000
    6.31010425473565210
    6.40110017182544100
    6.5011212151071474600
    6.60011143371110713510
    6.7000020022561113510
    6.80000211343894740
    6.9100011023361031360
    7.0001011013576132130
    7.1000001021483101181
    7.21000010135716131380
    7.30001001112410101571
    7.40001000112521217100
    7.5000000012024101190
    7.600000010123051572
    7.7000001000034210100
    7.800000000010539102
    7.900000000100311073
    8.000000000020105101
    8.10000000000001250
    8.20000000000002682
    8.30100000000021011
    8.40000000000010040
    8.50000000000000200
    8.60000000000000010
    8.70000000000000010
    8.80000000000000011
    9.00000000000000011
    9.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')
    

    4.6.3 箱线图

    比较两网站的片均评分和中位数,可以看到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站评分产生严重的拖尾,导致其片均评分明显小于中位数。

    4.6.4 两网站评分特征及原因推测

    通过bilibili的评分频数分布直方图,可以看到作品评分在高分段扎堆,呈现的趋势基本上是分数越高,作品越多。9.7分就有378部,占到了全部有评分动画的五分之一,严重地丧失了区分度。并且,从箱线图中也能看出b站低评分很多导致片均评分低于中位数。

    一般来说,作品评分极高和极低的作品数都应该很少,绝大多数作品评分应当集中在平均值左右(即正态分布)。很显然,b站的评分分布严重偏离了正态分布。

    相比于bilibili,bangumi的频数分布直方图呈现出两头低,中间高,左右对称的特点,相应的正态分布曲线与实际分布高度吻合。而且bangumi的分数集中度也较低,在6.6-6.7区间也只有266部动画,占比只有二十分之一多一点。所以至少从统计学规律上说,bangumi这个网站的评分更有参考意义。

    与bangumi的对比告诉我们,b站的评分数据存在诸多异常之处。

    既然分布很异常,那么b站评分到底代表了什么?产生这种分布的原因是什么?笔者尝试通过数据给出一些合理推断。

    • 相对人气指数

    我们需要回来关注另一组数据,那就是片均评分和人均评分。

    bilibilibangumi
    片均评分9.2115886.633856
    票均评分9.0621147.185152

    b站的片均评分高于票均评分,而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()
    

    可以提取出两个特征:

    1. 总体来说两站基本趋势相同,说明动画业界衰退态势明显,近十年来作品平均排名在后半部分徘徊;
    2. b站对于2012年之后的动画作品排名始终高于bgm,尤其是近几年的新番排名明显偏高,相对来说,老番的排名则偏低,这充分印证了上文提到的新番压制老番的情况。

    就这些情况,推测如下:

    作品年龄与其观众的年龄是成正比的

    而且是观众年龄越大,评价质量就越高,在一定程度上也就意味这给出5星的概率越低

    不同年龄段评价标准的差异影响了新番和旧番评分情况,同时也与好番不火情况有关。

    4.6.5 总结

    基于上述分析和事实,我总结了B站评分不正常分布产生的原因:

    1. 评分缺乏基本的指导

    在bangumi评分时,会从1星到10星分别提示
    不忍直视-很差-差-较差-不过不失-还行-推荐-力荐-神作和超神作,并且还会提示评分者谨慎评价。

    虽然只有这几个字的建议,但这能够在很大程度上促使评分者谨慎思考。

    而回过头看b站的评分环境,除了令人迷惑的“发表五星评价需扣除一枚硬币”之外别无他物。

    1. 评分没有限制

    bangumi在评分时首先要点击“看过”才能评分。虽然说这种形式上的限制可能没什么作用,但相比之下,B站作为一个提供视频源的网站,居然不用看番就可以评分,这极大降低了评分的门槛,严重降低了评分的可信度,而且我认为b站对于投五星需扣除1硬币这种操作荒谬至极,如果b站希望通过评分扣硬币这种方式促使点评者谨慎评价,那么应当是投任何分数都需要硬币,而且至少2个。

    1. 评分标准存在问题

    首先对于平台来说,评分机制缺乏指导,过于模糊,而对于用户而言,发表的评价质量也不高,往往非常片面,并且用户接受度的影响较大。但是另一方面,由于所有作品的评分和点评都是公开可见的,在评分时固然会受到已有评价的影响。有些人看起来很有主见,实际上很容易被带节奏,改变自己的想法。这一方面表示对一部作品没有自己的理解,没有形成明确的观点,另一方面也是从众心理的体现。

    所以我提出两条建议:

    • 对平台而言希望b站能提供基本的评分指导;
    • 对用户来说,希望在评分时能够做到冷静、谨慎。

    5 结语

    本文从爬虫入手,爬取bilibili和bangumi网站的动画作品数据,对动画作品进行了一些数据分析,了解了近年来动画行业的发展趋势,并且通过分析b站评分数据并将其与专业评分网站bangumi比较,发现

    • 与专业评分网站相比,b站评分的参考作用存在但有限

    • b站评分分布异常,区分度不大,佳作被埋没

    • 点评者们对评分标准把握出现偏差,过度追捧新番

    • b站评分机制不完善,缺乏限制和指导

    出于时间和能力原因,很多分析并不全面,甚至可能导致结论错误。接下来的工作便是优化代码,并对数据进行更深入的分析。

    展开全文
  • 快捷的bilibili命令行爬虫 这是一个用来爬取bilibili部分信息的命令行工具 持续更新中,喜欢的话来个star呗 (。・ω・。) 通过pip安装 现在只测试了Mac用户,Mac用户可以很方便的通过pip进行下载。在终端中输入如下...
  • 网络爬虫教程 这个仓库包含B站视频系列:网络爬虫的原始码和更多信息。 这个仓库的使用方式: 获取视频中的示例代码 欢迎帮助我完善代码中的注释以及 欢迎帮助我完善这个自述文件 (如果要转载别人的文章请注意版权...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,990
精华内容 1,996
关键字:

bilibili爬虫

爬虫 订阅