アニメのキャラクターページをBeautifulSoupとSeleniumでスクレイピングして、アニメ顔を分類する


アイカツ！シリーズ分析結果の描画でキャラクターの顔写真を使いたいなあと思ったけど、人数が多くて手運用が面倒くさい。
なのでBeautifulSoupでスクレイピング、SeleniumでPrintScreenを取得。
PrintScreenからOpenCVでキャラクターの顔を切り取るまで自動でやることにした。

## スクレイピング部

### まずは事前準備系


```python
from urllib import request
from bs4 import BeautifulSoup
from selenium import webdriver
import pandas as pd
import time
import os
import shutil
import itertools

# OpenCVは日本語ファイル名を許容していないのでマッピング用のファイルをロード
df=pd.read_csv("C:/XXXX/aikatsu_name_romaji_mapping.tsv", sep='\t', engine='python', encoding="utf-8")

# Chromeのドライバを読み込み
driver = webdriver.Chrome("C:/XXXX/chromedriver/chromedriver.exe") 

# スクレイピングするURLをtupleで持つ
character_urls =(
    "http://www.aikatsu.net/01/character/index.html",
    "http://www.aikatsu.net/02/character/index.html",
    "http://www.aikatsu.net/03/character/index.html",
    "http://www.aikatsu.net/aikatsustars_01/character/index.html",
    "http://www.aikatsu.net/aikatsustars_02/character/index.html",
    "http://www.aikatsu.net/aikatsufriends_01/character/",
    "http://www.aikatsu.net/aikatsufriends_02/character/",
    "http://www.aikatsu.net/character/"
)

# PrintScreen格納用のディレクトリの作成
target_dir = "C:/XXXX/download/"

if os.path.isdir(target_dir):
    shutil.rmtree(target_dir)
    time.sleep(1)
os.mkdir(target_dir)
```
ディレクトリ作成部分くらい関数化したほうが良かったかもしれない。

マッピングはこんな感じの簡素なつくりにしている。

<img width="174" alt="scraping_aikatsu3.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150939/14735c7f-14b6-39af-0198-7cb71a350361.png">

Pandasを使っているのは、対象キャラクタ数が67人程度でDBとかリッチにする必要がない、知見だけでさくっと作れるという理由だけです。


### Selenium使用時のあれこれ

