本文最后更新于:2020-09-13 20:14:56
To be over and done with.

前言

写在前边的话

两个概念:VTuberVUp

本文中会涉及到两个概念:VTuberVUp

虚拟YouTuber(日语:バーチャルユーチューバー,英语:Virtual YouTuber,也缩写为VTuber)是以YouTube为平台进行影片直播和投稿的虚拟偶像(日语:バーチャルアイドル),在YouTube以外的平台中又被称作虚拟主播(日语:バーチャルライバー)。

虚拟UP主(简称Vup)——广义上指以虚拟形象在视频网站上进行投稿活动的up主。

无论是VTuber,还是VUp,都可以定义为人设+角色+声音

采用3D/2D技术,部分虚拟YouTuber会借助安置在头部与肢体上的动作捕捉设备以及传感器将人物动作展现到虚拟角色上。而借助于实时运动捕捉的机制,虚拟YouTuber还可以通过多种方式与现实世界中的粉丝进行交流。一部分虚拟YouTuber也会使用Live2D建立虚拟角色模型,借助网络摄像头以及FaceRig等软件实现模型的动作。

在中华人民共和国大陆地区,由于法律和政策的原因,无法访问YouTube,所有用户不能直接观看虚拟YouTuber的视频。因此有用户在通过授权的情况下将相关视频转载到如bilibili弹幕网这类的网站。另外也有相当一部分虚拟YouTuber在中国网站内设有官方频道,并进行直接的,跨国的,面向中国市场的著作权直播。一部分虚拟YouTuber在bilibili的粉丝订阅量远高于YouTube的订阅量,还有一些日本虚拟YouTuber的活动中心向中国偏移。除此之外,在bilibili上也活动着一些虚拟视频制作人,称为“虚拟UP主”或“虚拟主播”,目前在中国大陆活跃的虚拟主播有虚拟次元计划(虚研社)、兰若-re、Siva_小虾鱼_、木糖纯和庄不纯、幽灵子辰、进击的冰糖等[注 7]。而部分虚拟主播也参加了如中科院物理所开发日这样的大型活动。另外,CCTV新科动漫频道旗下的虚拟形象新科娘也于2019年9月成为虚拟UP主,在bilibili进行直播。

由于本文重点不在此处,因此不进行详细描述。

由于同时采用了YouTubebilibili两方面的数据,为做描述上的区分,规定如下:

  • 所有在bilibili上投稿的账号,凡是被记录,均称为VUp,无论其是否为官方账户,也无论其是否在YouTube平台拥有官方账户。
  • 所有在YouTube上投稿的账号,凡是被记录,均称为VTuber,无论其是否在bilibili平台拥有账户。
  • 文章中出现的V这一称呼,既可以表示VUp也可以表示为VTuber,请根据上下文文脉加以判断。

关于舰长

主播房间内拥有自己的舰队,粉丝可以点击上船成为主播舰队的船员,船票有总督/提督/舰长三种。

这里没有找到具体的定义,根据我自己的理解,舰长就是一种直播时的打赏形式。

其对比可以参考下方表格

舰长提督总督
勋章突破粉丝勋章突破20级,身份期间永不熄灭粉丝勋章突破20级,身份期间永不熄灭粉丝勋章突破20级,身份期间永不熄灭
投稿评论身份特权在开通的主播投稿视频下评论,
专享头像框与装扮卡
在开通的主播投稿视频下评论,
专享头像框与装扮卡
在开通的主播投稿视频下评论,
专享头像框与装扮卡
直播间身份特权互动气泡框
用户身份卡
头像框
聊天栏入场特效
互动气泡框
用户身份卡
头像框
聊天栏入场特效
开通专享全平台广播
房间页进场广播
互动气泡框
用户身份卡
头像框
聊天栏入场特效
上船动画开通或续费,直播间内播放舰长上船动画开通或续费,直播间内播放提督上船动画开通或续费,直播间内播放总督上船动画
大航海专属道具友谊的小船,冲浪友谊的小船,冲浪友谊的小船,冲浪
大航海直播间发言特权专享底部弹幕特权
专项基佬紫弹幕
专享底部弹幕特权
专项基佬紫弹幕
弹幕字数上限20->40字
专享底部弹幕特权
顶部弹幕悬停
专项基佬紫弹幕
弹幕字数上限20->40字
价格首次购买:198元/月
续费8折优惠:158元/月
【安卓/pc】自动续费7折钜惠:
138元/月
*iOS因苹果政策原因暂不支持自动续费
首次购买:1998元/月
续费8折优惠:1598元/月
首次购买:19998元/月
续费8折优惠:15998元/月

本文中,只采取了有购买大航海的DD的数据。

在本文中,没有区分三种大航海的不同,下文中舰长提督总督均只称为舰长

关于DD

常说的DD一词来源于日语词汇「Daでも 大好Daいすき」

译为:对谁都说单推

这本身就是DD行为

现在,DD一词已经衍生出了动词词性,单独使用一个D可以用作动词,代表单推XX。举例:我永远单推小狐狸,才不会D其他女人。

由该词衍生出来一种说法:DD斩首

常见于各种日语VUp的生放送互动和弹幕中。

狗妈猫猫等VUp在直播时说过:DD斬首

注:上述内容可以详见参考链接部分。病娇猫猫,我大好

为什么选择这个题目

首先来说,这个题目类似于推荐系统,因此这里需要先描述一下推荐系统的意义。

推荐系统的意义

不参考任何内容的个人理解来说,所谓推荐系统,就是采用各种算法,基于大量/海量的数据进行计算,根据用户所浏览内容的一些特征(比如商品的类型:书籍、日用品、服饰、书籍分类:小说、传记等)进行定向推送,向用户推送一些同类型的内容(包括但不限于:商品、影视作品、文学作品),从而达到推销、引流等目的。

你说的这个系统,它有什么意义呢?

