データ収集からAI開発、Webアプリ公開まで全てPythonで済ませた話（2.データ整形編）

# はじめに
今回の記事はシリーズ物です。他の記事はこちら↓

[0.設計編(キーを判別するAIとは？)](https://qiita.com/hanata/items/38163b6dddbc2ec8812e)
[1.データ収集(クローリング)](https://qiita.com/hanata/items/cbc6cc377321a2880f5b)
[2.データ整形(スクレイピング)](https://qiita.com/hanata/items/692f98ee83295614599f)←この記事です。
[3.機械学習を用いたAIの開発](https://qiita.com/hanata/items/d1fb6241cccf405bbc58)
[4.Djangoを用いたWebアプリ開発](https://qiita.com/hanata/items/968040a366c9bb1dbc93)


この記事では、クローリングしたhtmlファイルから、教師データを作成するまでの過程を書いていきます。またコードに関するデータを扱いやすくするため、簡単なクラス設計を行いました。

# 音に関するクラスを作る
htmlファイルからコードの文字列を抜き出して、コードの出現頻度をカウントしていくのですが、いくつかやらなければいけないことがあります。

* コードの文字列（CmやDm7）から、余計な情報を省く。
コードの出現頻度をカウントしていくのですが、7thやdimなどの細かい部分まで考慮してカウントしていくと、説明変数が多くなるし、スパースなデータ（0が多いデータ）になってしまいます。そのため今回は、「ルート音」と「メジャーコードかマイナーコードかのフラグ」を元にカウントしていきます。

* J-Total Musicに掲載されているコードは、ギターで弾きやすくするために、転調された状態でコードが記載されている場合がある。
そのままカウントしてしまうと駄目なので、原曲キーに転調させてカウントする必要があります。幸い、J-Total Musicでは「どれだけキーをずらせば原曲キーに戻るか」という値が書いてあるので、それを元に転調しながら、コードをカウントしていきます。

もちろん「情報を省く」ことや「コードを転調させる」メソッドを書いていけばよいのですが、見通しが悪くなるので簡単なクラスを作ってみようと思います。

# クラスたち
## Noteクラス（音名に関するクラス）

```python
class Note:
    name_lst = ['C', 'C#/D♭', 'D', 'D#/E♭', 'E', 'F', 'F#/G♭', 'G', 'G#/A♭', 'A', 'A#/B♭', 'B']

    def __init__(self, s):
        if len(s) == 1:
            self.name = s
        else:
            for index, c in enumerate(Note.name_lst):
                if len(c) != 1:
                    c_split = c.split('/')
                    if s in c_split:
                        self.name = c

    def transpose(self, step):
        if step == 0:
            return self
        prev_index = Note.name_lst.index(self.name)
        now_index = (prev_index + step) % 12
        self.name = Note.name_lst[now_index]
        return self

    def __str__(self):
        return self.name
```

name_lstというクラス変数は、以前の記事で書いた12種類の音のリストになっています。
transposeメソッドを使用することで、音を指定した幅だけ上下させることができます。
初期化メソッドですが、今回はエラー処理の対応は書いておりません。正しい値が入力されている前提で書かれています。

## Chordクラス（コードに関するクラス）

```python
class Chord:
    # s = 'Dm7' など受け取る
    def __init__(self, s):
        # コードのルート音
        if len(s) == 1:
            self.root = Note(s)
        else:
            if s[1] == '#' or s[1] == '♭':
                self.root = Note(s[0:2])
            else:
                self.root = Note(s[0])

        # コードがメジャーかマイナーか
        if 'm' in s:
            self.is_major = False
        else:
            self.is_major = True

    def __str__(self):
        if self.is_major:
            return '{}_Major'.format(self.root.name)
        else:
            return '{}_minor'.format(self.root.name)
```
コードに関する文字列を受け取って、インスタンスが初期化されます。
上で述べたように、今回は「ルート音」と「メジャーコードかマイナーコードか」の情報だけ抽出します。

## Songクラス（曲に関するクラス）

```python
class Song:
    def __init__(self, name, key):
        """
        コンストラクタ
        :param name: str
        :param key: Chordのインスタンス
        """
        # 曲名
        self.name = name
        self.chord_count_dict = {
            'name': name,
            'key' : key,
            'C_Major': 0,
            'C_minor': 0,
            'C#/D♭_Major': 0,
            'C#/D♭_minor': 0,
            'D_Major': 0,
            'D_minor': 0,
            'D#/E♭_Major': 0,
            'D#/E♭_minor': 0,
            'E_Major': 0,
            'E_minor': 0,
            'F_Major': 0,
            'F_minor': 0,
            'F#/G♭_Major': 0,
            'F#/G♭_minor': 0,
            'G_Major': 0,
            'G_minor': 0,
            'G#/A♭_Major': 0,
            'G#/A♭_minor': 0,
            'A_Major': 0,
            'A_minor': 0,
            'A#/B♭_Major': 0,
            'A#/B♭_minor': 0,
            'B_Major': 0,
            'B_minor': 0,
        }
        self.original_key = key

    def append_chord(self, c):
        self.chord_count_dict[str(c)] += 1

    def to_DataFrame(self):
        return pd.DataFrame.from_dict(self.chord_count_dict, orient='index').T

```
インスタンス変数で、曲名・原曲キー・コード出現回数をあわせた辞書を持ちます。
コードのカウントは **append_chordメソッド**で行います。
最終的にデータフレームで出力するために**to_DataFrameメソッド**を用意しています。インスタンス変数の辞書をdfに変換してreturnするだけの簡単なメソッドです。


# スクレイピング
以上のクラスたちを使用しながら、必要な情報を抜き出してデータフレームを作っていきます。
流れとしては

1. htmlファイルからキーに関する情報を取得、取得できない場合スキップ
たまにキーの情報が書いてない場合があります。そういう曲は迷いなくスキップしていきます。

2. コードが書いてあるタグからコードを文字列で取得。Chordインスタンスを作成しながら、Songインスタンスにappendする

という感じになっております。

## コード

```python
# ライブラリ
import glob
import os
from bs4 import BeautifulSoup
import re
import pandas as pd
import codecs
from tqdm import tqdm

# 自作クラスのインポート
from music_class import Note, Chord, Song


# htmlからコードを抽出するときの正規表現パターン
CHORD_PATTERN = r'[A-G]{1}[#,♭]?[m]?'


# htmlからキーに関する情報を取得
def getKeyInfo(soup):
    info = soup.find_all('font', size='3')
    song_info = None
    
    # key情報が曲ごとにまちまちなので、調べる
    for i in info:
        if 'Original' in i.text:
            song_info = i.text
    # key情報がなかったらスキップ
    if song_info is None:
        return None, None, True

    # transpose幅を求める
    if '半音' in song_info:
        transpose_step = -1
    else:
        try:
            transpose_step = int(re.sub(r'\D', '', song_info))
        except:
            return None, None, True
    # original keyを文字列で取得
    try:
        original_key_str = re.search(CHORD_PATTERN, song_info).group()
    except:
        return None, None, True

    return original_key_str, transpose_step, False


def main():
    # 全htmlファイルを取得
    html_files = [p for p in glob.glob('html/**', recursive=True) if os.path.isfile(p)]

    # 出力用df
    ans_df = pd.DataFrame()

    # ファイルごと処理
    for html in tqdm(html_files):

        # ファイルを開く
        with codecs.open(html, 'r', 'shift-jis', 'ignore') as f:
            soup = BeautifulSoup(f, 'html.parser')
            song_name = soup.find('title').text.split('/')[0] # 曲名取得

            # keyや転調幅がいくつか取得する
            original_key_str, transpose_step, continue_flg = getKeyInfo(soup)

            # キーの情報が取得できない場合、スキップする
            if continue_flg:
                print('{}　はデータに不備があり、教師データとして使用できません'.format(song_name))
                continue

            # 曲インスタンスを作成
            song = Song(song_name, Chord(original_key_str))

            # コードが書いてあるタグをすべて取得
            chord_lst = soup.find_all('a', href=re.compile("^JavaScript:jump_1"))

            for c in chord_lst:
                # 文字列のコードからコードインスタンスを作成
                try:
                    chord_str = re.search(CHORD_PATTERN, c.text).group()
                except:
                    continue
                # Chordインスタンスを作成
                chord = Chord(chord_str)

                # コードをtransposeしてから、songインスタンスにappendして、コードをカウントする
                chord.root = chord.root.transpose(transpose_step)
                song.append_chord(chord)

            # df化したのち、出力用dfにappendする
            song_df = song.to_DataFrame()
            ans_df = ans_df.append(song_df)

    ans_df.to_csv('train.csv', encoding='shift-jis', index=False)



main()

```

# 教師データ完成！
こんなCSVが出来上がります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405491/64fa0582-fa4b-5fbe-dcb0-62375899ce9b.png)
小さくと見づらいですが、ちゃんとできています！

## データを眺めてみる
少しデータを覗いてみます。今回集めた曲たちの中で、各キーの曲数をグラフ化してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405491/e5f64fe6-6d7f-69c9-efeb-a7f13fb1c0a1.png)
1位はG（ト長調）でした。2位はC（ハ長調）ですね。その後も長調が続いていきます。今回集めたデータの中では、短調は全体的に少なめな傾向があります。
1位のGと最下位のG#/A♭mでは10倍近くの差があります。びっくりですね。

次はダイアトニックコードの出現割合を見てみましょう。キーCの曲で確認してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405491/8466001f-b783-2841-c37b-6120e1d4e9ba.png)
すごい見づらいのですが

一番下の層の青色は　C
下から二番目の黄緑色は　Dm
真ん中らへんの濃い青色は　F
その上の灰色っぽいのは　G
上のオレンジ色は　Am

を表しています。キーCのダイアトニックコードはC,Dm,Em,F,G,Am,Bm7-5なので、たしかにダイアトニックコードが他のコードに比べて頻繁に使用されているのが分かると思います。

# 次の記事はこちら
いよいよ機械学習モデル作成に入っていきます！

[3.xgboostを用いたAIの開発]()
[4.Djangoを用いたWebアプリ開発](https://qiita.com/hanata/items/968040a366c9bb1dbc93)