はじめに
今回の記事はシリーズ物です。他の記事はこちら↓
0.設計編(キーを判別するAIとは?)
1.データ収集(クローリング)
2.データ整形(スクレイピング)←この記事です。
3.機械学習を用いたAIの開発
4.Djangoを用いたWebアプリ開発
この記事では、クローリングしたhtmlファイルから、教師データを作成するまでの過程を書いていきます。またコードに関するデータを扱いやすくするため、簡単なクラス設計を行いました。
音に関するクラスを作る
htmlファイルからコードの文字列を抜き出して、コードの出現頻度をカウントしていくのですが、いくつかやらなければいけないことがあります。
-
コードの文字列(CmやDm7)から、余計な情報を省く。
コードの出現頻度をカウントしていくのですが、7thやdimなどの細かい部分まで考慮してカウントしていくと、説明変数が多くなるし、スパースなデータ(0が多いデータ)になってしまいます。そのため今回は、「ルート音」と「メジャーコードかマイナーコードかのフラグ」を元にカウントしていきます。 -
J-Total Musicに掲載されているコードは、ギターで弾きやすくするために、転調された状態でコードが記載されている場合がある。
そのままカウントしてしまうと駄目なので、原曲キーに転調させてカウントする必要があります。幸い、J-Total Musicでは「どれだけキーをずらせば原曲キーに戻るか」という値が書いてあるので、それを元に転調しながら、コードをカウントしていきます。
もちろん「情報を省く」ことや「コードを転調させる」メソッドを書いていけばよいのですが、見通しが悪くなるので簡単なクラスを作ってみようと思います。
クラスたち
Noteクラス(音名に関するクラス)
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クラス(コードに関するクラス)
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クラス(曲に関するクラス)
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するだけの簡単なメソッドです。
スクレイピング
以上のクラスたちを使用しながら、必要な情報を抜き出してデータフレームを作っていきます。
流れとしては
-
htmlファイルからキーに関する情報を取得、取得できない場合スキップ
たまにキーの情報が書いてない場合があります。そういう曲は迷いなくスキップしていきます。 -
コードが書いてあるタグからコードを文字列で取得。Chordインスタンスを作成しながら、Songインスタンスにappendする
という感じになっております。
コード
# ライブラリ
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が出来上がります。
小さくと見づらいですが、ちゃんとできています!
データを眺めてみる
少しデータを覗いてみます。今回集めた曲たちの中で、各キーの曲数をグラフ化してみます。
1位はG(ト長調)でした。2位はC(ハ長調)ですね。その後も長調が続いていきます。今回集めたデータの中では、短調は全体的に少なめな傾向があります。
1位のGと最下位のG#/A♭mでは10倍近くの差があります。びっくりですね。
次はダイアトニックコードの出現割合を見てみましょう。キーCの曲で確認してみます。
すごい見づらいのですが
一番下の層の青色は C
下から二番目の黄緑色は Dm
真ん中らへんの濃い青色は F
その上の灰色っぽいのは G
上のオレンジ色は Am
を表しています。キーCのダイアトニックコードはC,Dm,Em,F,G,Am,Bm7-5なので、たしかにダイアトニックコードが他のコードに比べて頻繁に使用されているのが分かると思います。
次の記事はこちら
いよいよ機械学習モデル作成に入っていきます!