它不是有没有意义的问题,它真的是那种,那种很少见的那种,它的作用,咳咳咳……(我编不出来词,就直接结束好了

咱们言归正传,推荐系统嘛,很简单呀,你想啊,比如你是个DD,天天D着一大~堆女人。

你打开了Bilibili手机客户端,却发现主页上都是各种各样的国际新闻,时事热点,鬼畜大军,吃瓜内容。你还会想继续看下去么?

什么,你说你都喜欢?

那么,如果给你推送了一些B站引流来的各种流量明星呢?

对吧!

如果是我的话,我会直接退出客户端,然后卸载(微笑脸)

(我不会说b的网页版已经靠自己写的脚本变成了自己喜欢的样子的)

如果像我这样的用户变多了,并且没有推荐系统的情况下,用户粘着性就会降低。

容易导致客户流失,收益下降。

对于电商网站来说,比如X宝,现在你打开客户端,往下滑动,就能看到一堆你最近在搜索,或者是你喜欢的东西。

虽然吃土的我嘴上说着不会买,但是每次越看越想看,结果就不知不觉地下了单。

需要推荐系统

综上所述,虽然我非常讨厌这样的被监控的感觉,但是为了经济发展(就是为了赚钱),果然没有推荐系统是不行的。尤其是如今这个信息飞速发展的时代。

举个最简单的例子,我最近在看《古书堂事件手帖》这个系列的书,之前在天闻角川的摊位前买过第一册,读后发现挺喜欢的,所以我想买下剩下的其他册,于是我在当当网上搜索关键词古书堂事件手帖2,找到了第二册书之后,下边会有剩下的其他册的快捷入口,这样我就非常方便地买下了全套的书籍,否则我需要一本一本去搜索。说不定我觉得操作起来比较麻烦就不会一次性买这么多了。

(如果想看我的书单,可以上方的Entertainment > Books栏目,或者点击这里的传送门

课程的原因

首先,课上,老师推荐自选题目,又要符合分类或者回归内容。

其实我还想到了另一个题目,《关于轻小说的分类问题》

因为最近沉迷各种轻小说无法自拔

但是考虑到实在是小说这个是数据不好寻找,虽然我可以直接使用文件内容去进行文本向量处理

但是我个人比较想做日语原版的轻小说,比如使用RNNLM(递归神经网络语言模型)等模型进行分词

这样就导致一个问题,我的教学环境是中文,我不确定这样制作的结果有没有人能看得懂

以上,我选择了下面这个题目

最近比较喜欢看各种VTuber的直播(毕竟视频势VTuber/VUp越来越少了,这里强烈安利一个视频系的VUp虚拟次元计划

而且反正这一点也没有别人会选择(大概,毕竟像我一样的死宅应该不多hhhhhhh

关于YTuber……(还没想好些什么,回头再补充)

研究内容

根据VTuber的粉丝数据和类型数据,预测各个类型的VTuber粉丝数据

由于上述内容计算量太大,预计计算时长月3个月*24h,赶不上DDL,所以决定更改内容了。

本次内容决定分析VUp的大航海数据,即每个VUp拥有多少个舰长DD这样的数据

根据被D的DD所D的VUp来分析VUp之间的关联性与相似性。

模拟简单的推荐系统,测试如果为没有"单推"的相同类型的VUp的DD推送新的VUp,被推送的VUp的被D概率。(我没有说绕口令)

举个🌰:

我单推小狐狸、猫猫、鹿乃、沙月酱

由前提条件可知,小狐狸所属Hololive,因此我可能会喜欢同样所属Hololive夏哥

又知我单推猫猫,并且hanser也单推猫猫,所以我可能会喜欢hanser

由于鹿乃所属花寄り女子寮,所以我可能同样会喜欢同样所属花寄り女子寮花丸はれる小東ひとな野々宮ののの(就在刚刚2020年9月6日19:00:00,ののの开了毕业直播,并正式宣布毕业了,我哭了)

因此我关注了夏哥,而且我本身就有关注hanser

因为鹿乃加入了花寄,我也关注了花寄的各位,并且开始了单推。

这样来说,这个推荐系统的影响就是正向的,有意义的。

研究意义

第一点

帮助经纪公司决策VTuber的发展前景以及该行业的未来发展,引入什么类型的V会有更好的营收,以及DD们的follow数。

针对游戏部企划艺人欺凌事件这件事,当时我只是抱着吃瓜的态度去看的,因此没有什么太深的感触,但是关于绊爱换人事件这件事我是从头到尾密切关注的,因为KizunaAI是我单推的第一个VTuber,虽然嘴上说着人工智障,但是我非常喜欢这个V。

(2017年的时候我还编辑过绊爱的百度百科词条来着)

所以我大致上有稍微了解过V圈的混乱程度,在最早的时候还不明显。

尤其是近两年,这个行业有了人气之后,各种各样的V如雨后春笋一般涌现了出来。

而随着V圈数量的暴增,也带来了各种各样的问题,具体表现为:中之人各种出事、V之间的摩擦引发双方粉丝的不理智行为、由于各种各样的原因而毕业、毕业后又回归。

毕业:谢拉

宣布毕业后回归:碧居结衣、有栖マナ(2020年9月2日宣布回归)

外加上资本引入,公会入驻,V圈逐渐演化成饭圈风气,b站直播区乌烟瘴气。

2020年7月1日,b站的V圈出现一条视频,一位名叫菜菜子Nanako的VUp宣布出道,粉丝数立刻增加。

该V的中之人是人们非常熟悉的蔡明,凭借着中之人的知名度,该V形象立刻引起了众多关注,其直播首秀在B站人气值突破600w,开播25分钟便达成“百舰”成就,迅速登上B站直播人气榜第一。

这一点充分证明了V圈的乱(人杂、水深)(我并没有说菜菜子Nanako或者蔡明本身不好,只是这一个例子非常明显,因此举出了这个而已。

我非常喜欢这个视频:【沙雕相声】这个专业你敢报吗!?(Ver7.1)

从二次元到三次元那段时间发生的事讽刺了个遍

其中小希的一句话令我记忆犹新:

虽然大家嘴上都讨厌财富密码,但外国人的身份还是吃得开。毕竟有些人看到我们二次元形象说中文,都会原地抽搐,口吐白沫。有了外国人的身份,不论是脏话还是搞黄色的……

有着外国人的身份,说一句自己爱中国,中国比自己国家好,就能赚得大量的粉丝和收入,岂不美哉。hhhhhhhhhhhh

你可能看不出来我写这一大坨东西和推荐系统有什么关系。。。

其实,上述出现的问题有一大部分的原因就是资本原因,如果各个V的经济公司可以做更好的策划,与各大视频网站进行更加深入的合作,充分利用数据,很有可能会挖掘到更多的潜在特定群体,三方均可以取得更多的收益(不仅限于资本上)

另外还有一点

为出道成为有名VTuber打下良好基础

正式内容

爬取数据

这里分为YouTube和Bilibili的数据

采用对于两个数据网站的数据进行爬取,而不是直接对视频网站进行爬取

至于理由嘛,嘿嘿嘿,因为我懒啊(逃

YouTube数据

VTuber的数据来源:http://virtual-youtuber.userlocal.jp/document/ranking

由于是静态加载,即按照页码划分好的,因此直接更改地址变换页码即可正常爬取

代码部分

下边是编写的爬虫代码

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
# %% 
import urllib.request # 用去获取网站链接请求
from bs4 import BeautifulSoup # 用于读取网站内容
import re
import csv
import codecs
import os
import time
import datetime

#%%
class CrawlVTB:
fileName = ""
fileHead = ""

def create_csv(self):
'''创建用于保存的文件'''
with open(self.fileName,'w', newline='', encoding='utf-8-sig') as f:
csv_write = csv.writer(f)
csv_write.writerow(self.fileHead)


def write_csv(self, rank, name, chanName, agencyName, fansCount, playedCount):
'''逐行写入内容'''
with open(self.fileName, 'a+', newline='', encoding='utf-8-sig') as f:
csv_write = csv.writer(f)
data_row = [rank, name, chanName, agencyName, fansCount, playedCount]
csv_write.writerow(data_row)
def print_log_info(self, log):
'''输出log'''
os.system('cls')
print(log)

def execute(self):
'''执行爬取的函数'''
loopStop = 0
for page in range(1,41):

url = "http://virtual-youtuber.userlocal.jp/document/ranking?page={}".format(page)
self.print_log_info("正在写入第"+str(page)+"页")
f = urllib.request.urlopen(url)
html = f.read().decode('utf-8')
soup = BeautifulSoup(html, "html.parser")

_dataRaw = soup.findAll(name="tr", attrs={"data-href":re.compile("^/user/")}) # 查找所有属性data-herf以/user/开头的tr元素

for item in _dataRaw:
rank = item.find('strong').string.strip().replace('位', '') # 顺位 去除‘位
_nameColumn = item.find(name="td", attrs={"class":"col-name"})
name = _nameColumn.find(name="a", attrs={"href":re.compile("^/user/")}).string.strip() # character名字
chanName = _nameColumn.find(name="span", attrs={"class":"text-secondary"})
chanName = '' if chanName==None else chanName.string.strip() # channal名字
agencyName = _nameColumn.find(name="a", attrs={"href":re.compile("^/office/")})
agencyName = '' if agencyName==None else agencyName.string # agency名字(所属)
_fansColumn = item.find(name="td", attrs={"class":"vertical text-right text-nowrap"})
fansCount = _fansColumn.find(name="span", attrs={"class":"text-success font-weight-bold"}).string.replace('人', '').strip() # fans数,去除‘人’
playedCount = _fansColumn.find(name="span", attrs={"class":"text-danger font-weight-bold"}).string.replace('回', '').strip() # 再生数,去除‘回’
self.write_csv(rank,name,chanName,agencyName,fansCount,playedCount)
self.print_log_info("正在写入 "+rank+"位: "+name)
self.print_log_info("第"+str(page)+"页写入完成")
loopStop+=1
if(loopStop>=5):
loopStop = 0
time.sleep(0.5)
self.print_log_info("全部写入完成")

def __init__(self, fileName, fileHead):
self.fileName = fileName
self.fileHead = fileHead
self.create_csv()
self.execute()
# %%
today=datetime.date.today()
def main():
formatted_today=today.strftime('%Y%m%d')
fileName = "[" + formatted_today + "] vtuber_ranking_crawl_result.csv"
fileHead = ["順位","ネーム","チャンネルネーム","所属グループ","登録者数","再生回数"]
cvtb = CrawlVTB (fileName, fileHead)
if __name__=="__main__":
main()
# %%

Bilibili数据

VUp的数据来源:https://vtbs.moe/

遇到的问题

网页内容动态加载

由于该页面采用动态加载的方式进行展示,即页面移动到最下方就自动加载新的数据,因此不能采用最普通的爬取方法。

参考这个内容:请问爬虫如何爬取动态页面的内容? - xilixjd的回答 - 知乎

我找到了解决方法

大体上描述一下就是:首先用python启动一个专用的模拟Chrome,通过python控制浏览器重复移动到最下方的操作,直到所有数据均以加载出来,此时保存该页面。

然后使用b4s将保存好的html页面转换为b4s的指定类型,按照静态网页的方式进行数据筛选。

  • 首先安装selenium

因为我用的python是使用Anaconda进行管理的,所以在安装的时候,我是用了命令conda install selenium进行安装,如果你使用的是pip进行管理包,同样可以使用pip install selenium进行安装。

如果大陆下载速度比较慢的话,可以尝试使用换源去解决,具体方法请自行检索,如果你是在懒得去检索=>这里附上了百度的搜索结果这里是Google老师的结果

报错Chromedirver

在我成功安装了selenium之后,开始执行相应代码

成功报错:WebDriverException: Message: ‘chromedriver’ executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home

于是我才知道原来还得安装这个chromedriver

根据报错提示,去官网下载,我不确定是不是任意版本都可以,所以我找了一个和我用的Chrome版本一致的版本下载。

我用的python是使用Anaconda进行管理的,因此我直接将下载好的exe文件放在了Anaconda的根目录下。

报错Chrome

上边的chromedriver下载好了之后,本以为终于可以运行了

然而事实却bīan yì qì告诉我,没那么简单

因为,编译器很愉快地抛出了一条错误 => WebDriverException: Message: unknown error: cannot find Chrome binary

至于为什么,我在网上找到了这样一篇文章关于selenium的webdriver调用Chrome时报错的解决方案

原来如此,因为我安装Chrome的时候选择了非C盘(也就是默认位置)去安装,这就导致了python代码在运行的时候无法在默认目录下找到chrome.exe文件

那么就需要吧含有chrome.exe文件的文件夹目录添加到系统path环境变量中,具体步骤如下(如果您知道怎样操作可以直接继续下边的内容,或者您可以点击下方按钮展开操作步骤)

首先找到你Chrome的安装目录,也就是chrome.exe文件的文件夹所在的目录,如下图

Chrome的安装目录

将这个目录地址复制下来,以做备用

接下来,在计算机(我的电脑)图标右键,点击最后一个属性选项,如图

在弹出的控制面板菜单中选择左侧的高级系统设置选项,如图

高级系统设置

选择最下边的环境变量选项,如图

环境变量选项

在下半部分的系统变量中,找到path一行,然后双击,如图

系统变量

在弹出的菜单中,点击右侧的添加按钮,在里边粘贴上第一步复制的地址,然后点击最下方的确定选项,如图

最后依次点击确认,直至关闭所有窗口

最后一步,重启电脑。(不确定这个步骤是否一定需要做,你可以尝试一下如果可以正常运行则不需要重启电脑,否则,重启一下?

代码部分

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
# %%
from selenium import webdriver
import time
from bs4 import BeautifulSoup

# %%
import csv
import datetime
import os
import re

today=datetime.date.today()
formatted_today=today.strftime('%Y%m%d')
fileName = "[" + formatted_today + "] vup_ranking_crawl_result.csv"
fileHead = ["順位","ネーム", "ルームナンバー","info","登録者数"]
#%%
driver = webdriver.Chrome() #用chrome浏览器打开

options = webdriver.ChromeOptions()
driver.get("https://vtbs.moe/")
# %%
def execute_times(times):
for i in range(times + 1):
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(1)
execute_times(230)

# %%
html=driver.page_source
# %%
print(html)
# %%
f = open("VUpAllRanking.html",'r',encoding="utf-8")
# f.write(html)
# %%
soup=BeautifulSoup(f.read(),'lxml')
# soup=BeautifulSoup(f.read(),'html5lib')
# %%
def create_csv():
'''创建用于保存的文件'''
with open(fileName,'w', newline='', encoding='utf-8-sig') as f:
csv_write = csv.writer(f)
csv_write.writerow(fileHead)
def write_csv( rank, name, roomNumber, chanName, fansCount):
'''逐行写入内容'''
with open(fileName, 'a+', newline='', encoding='utf-8-sig') as f:
csv_write = csv.writer(f)
data_row = [rank, name, roomNumber, chanName, fansCount]
csv_write.writerow(data_row)
def print_log_info(log):
'''输出log'''
os.system('cls')
print(log)
# %%
def execute():
rank = 1
_dataRaw = soup.findAll(name="div", attrs={"class":re.compile("^columns is-mobile is-multiline card")})

for item in _dataRaw:

_icon = item.find(name="div", attrs={"class":"column smallBottomMarginTopBottomPadding is-6-mobile is-3-tablet is-3-desktop is-3-widescreen is-3-fullhd"}).img.get('src')
_nameColumn = item.find(name="div", attrs={"class":"column is-12-mobile is-6-tablet is-6-desktop is-6-widescreen is-6-fullhd content smallBottomMarginTopBottomPadding"})
nameANDliveNO = _nameColumn.find('h4')
if( nameANDliveNO.text.split()[0]!="直播中"):
upName = nameANDliveNO.text.split()[0] # Up名字
liveNO = nameANDliveNO.text.split()[1] # 直播间号
else:
upName = nameANDliveNO.text.split()[1] # Up名字
liveNO = nameANDliveNO.text.split()[2] # 直播间号
intro = _nameColumn.p.text # 主播简介
_fansColumn = item.find(name="div", attrs={"class":"column is-hidden-mobile is-3-mobile is-3-tablet is-3-desktop is-3-widescreen is-3-fullhd"})
fansInfo = _fansColumn.findAll(name="div", attrs={"class":"column is-7"})
fansCount = fansInfo[0].text.strip() # 粉丝数
#fansChangeCount = fansInfo[1].text.strip() # 粉丝变化数
write_csv(rank, upName, liveNO, intro, fansCount)
print_log_info("正在写入 "+str(rank)+"位: "+upName)
rank += 1
# %%
def main():
create_csv()
execute()
if __name__=="__main__":
main()

舰长DD数据

VUp的数据来源:https://vtbs.moe/dd

遇到的问题

和上一个Bilibili数据相同,该页面也采用了动态加载的方式,因此解决方式也同上。

未遇到其他问题

代码部分

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
90
91
import time
from bs4 import BeautifulSoup
import re
import datetime
import os

today = datetime.date.today()
formatted_today = today.strftime('%Y%m%d')
fileName = "[" + formatted_today + "] vdd_ranking_crawl_result.csv"
fileHead = ["順位", "ネーム", "ルームナンバー", "info", "登録者数"]
# %%
driver = webdriver.Chrome() # 用chrome浏览器打开

options = webdriver.ChromeOptions()
driver.get("https://vtbs.moe/dd")
# %%


def execute_times(times):
for i in range(times + 1):
driver.execute_script(
"window.scrollTo(0, document.body.scrollHeight);")
time.sleep(1)


execute_times(1)

html = driver.page_source

f = open("VDDAllRanking.html", 'w', encoding="utf-8")
f.write(html)

f = open("VDDAllRanking.html", 'r', encoding="utf-8")

soup = BeautifulSoup(f.read(), 'lxml')

def create_csv():
'''创建用于保存的文件'''
with open(fileName, 'w', newline='', encoding='utf-8-sig') as f:
csv_write = csv.writer(f)
csv_write.writerow(fileHead)


def write_csv(rank, *name):
'''逐行写入内容'''
with open(fileName, 'a+', newline='', encoding='utf-8-sig') as f:
csv_write = csv.writer(f)
data_row = [rank]
for item in name:
data_row.append(item)
csv_write.writerow(data_row)


def print_log_info(log):
'''输出log'''
os.system('cls')
print(log)


def execute():
rank = 1

_dataRaw = soup.findAll(
name="div", attrs={"class": re.compile("^columns is-gapless")})
for item in _dataRaw:
tmpVtbNameList = []
tmpVtbIdList = []
ddIcon = item.img.get('src') # dd头像
ddName = item.find(name="div", attrs={
"class": "column is-one-third"}).span.text.split()[-1] # dd名字

oshiList = item.findAll(
name="a", attrs={"href": re.compile("^/detail/")})
if(oshiList[0].get('title') == '这是一名本站收录的VTB/VUP'):
oshiList.remove(oshiList[0])
for vtbItem in oshiList:
vtbId = vtbItem.get('href').split('/')[-1]
vtbName = vtbItem.text.strip()
tmpVtbIdList.append(vtbId)
tmpVtbNameList.append(vtbName)

# fansChangeCount = fansInfo[1].text.strip() # 粉丝变化数
write_csv(rank, ddName, tmpVtbNameList)
print_log_info("正在写入 "+str(rank)+"位DD: "+ddName)
rank += 1

def main():
execute()

if __name__ == "__main__":
main()

数据处理

舰长和VUP关联的数据

在上述内容中,收集到了舰长DDVUp的名单,那只是表明每个DD和自己单推D的舰长的关系。

我需要所有DD和所有VUp之间有所关联,因此需要对数据进行处理

VUp1VUp2VUp3……VUpn-1VUpn
DD1110……00
DD2000……01
DD3010……00
……………………………………
DDn-1001……00
DDn000……01

需要对舰长DDVUp的名单处理为上述格式

如果DDx推了VUPy,那么对应位置(DDx, VUPy)的值为1,否则值为0

VUp之间的相关性

算法介绍

相关性,这里需要计算其相似度。采用协同过滤算法进行。

协同过滤(collaborative filtering)是一种在推荐系统中广泛使用的技术。该技术通过分析用户或者事物之间的相似性(“协同”),来预测用户可能感兴趣的内容并将此内容推荐给用户。这里的相似性可以是人口特征(性别、年龄、居住地等)的相似性,也可以是历史浏览内容的相似性(比如都关注过和中餐相关的内容),还可以是个人通过一定机制给予某个事物的回应(比如一些教学网站会让用户对授课人进行评分)。比如,用户A和B都是居住在北京的年龄在20-30岁的女性,并且都关注过化妆品和衣物相关的内容。这种情况下,协同过滤可能会认为,A和B相似程度很高。于是可能会把A关注B没有关注的内容推荐给B,反之亦然。——wikipedia

这种算法,是推荐算法中比较著名的一种。

举个例子:当你打开在b站客户端首页,你会发现有很多视频,有些是你感兴趣的,有些可能是你不感兴趣的。如果你是一个DD,那么你的首页很有可能会看到很多“单推的”VUp的视频。如果你喜欢看MMD,那你的首页很可能会出现诸如《耗时xxx秒,算了白嫖吧》、《布料解算/XX渲染》等等相关的MMD视频。这里排除一些靠流量和热点推送的视频和广告,上述这些视频就是推荐算法中相似度计算的一种方法。

算法描述

如上表中的内容,如果我需要计算不同VUp之间的相关性,我需要计算不同V之间的向量关系。

其中向量代表V对于每个DD的相关性,每个V对应一个长度为18486的向量

计算两个V之间的关系就是计算两个V之间的向量夹角余弦值

如下边的这个公式

ab  =  a  b  cosθa\cdot b\;=\;{\vert\vert\mathrm a\vert\vert\;\vert\vert\mathrm b\vert\vert\;\cos}\theta

其具体计算如下

similarity=cos(θ)=ABab=i=1nAi×Bii=1n(Ai)2×i=1n(Bi)2{\mathrm{similarity}=\cos(}\theta)=\frac{A\cdot B}{\vert\vert\mathrm a\vert\vert\vert\vert\mathrm b\vert\vert}=\frac{\sum_{i=1}^nA_i\times B_i}{\sqrt{\sum_{i=1}^n{(A_i)}^2}\times\sqrt{\sum_{i=1}^n{(B_i)}^2}}

根据上述公式可知

  • A和B分别代表两个V,计算时需要按照公式计算A和B的向量值。
  • 如果两个V的消费群体完全一致,那么他们的夹角为0,其夹角余弦值达到最大1
  • 有其计算值可知,计算的内容只有0和1,那么其计算结果不会出现负值,即处于[0,1]

舰长DD数据

计算DD数据

于是我在做DD关联性数据的时候,发现了数据的可怕之处

在案待斩首的DD共18684名

代码部分
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
# %%
import tensorflow as tf
import numpy as np
import csv
import cupy as cp
import pandas as pd
import sys
import os

# %%
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
config = tf.compat.v1.ConfigProto()
config.gpu_options.per_process_gpu_memory_fraction = 0.9 # 占用GPU90%的显存
session = tf.compat.v1.Session(config=config)
# %%
filename = '[20200903] vdd_ranking_crawl_result.csv'
# filename可以直接从盘符开始,标明每一级的文件夹直到csv文件,header=None表示头部为空,sep=' '表示数据间使用空格作为分隔符,如果分隔符是逗号,只需换成 ‘,’即可。
df = pd.read_csv(filename)
print(df.head())
# %%
dList = df.iloc[:, -1] # VUp名称的一列

# %%
DDList = list(df.iloc[:, 1]) # 用于存储所有DD的名字

# %%
today = "20200905"
formatted_today = today.strftime('%Y%m%d')
fileName = "[" + formatted_today + "] vdd_OSHI_result.csv"
# fileHead = ["DDName"]+VUpList

# %%
def create_csv():
'''创建用于保存的文件'''
with open(fileName, 'w', newline='', encoding='utf-8-sig') as f:
csv_write = csv.writer(f)
csv_write.writerow(fileHead)

def write_csv(rank, name):
'''逐行写入内容'''
with open(fileName, 'a+', newline='', encoding='utf-8-sig') as f:
csv_write = csv.writer(f)
data_row = [rank]
for item in name:
data_row.append(item)
csv_write.writerow(data_row)

# %%
def similarity(vector1, vector2):
'''计算相似性,即计算余弦值'''
return tf.norm(tf.multiply(vector1,vector2)/(tf.norm(vector1)*(tf.norm(vector2))))

# %%
martix_df = pd.read_csv(fileName)

# %%
list((martix_df.iloc[1,1:-1]))

#%%
# 计算第一行和第五行的相似度
print(float(similarity(np.array(martix_df.iloc[10,1:-1],dtype=float),np.array(martix_df.iloc[1,1:-1],dtype=float))))

# %%
fileName = "[" + formatted_today + "] vdd_similarity_displayTable.csv"
fileHead = ["DDName"]+DDList

# %%
def clear_output():
"""
clear output for both jupyter notebook and the console
"""
os.system('cls' if os.name == 'nt' else 'clear')
if 'ipykernel' in sys.modules:
from IPython.display import clear_output as clear
clear()

# %%
create_csv()
for index in range(1,164): # 164~169
tmpList = []
for indexInner in range(18684):
clear_output()
print(str(index) + "/" + str(18685) +":正在写入"+str(indexInner) + "/" + str(18685) +":" + "子内容:"+ martix_df.iloc[indexInner,0])
tmpList.append(float(similarity(np.array(martix_df.iloc[index,1:-1],dtype=float),np.array(martix_df.iloc[indexInner,1:-1],dtype=float))))
write_csv(martix_df.iloc[index,0],tmpList) # VUp名称的一列
print("全部写入完成")
计算时间

这些DD要计算相关性,那么就需要给每两个DD之间进行一次计算

也就是说需要共计:18684 * 18684 = 349,091,856 次 运算

而且每次所谓的计算是计算两个1031维度向量的余弦值

即每次计算共需要1032次乘法运算+1030次加法运算+(2 * 1031)次乘方计算 + 2次开方计算

在进行上述这些计算的之前,我毫不知情这次计算量的庞大性。

运行开始时间:2020年9月4日21:03:11

运行结束时间:2020年9月4日23:42:35,理由是因为输出内容太多,python卡死了

打开输出文件,发现总共计算了162行

于是关机睡觉,转天再战

虽然没有具体计算出来需要多久,但是大概知道是很久,所以今天,2020年9月5日,从早上开始研究如何提高计算速度

然后顺便为程序加上了个数字显示(昨天忘记了,确实不好看

经过询问google老师,发现Cupy是一个好东西,因为可以用到CUDA调用显卡进行计算,所以速度会很快

于是经过一番苦战,终于成功安装了CUDA和Cupy

然后运行,经过人工智能(只有人工没有智能)的计时,每进行1000次计算共需要25s左右的时长

18684 * 18684 = 349,091,856次计算,所以总共需要2424个小时左右,大月3个月

我要疯掉了!!!

于是又一番调整代码,结果发现,速度并没有太多提升

找新的解决办法——

经过X总的推荐,我安装了TensorFlow-GPU,又是一番苦战

我成功运行了代码,看了下任务管理器,GPU占用竟然是0%???

根据这篇文章tensorflow on GPU: no known devices, despite cuda’s deviceQuery returning a “PASS” result,我找到了原因

This error may be caused by your GPU’s compute capability, CUDA officially supports GPU’s compute capability within 3.5 ~ 5.0

根据英伟达这篇内容,可以看出来,大概我的GPU算力达不到要求。

因此,无论是我在使用cupy还是TensorFlow进行计算的时候,仍然没有调用到GPU,只是靠这颗可怜的CPU来计算的。。

然后我又记了一次时间,发现,在使用cupy和TensorFlow进行计算的时候,平均时间达到了30s+/k次的计算,甚至比最初numpy的计算时间还要久

所以最终我放弃了GPU加速,改回用Numpy和CPU进行计算。

计算VUp数据

由于上述舰长DD的数据太过于庞大,计算量超出预期,遂更改策略。

转向计算VUp的粉丝数据。

这样做,计算量就由18684 * 18684 = 349,091,856变成2031 * 2031 = 4,124,961,瞬间减小了两个数量级的数据。

说干就干

代码部分
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
# %%
import csv
import numpy as np
import pandas as pd
import datetime
import os
# %%
filename = '[20200903] vdd_ranking_crawl_result.csv'
# filename可以直接从盘符开始,标明每一级的文件夹直到csv文件,header=None表示头部为空,sep=' '表示数据间使用空格作为分隔符,如果分隔符是逗号,只需换成 ‘,’即可。
df = pd.read_csv(filename)
print(df.head())

# %%
today = datetime.date.today()
formatted_today = "20200905"
fileName = "[" + formatted_today + "] vdd_OSHI_result.csv"

# %%
def create_csv():
'''创建用于保存的文件'''
with open(fileName, 'w', newline='', encoding='utf-8-sig') as f:
csv_write = csv.writer(f)
csv_write.writerow(fileHead)


def write_csv(rank, name):
'''逐行写入内容'''
with open(fileName, 'a+', newline='', encoding='utf-8-sig') as f:
csv_write = csv.writer(f)
data_row = [rank]
for item in name:
data_row.append(item)
csv_write.writerow(data_row)

# %%
def similarity(vector1, vector2):
'''计算相似性,即计算余弦值'''
return np.dot(vector1, vector2)/(np.linalg.norm(vector1)*(np.linalg.norm(vector2)))

# %%
martix_df = pd.read_csv(fileName)
# %%
DDList = martix_df.columns.values.tolist()
# %%
martix_df = martix_df.T

# %%
today = datetime.date.today()
formatted_today = "20200905"
fileName = "[" + formatted_today + "] vup_similarity_displayTable.csv"
fileHead = ["VUpName"]+DDList
# %%
import sys
import os
def clear_output():
"""
clear output for both jupyter notebook and the console
"""
os.system('cls' if os.name == 'nt' else 'clear')
if 'ipykernel' in sys.modules:
from IPython.display import clear_output as clear
clear()

# %%
# create_csv()
for index in range(1,1032): # 164~169
tmpList = []
for indexInner in range(1,1032):
clear_output()
print(str(index) + "/" + str(1032) +":正在写入"+ str(indexInner) + "/" + str(1032) +":" + "子内容:"+ martix_df.iloc[0,indexInner])
tmpList.append(similarity(list(martix_df.iloc[index,1:-1]),list(martix_df.iloc[indexInner,1:-1])))
write_csv(martix_df.iloc[index,0],tmpList) # VUp名称的一列
print("全部写入完成")
计算时间

根据新的计算内容,只需要计算2031 * 2031 * 18684 = 77,070,771,324 次矩阵运算

经过手工及时,总运算时间大约17个小时,大大缩短了计算时间。

数据可视化

数值处理

由于所评价的VUp数量过于多,而各个V之间的关联性并不是非常强(指有超过半数的DD群体)

这就导致了每个V与其他V之间的关联评价值非常小

比如我在查询与V喵田弥夜Miya醉相思的前九位V时出现了如下结果

1
query('喵田弥夜Miya',True)
顺位VUpName喵田弥夜Miya
590喵田弥夜Miya1.000000
30星月千雪Channel0.134840
812黑桃影0.124575
126柚子Yuuneko0.120605
200奈美0.106600
119星空凛脂0.077850
388宝钟玛琳Official0.072654
544虎丸幼一Official0.072654
335夜行游鬼0.072075
408EricaZehnt_埃莉卡0.071067

结果如上,但是这个已经是结果很高的了。

因此,需要对结果进行归一化。

首先想到的是python自带的(0,1)标准归一化,xnormalization=xMinMaxMin{x}_{normalization}=\frac{x-Min}{Max-Min},以及Z-score标准化,xnormalization=xμσ{x}_{normalization}=\frac{x-\mu }{\sigma }

但是这里又个问题由于该数据统计时会有自身指向自身的情况(及相关度为1),该数据中最小值为0,因此归一化前后值不变。

所以这里决定采用手动增强和减弱关联度,使得相关度值分布在一个比较均衡的位置上,具体方法如下:

1
2
3
4
5
6
7
8
def Query(row, col):
'''查找指定的行和列,手动归一化处理'''
if(query('row',True)):
value = df[df['VUpName'].isin([row])][col].values[0]
if(0<value<0.1): return value * 10
elif(0.333>value>0.1): return value * 3
elif(0.5>value>0.34): return value * 2
else:return value

上述代码首先根据给定的行列名称读取到相应的值,然后根据规则进行归一化。经过归一化之后,关联性数值处于一个比较合理的范围,便于后期画图。

数据可视化

在为其归一化后,可以用图像的方式展示各个V之间的关联度。

根据MiracleXYZ大佬的文章(具体见下文链接处),这里尝试采用了Python的networkx库绘制V之间的关系。

经过多方尝试,我发现不知什么原因,当我绘制各个节点之间的带权路径时,会耗费很长时间。结束之后,我尝试使用G.edges()去查询内部边的关系,jupyter server出现了无响应重启等问题。

而我又尝试画图,依旧会出现相同问题。

排查问题

最初我认为是绘制函数出现错误导致的,因此尝试使用相同的方法,绘制几个简单的节点,并且随机生成一些权值,结果发现可以正常绘制,具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
#!-*- coding:utf8-*-
import networkx as nx
import matplotlib.pyplot as plt
import random
G=nx.Graph()
for u, v in nx.barabasi_albert_graph(10,2,seed=1).edges():
G.add_edge(u,v,weight=random.uniform(0,0.4))
pos=nx.spring_layout(G,iterations=20)
#以下语句绘制以带宽为线的宽度的图
nx.draw_networkx_edges(G,pos,width=[float(d['weight']*10) for (u,v,d) in G.edges(data=True)])
nx.draw_networkx_nodes(G,pos)
plt.show()

因此推断,由于数据量较大,超出了jupyter的限制,因此导致了其重启。

于是决定放弃自行绘制图片,因此,下述代码部分仅保留至写入节点和边的关系,不代表全部代码。

代码部分

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# %%
import numpy as np
import pandas as pd
# %%
filename = '[20200905] vup_similarity_displayTable.csv'
df = pd.read_csv(filename)
icon = pd.read_csv('[20200913] vup_ranking_crawl_result.csv')

# %%
iconTB = icon.loc[:,['ネーム','アイコン']]
# %%
icon[icon['ネーム'].isin(['赤井心Official'])]['アイコン']
# %%
icon.loc['赤井心Official',['ネーム','アイコン']]
# df[df.iloc[:,0]=='赤井心Official']['花园Serena']
# %%
def query(target, head=True, ascending=False):
if(target in df.columns):
return df.loc[:,['VUpName',target]].sort_values(by = target, ascending=ascending).head(10)\
if(head) else df.loc[:,['VUpName',target]].sort_values(by = target, ascending=ascending)
else: return False
# %%
query('赤井心Official',True)
# %%
def iconQ(target):
'''查找icon'''
return icon[icon['ネーム'].isin([target])]['アイコン'].values[0]
# %%
# %%
VUpNameList = list(iconTB['ネーム'])
IconNameList = list(iconTB['アイコン'])
# %%
zipList = list(zip(VUpNameList,IconNameList))
# %%
# %%

IconNameList[0].replace('https://i2.hdslb.com/bfs/face/', '').replace('@256h_256w', '')

# %% 清除控制台输出的内容
import sys
import os
def clear_output():
"""
clear output for both jupyter notebook and the console
"""
os.system('cls' if os.name == 'nt' else 'clear')
if 'ipykernel' in sys.modules:
from IPython.display import clear_output as clear
clear()

# %%
# %% 下载所有头像到本地计算机
import requests
for i in enumerate(zipList):
with open('./icon/{0}.jpg'.format(i[1][0]), 'wb') as file:
file.write(requests.get(i[1][1]).content)
clear_output()
print(i[1][0]+"\t图片下载完成")
print("全部完成")
# %%
VUpNameList
# %%

import networkx as nx
from matplotlib import pyplot as plt
G = nx.Graph()
# %%
G.add_nodes_from(VUpNameList)
# %%
import matplotlib.image as mpimg
import glob

path = './icon/'
files = [f for f in glob.glob(path + "*.jpg")]
img = []
for f in files:
img.append(mpimg.imread(f))
clear_output();print(f)
# %%
N = len(files)

# %%
len(img)
# %%
for f in range(2036,2338):
img.append(mpimg.imread(files[f]))
clear_output();print(f)


# %%
for item in VUpNameList:
print(df.loc[item])
# %%
def Query(row, col):
'''查找指定的行和列,手动归一化处理'''
if(query('row',True)):
value = df[df['VUpName'].isin([row])][col].values[0]
if(0<value<0.1): return value * 10
elif(0.333>value>0.1): return value * 3
elif(0.5>value>0.34): return value * 2
else:return value
# %%
Query('七濑胡桃menherachan', '喵田弥夜Miya')

# %%
query('喵田弥夜Miya',True)
# %%
VUpNameList[0]
# %%
for i in range(len(VUpNameList)):
for j in range(i,len(VUpNameList)):
G.add_edge(VUpNameList[i],VUpNameList[j],weight=Query(VUpNameList[j],VUpNameList[j]))

图片展示

这里的图片非上述程序绘制出来的,请注意!

余弦相似度

根据上述数据,去除值为0的V之间的关系(即把没有关系的V连线去掉)得到下图结果。

过滤后的余弦相似度

来自于MiracleXYZ大佬的方法

计算过两个向量之间距离的平方的倒数,以此作为相似度衡量标准,具体公式如下:

similarity=1i=1n(AiBi)22\mathrm{similarity}=\frac1{\sqrt{\sum_{i=1}^n{(A_i-B_i)}^2}^2}

距离的平方反比

但是由于当一个V的人气变高,单推的DD增多之后,其余其他V的距离就会变大,因此会向边缘分布。

最后的内容

关于实验环境

OS:Windows 10 2004

Processor:Intel® Core™ i7-8550U CPU @ 1.80GHz

Memory: 11.9GB 2133MHz

关于程序代码的说明

IDE: Visual Studio Code

Python/Based: Anaconda3

采用了Jupiter格式,使用VisualStudio Code进行运行

因此会出现#%%的内容,如果出现这个内容,代表该程序代码是分段运行的,如果不采用分段运行可能会出现一些问题,请注意。

如果你采用的是Jupiter Notebook的话,```#%%``所划分的每一块内容单独运行,大概可以。。

想说的话

虽然这篇文章是关于推荐系统的文章,但是我还是想说这句话。

人们能从互联网上获取到的消息越来越假,越来越无法了解到世界最原本、最真实的样子。——By:我的大学导师

就如同这句话所说,因为推荐系统的存在,并且由于这项技术被现在互联网科技广泛使用,人们单从互联网上获取到的消息越来越客制化,换句话说,你的手机、电脑越来越了解你,它们只会给你推送你喜欢的内容。

你的手机比你自己更了解你 ——《未来简史》读后感

比如你要买房,那么你想看到的是房价降低的消息,推荐系统了解后就会一直给你推送相关房价降低的消息。如此,你就会认为房价确实降低了,然而你并没有获得真实的情报。

但是,如果人们只沉浸于这样的信息社会的话,人类会不会越来越不像人类自己了呢?

铁心博弈

先行研究

https://miraclexyz.github.io/2018/07/19/vtuber-analysis/index.html

https://www.zhihu.com/question/351715743

https://www.bilibili.com/read/cv6413930/

https://note.com/sushitenko/n/n0bd11efaaf9d

https://github.com/pren1/fans_kalman

https://github.com/dd-center

参考链接

概念性内容

虛擬YouTuber - 维基百科:https://zh.wikipedia.org/zh-hans/虛擬YouTuber

什么是vup/vtuber?:https://www.bilibili.com/read/cv2980931/

B站vup出道是什么意思:https://zhidao.baidu.com/question/589829832033860245.html

Bilibili帮助中心-我是观众-大航海:https://link.bilibili.com/p/help/index?id=4#/great-navigation

Vtuber中的DD是什么意思鸭?:https://www.zhihu.com/question/312507329

白上吹雪吧(就想知道不知道DD和vtb是什么意思犯法吗?):https://tieba.baidu.com/p/6179104176

你们说的DD是什么意思啊? NGA玩家社区(艾泽拉斯国家地理):https://ngabbs.com/read.php?tid=19373074&page=e&rand=695

DD斩首 最高指令(狗妈):https://www.bilibili.com/video/BV157411M7po

【十分钟看猫猫】病娇猫【台词回】:https://www.bilibili.com/video/BV114411572z

动态爬虫

【解决】关于selenium的webdriver调用Chrome时报错的解决方案:https://www.jianshu.com/p/bd5ecb999925

请问爬虫如何爬取动态页面的内容?:https://www.zhihu.com/question/46528604

Python - pandas DataFrame 数据选取,修改,切片:https://blog.csdn.net/yoonhee/article/details/76168253

进行计算

tensorflow on GPU: no known devices, despite cuda’s deviceQuery returning a “PASS” result: https://stackoverflow.com/questions/42326748/tensorflow-on-gpu-no-known-devices-despite-cudas-devicequery-returning-a-pas

数据处理

协同过滤:

协同过滤推荐算法的原理及实现:https://blog.csdn.net/yimingsilence/article/details/54934302

其他内容

推荐系统实践–概述:https://www.cnblogs.com/qwj-sysu/p/4363421.html

浅谈个性化推荐系统(一):https://zhuanlan.zhihu.com/p/65488931

艾叶 - 铁心博弈【2020拜年祭单品】:https://www.bilibili.com/video/BV1MJ411C7ie

日语分词器的介绍与比较:https://flashgene.com/archives/41086.html

【卒業】ありがとう、ばいばい。【野々宮ののの/花寄女子寮】:https://www.youtube.com/watch?v=WB3vTgbBGxc

ホロライブプロダクション公式サイト - VTuber事務所:https://www.hololive.tv/

hanser:https://space.bilibili.com/11073

Vtuber也许已经来到了最危险的时刻…:https://www.gcores.com/articles/113674

vtuber如何收益化,如何商业化赚钱讲解:https://www.bilibili.com/read/cv2405463/

如何评价蔡明出道 VUP?:https://www.zhihu.com/question/404869735

蔡明- 维基百科:https://www.wikiwand.com/zh-hans/蔡明

【沙雕相声】这个专业你敢报吗!?(Ver7.1):https://www.bilibili.com/video/BV1a54y1S7UW

【毕业纪念】一路走来,感谢有你们的陪伴:https://www.bilibili.com/video/BV19J411r7aB

【またね。】谢拉毕业直播+哭个锤子【Overidea学院】:https://www.bilibili.com/video/BV19J411r7Qk

【有栖Mana】黑白狐的回归直播~!:https://www.bilibili.com/video/BV1eK4y1e7y9

android – 添加到networkx图的一个节点的图像未显示:http://www.366service.com/cn/qa/33dcf81dff5f8a45293dca5021ac21d9

Python中使用networkx库绘制带权图(以边的权值为宽度):https://blog.csdn.net/shelsea/article/details/96428024

お礼

非常感谢为本项目提供支持的所有人

也非常感谢被我引用的所有文章及其作者,也请原谅我的无言引用

最后感谢Google老师,是她,为我提供了24小时的耐心指导。(灵感来源于:If Google Was A Guy,片源:YouTube)

如果存在任何的版权或著作权问题,请及时和我联系,本人会及时配合进行修改或者删除。