声優歴何年目で「アイカツ！」シリーズの声優になったのかWikipediaをスクレイピングして確かめる

当記事は[アイカツ！ Advent Calendar 2019](https://adventar.org/calendars/4521)18日目の記事です。
昨日はgecko655さんの記事「[アイカツ！楽曲の音楽的特徴をSpotifyに教えてもらう](https://gecko655.hatenablog.com/entry/aikatsu_adc_2019_17)」でした。

## やりたいこと

「アイカツ！」シリーズを支える要素のひとつとして声優さんだと思います。
アイカツ！声優になるにはということで、一体何年目で「アイカツ！」シリーズの何らかの役にキャスティングされるのかというのをWikipediaをスクレイピングして取得します。

構成としては大きく2機能に分けています。
1.アニメのキャラクター情報に記載されている声優名を取得する。
2.1.で取得した声優名のキャスト情報を取得して加工する。

## 実装

### 1.アニメのキャラクター情報に記載されている声優名を取得

```python
import re
import requests
from urllib.request import urlopen
from urllib.parse import urlparse
import urllib.request
from bs4 import BeautifulSoup
from collections import OrderedDict
```

```python
# Wikipediaのアニメページから声優をgetする
def get_voice_actor(soup):
    list_voice_actors_tmp=[]
    replace_list = ['/歌-', '\[', '\（', "\n"]
    split_list = ['→', '、']

    for target in [i.text for i in soup.findAll('dd')]:
        if target.startswith("声"):
            # 不要文字列の除外
            voice_actor = target.replace('声 - ','')
            voice_actor = voice_actor.replace(" ","")       

            for i in replace_list:
                m = re.search(i, voice_actor)
                if(bool(m)):
                    voice_actor = voice_actor[0:m.start()]

            # 降板など複数キャストの分割処理
            split_flg = False
            for i in split_list:
                tmp_voice_actor_list = voice_actor.split(i)
                if len(tmp_voice_actor_list) > 1:
                    # extendするので重複するが、スクレイピング時点で重複しているためまとめて重複削除
                    list_voice_actors_tmp.extend(tmp_voice_actor_list)
                    split_flg = True
            if split_flg:
                continue

            list_voice_actors_tmp.append(voice_actor)
    return list_voice_actors_tmp
```

```python
#アイカツ声優の一覧を取得する
target_work_characters = ['アイカツ!の登場人物一覧','アイカツスターズ!', 'アイカツフレンズ!']

list_voice_actors = []
for character in target_work_characters:
    html = requests.get(r'https://ja.wikipedia.org/wiki/{}'.format(urllib.parse.quote_plus(character, encoding='utf-8')))
    list_voice_actors.extend(get_voice_actor(BeautifulSoup(html.text, "lxml")))
```

やっていることとしては、「アイカツ！」シリーズのキャラクターに記載されている声優名を取得。
声優名についているsuffixなどを除外する処理です。

結果としては以下のようなListが出力される形となります。

```
['諸星すみれ',
 '田所あずさ',
 '大橋彩香',
 '黒沢ともよ',
 '沼倉愛美',
 '安野希世乃',
 '三村ゆうな',
 '瀬戸麻沙美',
 '森谷里美',
 '下地紫野',
　　　:
　　　:
 '和久井優',
 '冨岡美沙子',
 '田中那美',
 '山岡ゆり',
 '斎賀みつき',
 '坂本くんぺい',
 '高橋伸也',
 '小野塚貴志',
 '森なな子']
```

output結果上同名のキャストが重複します。
実装上、諸星すみれさんのような複数作品に跨いでキャスティングされている方がいるためです。
重複削除はここでは実施せず、後続の処理で削除するようにしています。



キャスト降板等、１役に対し複数人いるものがあるので、分割してListにextendするようにしています。


## 2.1.で取得した声優名のキャスト情報を取得して加工する。

```python
# 声優のキャスト情報の取得
def get_cast(html):
    soup = BeautifulSoup(html, "lxml")

    list_cast=[]
    # アニメ・ゲームの単位でループ
    for extract_dl in soup.findAll('dl') :
        for extract_dl_child in extract_dl.children :
            #改行は除外
            if extract_dl_child.string == '\n':
                continue
            # 最初の行に年が設定されるので、設定用取得
            if extract_dl_child.string is not None:
                year = extract_dl_child.string
            # 作品とキャストを取得
            else:
                #作品分ループする
                for extract_li in extract_dl_child.findAll('li'):
                    extract_a = extract_li.find('a')
                    # Nonetypeのデータも取れるのではじく
                    if isinstance(extract_a,type(None)) == False:
                        title = extract_a.text
                        # キャラクター名を取得
                        title_char = str(extract_li.get_text())
                        character = title_char[title_char.find('（')+1:title_char.find('）')]
                        # 年,タイトル名,キャラクター名で1データにする。
                        list_cast.append("{},{},{}".format(year, title.replace(" ",""), character))                        
    return list_cast

# Wikipediaから声優情報をget
def get_html(target):
    
    sufffix_dict = {"声優": "_(%E5%A3%B0%E5%84%AA)","俳優": "_(%E4%BF%B3%E5%84%AA)",} 
    
    res = requests.get(r'https://ja.wikipedia.org/wiki/{}'.format(urllib.parse.quote_plus(target, encoding='utf-8')))
    
    if res.status_code == 200:
        return get_cast(res.text)

    # Pythonのバージョンによって順序保証されていないケースがあるので考慮
    for suffix in OrderedDict(sufffix_dict).values():
        print(suffix)
        res = requests.get(r'https://ja.wikipedia.org/wiki/{}{}'.format(urllib.parse.quote_plus(target, encoding='utf-8'),suffix))

        if res.status_code == 200:
            break
            
    return get_cast(res.text)

# 声優のキャスト情報の加工
def get_target_carrer(target_work, list_voice_actors):
    list_carrer =[]
    for target in set(list_voice_actors):
        # Wikipediaから声優情報をget
        list_cast = get_html(target)

        # 情報がとれなかったら次の声優へ
        if len(list_cast) == 0 :
            continue

        # 対象作品までの声優のキャリアを取得       
        # 対象作品までの作品数と経験年数を取得
        cast_year = None
        debut_year = None
        count_cast_num = 1
        for str_cast in sorted(list_cast):
            #print(str_cast)
            # デビュー年を取得する。年代は抽象的なので外す
            if not str_cast.split(',')[0].endswith("年代") and debut_year is None:
                debut_year = str_cast.split(',')[0]

            # 対象作品に初めてキャストされた年を取得
            if str_cast.split(',')[1] == target_work:
                cast_year = str_cast.split(',')[0]

            # 対象作品に初めてキャストされた年が取れたら、次の声優へ
            if cast_year is not None:
                target_work_carrer = int(cast_year.replace('年',''))  - int(debut_year.replace('年',''))+ 1
                list_carrer.append("{},{},{},{},{},{}".format(target_work,
                                                              target, 
                                                              debut_year, 
                                                              cast_year, 
                                                              str('{}年目'.format(target_work_carrer)),
                                                              str('{}作品'.format(count_cast_num))
                                                             )
                                  )
                break
            count_cast_num = count_cast_num + 1
    return list_carrer
```

```python
target_works_list = ['アイカツ!', 'アイカツスターズ!', 'アイカツフレンズ!']
list_va_carrer = []
for target_work in target_works_list:
    list_va_carrer.extend(get_target_carrer(target_work, list_voice_actors))
```

ここでは声優名から、その声優がキャスティングされたアニメ・ゲーム作品を取得してソート。
デビュー作から「アイカツ！」シリーズに最初にキャスティングされたときの声優歴とそれまでにキャスティングされた作品数を出しています。

outputとしてはこのような形です。

```
['アイカツ!,神田朱未,2000年,2013年,14年目,152作品',
 'アイカツ!,大橋彩香,2012年,2012年,1年目,3作品',
 'アイカツ!,能登麻美子,1998年,2012年,15年目,405作品',
 'アイカツ!,金元寿子,2009年,2015年,7年目,209作品',
 'アイカツ!,横尾まり,1980年,2013年,34年目,155作品',
 'アイカツ!,家中宏,1986年,2012年,27年目,152作品',
 'アイカツ!,長谷美希,2010年,2014年,5年目,11作品',
 'アイカツ!,洲崎綾,2010年,2014年,5年目,61作品',
 'アイカツ!,森なな子,2013年,2015年,3年目,6作品',
 'アイカツ!,森谷里美,2008年,2012年,5年目,28作品',
　　　                 :
　　　                 :
 'アイカツフレンズ!,山岡ゆり,2009年,2018年,10年目,66作品',
 'アイカツフレンズ!,久保田梨沙,2015年,2018年,4年目,26作品',
 'アイカツフレンズ!,保村真,2000年,2018年,19年目,206作品',
 'アイカツフレンズ!,長谷川育美,2016年,2018年,3年目,27作品',
 'アイカツフレンズ!,杉田智和,1999年,2019年,21年目,672作品',
 'アイカツフレンズ!,中恵光城,2014年,2018年,5年目,68作品',
 'アイカツフレンズ!,桑原由気,2013年,2018年,6年目,93作品']
```


声優歴の出し方については、最初にキャスティングされた役をデビュー年として、「アイカツ！」シリーズに最初にキャスティングされた年から減算することで出しています。

声優名だと「XXXX_(声優名)」みたいなケースが拾えないので、拾えるような実装をしています。


課題としては、逢来りんさんみたいなデビューしたての声優や、美山加恋さんのような俳優も流行られている方の場合、一般的な声優のWikipediaのフォートマットと異なっているのでそれが拾えないという点があります。
そこまで拾うような実装にするとさすがに個別実装が多くなりそうなので、やらないほうがいいかなと思いつつも、拾えなかった人が誰かくらいは分かるような実装にしたほうが良いかもと思いました。


### 一覧

結果をPandasで加工しつつoutput、エクセルで調整した一覧が以下になります。

```python
import pandas as pd
abc = pd.Series(list_va_carrer)
df = abc.str.split(',', expand=True)
df.columns = ["作品名", "声優名", "デビュー年", "配役年", "デビュー～配役まで(年数)","デビュー～配役まで（役数）"]
df.to_csv("aikatsu.csv")
```

<img width="559" alt="アイカツ!声優一覧.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150939/aa8c4660-84e2-cd19-701a-c4b456b7a717.png">


## さいごに

[アイカツ！ Advent Calendar 2019](https://adventar.org/calendars/4521)まだ数日空いていますので、是非お気軽にご参加ください！