カラオケに入ってない曲を知りたかったので頑張った

# 概要

カラオケに行って曲探して「あれ、ないじゃん」ってなるのめっちゃがっかりするんで嫌なんですよね。

今回はそんな悩みを解消できる(嘘)CLIアプリケーションを作りました。
使い方はREADMEに記載してます！
https://github.com/SugarShootingStar/song-exist-in-akashicRecords

これを使えば先に歌えないことがわかります。悲しい。

(例)DAMに入ってないシドの曲を検索した結果
<img width="400" alt="スクリーンショット 2018-09-13 13.36.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/264910/0f5dd32a-e89e-b344-d962-86798a2564c3.png">

(2018/09/13 追記)
以前公開していた画像の出力結果では、シドの「僕、ディナー」がDAMに存在していないと表示されていましたがこれは誤検知でした。
原因はカラオケサイトと歌詞サイトの読点(「、」「､」)が全角半角別々だったためだとわかりました。
この不具合はjaconvを使うことで解決できました。


## 使用技術
- [node.js(言語)](https://github.com/nodejs/node)
  - 環境を用意するのが楽なので。
- [yarn(パッケージマネージャー)](https://www.npmjs.com/package/yarn)
  - npmの上位互換です。ライブラリを手軽に導入できます。
- [babel(トランスパイラ)](https://www.npmjs.com/package/babel-cli)
  - まだES6で書いたことがなかったので使ってみました。
- [puppeteer(ライブラリ)](https://www.npmjs.com/package/puppeteer)
  - Google Chromeでスクレイピングできるヘッドレスブラウザです。同じようにスクレイピングできるphantomJSの上位互換です。
- [consola(ライブラリ)](https://www.npmjs.com/package/consola)
    - コンソールに出力するメッセージをカラフルにできます。なんかリッチにしたかったので使いました。
- [jaconv(ライブラリ)](https://www.npmjs.com/package/jaconv)
    - カラオケのサイトと歌詞サイトで一部記号などの表記ゆれがあったため、比較する際に統一するのに使いました。便利です。
  
## スクレイピング対象サイト

- カラオケサイト
  - [DAM](https://www.clubdam.com/app/search/searchKaraokeKeywordArtist.html)
  - [JoySound](https://www.joysound.com/web/search?ntx=1)
- 歌詞サイト
  - [歌ネット](https://www.uta-net.com/index.html)

## フロー

1. カラオケのサイトにアクセスし、アーティストのページへ遷移。最後のページまでクリックして曲一覧を配列で取得。
2. 歌詞サイトにアクセスし、アーティストのページへ遷移。曲一覧を配列で取得。(全曲1ページにまとめてあったので楽)
3. 歌詞サイトの配列からカラオケにある曲を除外し、そこから結果配列を生成。
4. 結果配列を標準出力する。

## 曲の比較について
カラオケサイトと歌詞サイトの曲をそのまま文字列比較すればいいと思ったんですが、曲によってスペースがあったりなかったり、記号が全角だったり半角だったり…とりあえずここらへんを統一する必要がありました。

- 記号や英数などの統一
    - jaconvの `normalize(string)` を使いました。 `全角英数記号を半角に、半角カタカナを全角に変換します。` って書いてます。便利な時代になりましたね。
- 空白の統一
    - 空白があれば全部詰めて比較するようにしました。空白があればsplitして配列作って、そのままjoinで文字列結合してます。

```javascript:helper.js

exports.sanitizeSong = function(song) {
  // 記号の全角、半角を統一
  let retSong = jaconv.normalize(song);
  // スペースの数をあわせる
  retSong = retSong.split(" ").join("");

  return retSong;
}
```

## 苦労したこと

- babelを使ってnode.jsでES6が動く環境を作るところ
  - あるあるだけど環境を作るのが一番しんどかった。幸いなことに検索すればだいたいのトラブルシューティングはできた。
  - puppeteerでは `async await` を使うので `babel-plugin-transform-runtime` を使用する必要があった。
  - node.jsで実行するだけならトランスパイルしたファイルを出力しなくていいので、 `babel-node ファイルパス` で動かせる。
- ページの遷移周り
  - puppeteerでは `page.click()` などでクリックイベントを起こすことができるのだが、クリックした後DOM要素が生成されるまで待たないとテキストなどが取得できない。初歩的だが結構ここで詰まった。
  - `page.click()` と `page.waitForNavigation` を `Promise.all` でくくったら上手く行った。下記はアーティストの曲一覧ページで次のページをクリックしたときの処理。
 
```JavaScript:dam.js
// 次のページ行く
await Promise.all([
  page.click(nextSelector),
  // waitUntilでDOM要素が生成されるまで待つのを指定
  page.waitForNavigation({timeout: 60000, waitUntil: "domcontentloaded"}),
]);
```
- ` page.evaluate ` 関係の動作

```JavaScript:dam.js
// ページ内の曲をすべて配列で取得
const songs = await page.evaluate(() => {
  const listSelector = "table.list > tbody > tr > td.song > a";
  const list = Array.from(document.querySelectorAll(listSelector));
  return list.map(data => data.innerText);
});
```
  - evaluate内ではヘッドレスブラウザ内で処理が実行されるため？中で `console.log` しても確認できないし、evaluate外で宣言された変数も使用できない。(undefinedになる。)

## 参考にした記事

- [puppeteerでの要素取得方法](https://qiita.com/go_sagawa/items/85f97deab7ccfdce53ea)
- [puppeteerでよく使うであろう処理の書き方](https://qiita.com/rh_taro/items/32bb6851303cbc613124)

## 小ネタ

<img width="728" alt="スクリーンショット 2018-09-12 21.29.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/264910/8fd32455-dc98-5c2a-5881-0e9e73486536.png">
Google Chromeだと検証ツール出して `Elements` の任意の場所で右クリックして `Copy selector` を押すと一瞬でセレクタがコピペできるので便利です。

## 備考&感想

まだ作ったばっかりなのでこれから改善していく予定です。とりあえずJoySoundでも取得できるようにする予定です。
スクレイピング楽しい！！！