Selenium使うと大体環境変数にdriverの格納先を設定するけど、使い捨てツールなのでそこまでリッチである必要性がない。
ということで以下参考にしつつ、driverをべた書きしている。
[たった3行のpythonで始めるSelenium入門](https://qiita.com/mastar_3104/items/0a1ce2bfa1d29287bc35)

また実行時に以下エラーが出た。
`WebDriverError: unknown error: Runtime.executionContextCreated has invalid` 
これは使用しているdriverのバージョンがchromeのバージョンと違うので、chromeのバージョンにあわせてあげると解決する。


### スクレイピング実施部

```python
for character_url in character_urls:
    html = request.urlopen(character_url)
    soup = BeautifulSoup(html, "html.parser")
    
    # 各キャラクター紹介の情報を取ってくる
    characters=soup.find_all("a")
    idol_names = [i.find('img') for i in characters]
    urls = [i.get('href') for i in characters]
    
    character_url_prefix=character_url.split("index.html")
    
    for i, j in zip(idol_names, urls):
        # altタグが正しく取れない場合は処理をはじく
        if i == None:
            continue
        # キャラクター以外の情報をはじく
        if j.startswith("http") or j.startswith("../")  or j.startswith("index"):
            continue

        idol_name = i.get("alt").replace("　","").replace(" ","")
        print(idol_name)
        
        # seleniumのページ表示と調整
        driver.get(character_url_prefix[0]+j) 
        driver.set_window_size(1250, 1036)
        driver.execute_script("document.body.style.zoom='90%'")

        # 白百合かぐやはaltの情報が空なので固定値をセット
        if idol_name == "":
            idol_name = "白百合かぐや"
            
        # OpenCVは日本語名が使えないので、ローマ字に変換する
        idol_name_romaji = df[df["character"]==idol_name]["romaji"].values[0]

        file_name="{}{}.png".format(target_dir, idol_name_romaji)

        # すでに同名ファイルが存在している場合、リネームする
        if os.path.exists(file_name):
            for i in itertools.count(1):
                newname = '{} ({})'.format(idol_name_romaji, i)
                file_name="{}{}.png".format(target_dir, newname)

                # 同名ファイルが存在しなければ終了
                if not os.path.exists(file_name):
                    break
                    
        # webページ遷移時のエフェクト回避のため少し長めにスリープ時間をセット
        time.sleep(5)            
        driver.save_screenshot(file_name)

driver.quit()

```

こんな感じでデータを取得することができる。
もともとはaltの日本語名をファイル名に付けていたんだけど、それだとOpenCVで読み込みできないので、わざわざ変換かけてローマ字表記に変換している。(ローマ字は適当なので間違えてるかも)

<img width="978" alt="scraping_aikatsu.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150939/bf6f72ec-fd6d-18d4-ff17-27ab42d779e5.png">



またキャラクター名(idol_names)を取得する際に以下みたいにNoneが取れてきてしまう。
各キャラクターのURLとキャラクター名をzipでループするので、要素数をそろえる必要があるので、ループ前ではなく、中で弾くようにしている。

```
[<img alt="アイカツオンパレード！" src="../images/logo.png"/>,
 <img alt="アイカツオンパレード！通信" src="../images/bt-aikatsuonparadecom.png"/>,
 <img alt="アイカツオンパレード！とは" src="../images/bt-aikatsuonparade.png"/>,
 <img alt="放送情報" src="../images/bt-tvinfo.png"/>,
 <img alt="キャラクター" src="../images/bt-character.png"/>,
 <img alt="おはなし" src="../images/bt-story.png"/>,
 <img alt="CD" src="../images/bt-cd.png"/>,
 <img alt="BD/DVD" src="../images/bt-bddvd.png"/>,
 <img alt="NEWS" src="../images/bt-news.png"/>,
 <img alt="TOP" src="../images/bt-top.png"/>,
 <img alt="姫石らき" src="images/bt-raki.png"/>,
 <img alt="友希あいね" src="images/bt-aine.png"/>,
 <img alt="湊みお" src="images/bt-mio.png"/>,
 <img alt="星宮いちご" src="images/bt-ichigo.png"/>,
 <img alt="大空あかり" src="images/bt-akari.png"/>,
 <img alt="虹野ゆめ" src="images/bt-yume.png"/>,
 <img alt="BANDAINAMCO Pictures" height="53" src="../images/bnp.png" width="118"/>,
 None]
```

### OpenCV部

```python
import os
import cv2
from pathlib import Path

# ディレクトリの作成
download_dir = '{0}parse/'.format(target_dir)

if os.path.isdir(download_dir):
    shutil.rmtree(download_dir)
    time.sleep(1)
os.mkdir(download_dir)

# 特徴量ファイルをもとに分類器を作成
classifier = cv2.CascadeClassifier('C:/XXX/lbpcascade_animeface.xml')

# スクレイピングしたディレクトリのファイルを取得
p = Path(target_dir)

for i in list(p.glob("*.png")): 
    # 顔の検出
    image = cv2.imread(i.as_posix())
    
    # ディレクトリの作成
    file_tmp=i.as_posix().split("/")
    parse_dir = '{0}{1}/'.format(download_dir, file_tmp[len(file_tmp)-1:len(file_tmp)][0].split(".")[0])
    os.mkdir(parse_dir)
    
    # グレースケール化
    gray_image = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
    faces = classifier.detectMultiScale(gray_image)

    for i, (x,y,w,h) in enumerate(faces):
        # 一人ずつ顔を切り抜く。長方形にするのでy座標を調整
        face_image = image[y-50:y+h, x:x+w]
        output_path = '{0}{1}.png'.format(parse_dir, i)
        # 書き込み
        cv2.imwrite(output_path ,face_image)

```

OpenCVは長方形にしたかったので、座標を少しいじっているくらいで以下の内容のままです。
[OpenCVでアニメの顔検出](https://qiita.com/mczkzk/items/fda37ac4f9ddab2d7f45)

<img width="971" alt="scraping_aikatsu2.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150939/a65153fd-0c1c-ad05-3b08-1998859e738a.png">

こんな感じで取れます。
ポーズの関係で何人か分類器ではキャラクターと特定されなかったものが見受けられる。
数名なので、さすがにそれは手動でやるしかないかなー。