概要
カラオケに行って曲探して「あれ、ないじゃん」ってなるのめっちゃがっかりするんで嫌なんですよね。
今回はそんな悩みを解消できる(嘘)CLIアプリケーションを作りました。
使い方はREADMEに記載してます!
https://github.com/SugarShootingStar/song-exist-in-akashicRecords
これを使えば先に歌えないことがわかります。悲しい。
(2018/09/13 追記)
以前公開していた画像の出力結果では、シドの「僕、ディナー」がDAMに存在していないと表示されていましたがこれは誤検知でした。
原因はカラオケサイトと歌詞サイトの読点(「、」「、」)が全角半角別々だったためだとわかりました。
この不具合はjaconvを使うことで解決できました。
使用技術
-
node.js(言語)
- 環境を用意するのが楽なので。
-
yarn(パッケージマネージャー)
- npmの上位互換です。ライブラリを手軽に導入できます。
-
babel(トランスパイラ)
- まだES6で書いたことがなかったので使ってみました。
-
puppeteer(ライブラリ)
- Google Chromeでスクレイピングできるヘッドレスブラウザです。同じようにスクレイピングできるphantomJSの上位互換です。
-
consola(ライブラリ)
- コンソールに出力するメッセージをカラフルにできます。なんかリッチにしたかったので使いました。
-
jaconv(ライブラリ)
- カラオケのサイトと歌詞サイトで一部記号などの表記ゆれがあったため、比較する際に統一するのに使いました。便利です。
スクレイピング対象サイト
フロー
- カラオケのサイトにアクセスし、アーティストのページへ遷移。最後のページまでクリックして曲一覧を配列で取得。
- 歌詞サイトにアクセスし、アーティストのページへ遷移。曲一覧を配列で取得。(全曲1ページにまとめてあったので楽)
- 歌詞サイトの配列からカラオケにある曲を除外し、そこから結果配列を生成。
- 結果配列を標準出力する。
曲の比較について
カラオケサイトと歌詞サイトの曲をそのまま文字列比較すればいいと思ったんですが、曲によってスペースがあったりなかったり、記号が全角だったり半角だったり…とりあえずここらへんを統一する必要がありました。
- 記号や英数などの統一
- jaconvの
normalize(string)
を使いました。全角英数記号を半角に、半角カタカナを全角に変換します。
って書いてます。便利な時代になりましたね。
- jaconvの
- 空白の統一
- 空白があれば全部詰めて比較するようにしました。空白があればsplitして配列作って、そのままjoinで文字列結合してます。
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
でくくったら上手く行った。下記はアーティストの曲一覧ページで次のページをクリックしたときの処理。
- puppeteerでは
dam.js
// 次のページ行く
await Promise.all([
page.click(nextSelector),
// waitUntilでDOM要素が生成されるまで待つのを指定
page.waitForNavigation({timeout: 60000, waitUntil: "domcontentloaded"}),
]);
-
page.evaluate
関係の動作
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になる。)
参考にした記事
小ネタ
Google Chromeだと検証ツール出して Elements
の任意の場所で右クリックして Copy selector
を押すと一瞬でセレクタがコピペできるので便利です。
備考&感想
まだ作ったばっかりなのでこれから改善していく予定です。とりあえずJoySoundでも取得できるようにする予定です。
スクレイピング楽しい!!!