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

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

[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)

# 前回記事（設計）のまとめ
前回記事の内容をまとめると

* 曲のキーが決まることで、曲中で頻繁に使用されるコードが決まる
* 逆向きに考えると、曲中で使用されているコードをカウントすることで、キーを求めることができそう

これを機械学習タスクとするために

* 入力：曲中で使用されているコードをカウントしたデータ
* 出力：曲のキー

となるモデルを作成しようということでした。

# データを集める
必要なデータが決まったので、データを集めなければいけません。コード譜が掲載されているサイトからWebスクレイピングしようと思うのですが、コード譜掲載サイトはいくつかあります。

## 無料で使えるコード譜掲載サイト
1. [U-fret](https://www.ufret.jp/)  
コード譜と言ったらこのサイト。頻繁に更新されており、自動スクロール機能が使いやすい。
2. [J-Total Music](https://music.j-total.net/)  
古くから存在しているサイト。曲数多い。U-fretが登場する前はよく使用していました。
3. [楽器.me](https://gakufu.gakki.me/)  
比較的新しめのサイト。広告等多いものの、掲載曲数も数多い。


有名なものは上記の3つです。最大手？のU-fretを使用したいのですが、今回は**J-Total Music**を使用します。なぜならJ-Total Musicは他のサイトと違って **曲のキーも一緒に掲載している** からです。要は正解ラベルが掲載されているということです。

# クローリングの流れ
1. [アーティスト検索ページ](https://music.j-total.net/a_search/)から、五十音のリンクを取得
2. [五十音のアーティスト一覧ページ](https://music.j-total.net/a_search/a.html)から、各アーティストの曲一覧リンクを取得
3. [アーティストの曲一覧ページ](https://music.j-total.net/db/search.cgi?mode=search&page=1&sort=ac_new&word=%89%CC%81Faiko/&method=and)から掲載曲数を取得。そこから最大ページ数を割り出す。
4. 最大ページ数をもとに、ページめくりをしながら、各アーティストの楽曲のリンクを取得し、HTMLをダウンロードする


## コード
### ライブラリ

```python
import os
from bs4 import BeautifulSoup
import requests
import urllib.request
from tqdm import tqdm
import math
import time
import re
```

### 1. [アーティスト検索ページ](https://music.j-total.net/a_search/)から、五十音のリンクを取得

```python
# 保存先作成
save_dir = './html/'
os.makedirs(save_dir, exist_ok=True)

# アーティスト検索ページ
url = 'https://music.j-total.net/a_search/'
r = requests.get(url)
soup = BeautifulSoup(r.content, 'html.parser')

# エンコーディングをセット（shift-jis）
r.encoding = r.apparent_encoding

# 各五十音のリンクを取得
moji_links = []
gojyuon = soup.find_all('select')
for g in gojyuon:
    moji_lst = g.find_all('option')[1:] # 最初の要素はリンクが含まれていない
    for moji in moji_lst:
        moji_links.append(moji.get('value'))
```

### 2. [五十音のアーティスト一覧ページ](https://music.j-total.net/a_search/a.html)から、各アーティストの曲一覧リンクを取得

```python
# 頭文字ごと処理
for moji_link in tqdm(moji_links):
    # 各文字のURL
    moji_url = url + moji_link
    
    # html取得
    r_moji = requests.get(moji_url)
    soup_moji = BeautifulSoup(r_moji.content, 'html.parser')

    # アーティストのリンクを取得して、　アーティスト名：リンク　となる辞書を作成する
    artist_links = soup_moji.find_all('a', href=re.compile("^//music.j-total.net/db/search.cgi"))
    artist_link_dict = {}
    for link in artist_links:
        name = link.text.replace('\n', '').replace(' ', '')
        if len(name) == 0:
            continue
        artist_link_dict[name] = 'http:' + link.get('href')
```

アーティスト名：リンク　となるように辞書を作成していきます。

### 3. [アーティストの曲一覧ページ](https://music.j-total.net/db/search.cgi?mode=search&page=1&sort=ac_new&word=%89%CC%81Faiko/&method=and)から掲載曲数を取得。そこから最大ページ数を割り出す。

```python
for artist in artist_link_dict:
    # 曲数をカウントする
    song_cnt = 0
    
    # アーティストごとにhtmlを保存するフォルダを作成する
    artist_dir = save_dir + artist
    os.makedirs(artist_dir, exist_ok=True)

    # アーティストの曲一覧ページを開いてhtml取得
    ar_url = artist_link_dict[artist]
    try:
        r_ar = requests.get(ar_url, timeout=WAITING_TIME)
    except:
        print('{} skipされた'.format(artist))
        continue
    soup_ar = BeautifulSoup(r_ar.content, 'html.parser')

    # 歌手の総曲数を求める
    pg = soup_ar.find_all('font', size='3')[-1]
    pg_string = pg.text.split(' ')
    total_num_index = pg_string.index('件中') - 1
    total_num = int(pg_string[total_num_index])

    # 総曲数からページ数を求める
    max_page = math.ceil(total_num / 20)
```
アーティストの曲一覧を開く際、どうやらDBを叩いている様子なので、タイムアウトが発生して処理が中断することが多々ありました。
それを防ぐために、requests.getの際に待つ時間を設定し、例外処理を書いています。

### 4. 最大ページ数をもとに、ページめくりをしながら、各アーティストの楽曲のリンクを取得し、HTMLをダウンロードする

```python
for artist in artist_link_dict:

    ######

    # ページごと処理
    for p in range(1, max_page+1):
        page_url = ar_url + '&page={}'.format(p)
        try:
            r_song_lst = requests.get(page_url, timeout=WAITING_TIME)
        except:
            print('{} {} skipされた'.format(artist, p))
            continue
        soup_song_lst = BeautifulSoup(r_song_lst.content, 'html.parser')

        # そのページの全曲URLを取得
        song_links = soup_song_lst.find_all('a', href=re.compile("^//music.j-total.net/db/rank.cgi\?mode"), target='')

        # 曲ごと処理
        for s_link in song_links:
            s_url = 'http:' + s_link.get('href')
            song_name = s_link.find('b').text

            # 曲名.htmlで保存したいが、曲名に変な文字が入ってる場合はsong_cntで代用する
            try:
                data = urllib.request.urlopen(s_url, timeout=WAITING_TIME).read()
                try:
                    with open(artist_dir + '/{}.html'.format(song_name), mode='wb') as ht:
                        ht.write(data)
                except:
                    with open(artist_dir + '/{}.html'.format(song_cnt), mode='wb') as ht:
                        ht.write(data)
            except:
                print('{} {} {} skipされた'.format(artist, p, s_url))
            song_cnt += 1
            time.sleep(1) # お約束

    print('{} ok'.format(artist))
```

曲名.htmlでダウンロードしたいのですが、ファイル名にふさわしくない文字が使用されているケースがあります。そんな時は、song_cnt（アーティストの中で何番目の曲か）をファイル名に代用しています。

# 収集結果
PCをつけっぱなしでクローリングを実行するのですが、アーティスト名関連で度々エラーが起こってしまい、全アーティストを確認するのに3日近くかかってしまいました。
また、アーティストによってはタイムアウトでスキップされたものもあります。ただ、スキップされた曲はそこまで多くない（全体の1%程度）ので、スキップされたものは素直に諦めることにします。

集まったhtmlファイルは全部で **23043件**でした。思ったより少なかったので、いずれクローリングし直そうと思いますが、データ数としては十分と思われます。

次回は収集したhtmlから、必要なデータを抽出していきます。またコードの情報を扱うために、クラス設計も行っていきます。

# 次の記事はこちら

[2.データ整形(スクレイピング)](https://qiita.com/hanata/items/692f98ee83295614599f)←次の記事です
[3.xgboostを用いたAIの開発]()
[4.Djangoを用いたWebアプリ開発](https://qiita.com/hanata/items/968040a366c9bb1dbc93)