【爬虫】 谁才是近几年的番剧“神作”?

谁才是近几年的番剧“神作”?

有一段时间看bamgumi上的评分,尤其是评分的排行榜的时候,我就不由得回忆起那些曾经高开低走的动画,在刚开播的时候还能在排行榜前排看到它们,但一道出现了奇怪的结局之后,又早就掉得无影无踪,实在是令人感叹。当然,也有很多的佳作,在刚开播的时候岌岌无名,但最后才发现它其实是唯一真神。

又发现,正好bangumi有了natabare这样的动画评分历史变化功能,那我不如直接做一个数据可视化,来看出近几年番剧的评分变化是怎么样的。那既然有了点子,那就直接去做吧

分析一下网页资源

单个番剧

首先要做的第一件事情那就肯定是观察natabare网站的构成,然后找到得到对应数据的办法了。

这里是natabare的折线图(以某动画为例子)

我们右键审查元素->网络 就可以看到这个网站加载的各种资源。

这里是network

我们注意到,其中有一个json文件,这里面记载了这部动画每一天的各种数据。

这里是network

那么在这里我们就可以很高兴地发现!里面的数据居然已经直接发出来了,根本不需要使用js逆向,或者beautifulsoup之类的其他的较为进阶的爬虫的技术了,我们只要把json接受下来然后对其进行处理,就可以直接搞定了。

我们对网页的资源分析已经完成,接下来就是写代码了。

番剧列表

我们选取2020年的评分靠前的动画列表:

列表

我们可以通过获得番剧列表网页,来得到我们所需的评分靠前的动画。

接收资源

本人并没有考虑过接收的效率问题,所以没有使用scrapy,aiohttp等等可能可以非常有效地提升爬取效率的工具,只用了非常朴素的request库,为了方便编写…

获取某一部番剧的json

爬取番剧json的函数大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def scrape_data(_id):
headers = {'Connection': 'close'}
headers['User-Agent'] = random.choice(user_agent_list)
site_url = base_url + str(_id)
# 关闭警报
requests.packages.urllib3.disable_warnings()
tries = 0
while tries < 5:
try:
r = requests.get(site_url, verify=False, headers=headers, timeout=15)
if (r.status_code != 200):
return '404'
json_data = json.loads(r.text)
return json_data
except requests.exceptions.RequestException:
print('timeout. Retrying in' + str(tries) + '/5')
tries += 1

其中最重要的一个步骤可能就是timeout的处理了。如果没有多次尝试的错误处理,最后非常有可能会卡在一半不再运行。因为请求在超出一定时间限制之后,就不会再获得任何信息。

将一部番剧的所有信息用一个类进行处理

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
class Anime:
def __init__(self, _id):
raw = scrape_data(_id)

if raw == '404':
self.score = 'error'
print('No data for')
else:
if raw['subject']['name_cn'] != '':
self.name = raw['subject']['name_cn']
else:
self.name = raw['subject']['name']
self.score = []
# self.image = raw['subject']['images']['grid']
self.air_date = raw['subject']['air_date']
self.score = self.get_score(raw)


def get_score(self, raw):
scores = {}
score_set = raw['history']
i = 0
isopen = False
while i < len(score_set)-1:
time = score_set[i]['recordedAt'][0:10]
i += 1
if compare_air_date(time, self.air_date) == 3:
continue
if 'rating' not in score_set[i]:
continue
now_score = calculate_score(score_set[i]['rating']['count'])
if not now_score:
self.air_date = time
continue
scores[time] = now_score
clock = datetime.datetime.strptime(time, '%Y-%m-%d') + datetime.timedelta(days=-1)
while clock.strftime('%Y-%m-%d') not in scores and isopen:
# (datetime.datetime.now() + datetime.timedelta(days=-1)).strftime("%Y-%m-%d %H:%M:%S")
scores[clock.strftime('%Y-%m-%d')] = now_score
clock += datetime.timedelta(days=-1)
# print(clock.strftime('%Y-%m-%d'))
isopen = True

return scores

类中存储的信息有: 动画名称开播时间(用于筛选较为有用的信息)各个时间段的评分。 其中也内置了一个方法get_score, 用于提取json中的评分信息。

获取番剧列表

我们获取到番剧列表的网站https://bgm.tv/anime/browser/airtime/2020?sort=rank&page=2,其中2020是可替换成各种年份的,而参数page代表了页码号。通过beautifulsoup来获取番剧列表中的元素,得到列表中每一个动画的id。

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
def get_url(year, page):
"""获取年份,页码对应url"""
return "https://bgm.tv/anime/browser/airtime/"+ str(year) + "?sort=rank&page=" + str(page)


def launch_to_page(year, page):
"""发送get请求"""
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0',
'Connection': 'close'}
html = requests.get(get_url(year, page), headers = headers).text
return html


def get_soup(year, page):
"""分析网页进行id的提取"""
html = launch_to_page(year, page)
soup = BeautifulSoup(html, 'html.parser')
soup_of_list = soup.find_all('li', class_=['item odd clearit', 'item even clearit'])
links = [li.find('a')['href'] for li in soup_of_list]
ids = [int(li[9:]) for li in links]
return ids


def get_anime_after(year):
"""得到某一年以后的每个番剧构成的列表"""
# 今年是2024!
ids = []
for y in range(year, 2025):
print("\n scraping anime page for year " + str(y) + "...")
for page in range(1, PAGES_LIMIT + 1):
print("scraping page: " + str(page) + "...")
ids = ids + get_soup(y, page)
return ids

在这里值得一提的,可能就是使用了beautifulsoup的get_soup函数。我们可以分析一下列表的html元素构成:

2022年的html元素

可以发现,为了做出斑马纹样式,它的列表是作出了奇偶性的分类,所以这两种class都在我们的考虑范围之内。

得到了每一个列表的元素之后,我们再获取它的跳转链接,用切片获取它的id就好了。

存储数据

写入csv文件

这里我们将所有数据写入csv文件。csv是一种纯文本文件,可以使用它来写入信息到表格中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def write_files():
# write field
fieldnames = ['name']
fieldnames.extend(pd.date_range(START_DATE, END_DATE).strftime("%Y-%m-%d").tolist())
with open('data.csv', 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
ids = get_anime_after(2018)
cnt = 0
for _id in ids:
cnt += 1
print("writing row: " + str(_id) + "(" + str(cnt) + "/" + str(len(ids)) + ")")
a = Anime(_id)
if a.score == 'error':
continue
row = a.score
row['name'] = a.name
writer.writerow(row)

这样子我们的爬虫就大功告成了。可是怎么样才能让我们获取到的数据显示出来更方便地看到呢?这里就需要数据可视化的作用了。

数据可视化

由于数据是由时间变化的,这里更适合制作一个条形图的视频,这里推荐用flourish

我们把自己的数据(csv文件)传入flourish,就可以直接得到视频了。

我们得到的视频截图

感谢你读到最后。你也可以点击此链接看到最后成品。


【爬虫】 谁才是近几年的番剧“神作”?
http://blog.bluspace.ren/2024/09/09/【爬虫】谁才是近几年的番剧“神作”?/
作者
Blauter
发布于
2024年9月9日
许可协议