歌詞と曲名の関係性

ゆるめの内容です。

先日、友人とのリモート飲み会でMr. Childrenのライブ映像を流していたら、 自然とイントロクイズをする流れになりました。

ただ酔っ払っていることもあって答えあぐねていると、 そのうち桜井さんが歌詞で答えを言っちゃうんですね。

ここでふと疑問に思ったのは、「歌詞の中に曲名の文字列を含む楽曲の割合はどのくらいだろう🤔」ということです。 そこで以前Pythonで書いたスクレイピングのコードを流用し、さくっと集計してみました。

使用したコード

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import time
import re
from urllib.parse import urljoin
import requests
import lxml.html
import argparse


URL_UTA_NET = 'http://www.uta-net.com/'


def main():
    """
    scrape_list_page(): artist の楽曲一覧から song ページの URL を取得
    scrape_song()     : 1つの song のページから曲名、アーティスト名、歌詞を取得
    extract_song_id()     : URL から曲IDを抽出
    """
    parser = argparse.ArgumentParser()
    parser.add_argument(
        'artist_id', help='Artist ID. (show uta-net ' + URL_UTA_NET + ')')
    args = parser.parse_args()

    artist_url = URL_UTA_NET + 'artist/' + args.artist_id

    count_contain = 0
    count_songs = 0
    artist_name = ''

    session = requests.Session()  # 楽曲情報を取得するのに使用するsession
    song_urls = scrape_song_list(artist_url)  # イテレータ

    for i, song_url in enumerate(song_urls):
        song_id = extract_song_id(song_url)
        #print(i, song_url)
        count_songs += 1

        print(i+1)

        response = session.get(song_url)
        song_info = scrape_song(response)
        artist_name = song_info['artist']

        print(song_info)

        if (does_contain_title(song_info)):
            count_contain += 1
            print(' Lyric contains title!!! ')
        else:
            print(' Lyric does not contain title. ')
        print()

        time.sleep(5)

    print()
    print('結果: %sは %d/%d 曲が歌詞にタイトルを含みます' %
          (artist_name, count_contain, count_songs))


def scrape_song_list(artist_url):
    """
    パーマリンク一覧の中から楽曲ごとのURLを抽出
    例えば、<td class="side td1"><a href="/song/69260/">... から "/song/69260"

    2020/04/26: 楽曲一覧が複数ページにまたがる場合に対応
    """

    # ページ数を取得
    response = requests.get(artist_url)
    num_page = int(re.search(r'全([0-9]+)ページ中', response.text).group(1))
    print('num_page: %s' % num_page)

    for i in range(1, num_page + 1):
        try:
            response = requests.get(artist_url + '/0/' + str(i) + '/')
            root = lxml.html.fromstring(response.content)
        except:
            break

        for a in root.cssselect('td.side.td1 a[href^="/song/"]'):
            url = urljoin(response.url, a.get('href'))
            yield url

    return


def scrape_song(response):
    """
    引数 response から曲名、アーティスト、作詞者、作曲者、歌詞を取得
    """
    root = lxml.html.fromstring(response.content)
    song = {'url': response.url,
            'key': extract_song_id(response.url),
            'title': root.cssselect('div.title h2')[0].text,
            'artist': root.cssselect('div.kashi_artist span[itemprop="byArtist name"]')[0].text,
            'lyricist': root.cssselect('div.artist_etc.clearfix h4')[0].text,
            'comporser': root.cssselect('div.artist_etc.clearfix h4')[1].text,
            }
    item = lxml.html.tostring(root.cssselect('#kashi_area')[0]).decode('utf-8')
    lyric = lxml.html.fromstring(item).text_content()
    song['lyric'] = lyric.replace('\u3000', ' ')

    return song


def extract_song_id(url):
    """
    URL から楽曲IDを抽出
    """
    return re.search(r'/song/([0-9]+)/$', url).group(1)


def does_contain_title(song_info):
    """
    TODO: 表記ゆれへの対応
    """
    return song_info['title'].strip() in song_info['lyric']


if __name__ == '__main__':
    main()

以前のコードを流用したので1,2時間で書けました。

やっていることとしては、

  1. 指定したアーティストの楽曲リストを歌詞サイトから取得
  2. 全楽曲のなかで、歌詞の中に曲名を含む割合を算出

実行結果

$ ./scrape_song.py 684    # uta-netでのアーティストID

(中略)

結果: Mr.Childrenは 118/235 曲が歌詞にタイトルを含みます (0.502128)

ほぼ50%でした。意外と少ない印象ですがどうでしょう。

注意点としては、完全一致で判定しているので表記ゆれはカウントされず、実際より低い割合になってしまうことです。 例えば、知らない人はいないであろう代表曲「innocent world」については、 曲名は英字で「innocent world」ですが、歌詞に含むのはカタカナで「イノセント ワールド」です。 この場合は「歌詞に曲名を含む」曲にカウントされません。

まあ集計のポリシーなので何が正しいとかはありませんが。 本音をいうと複雑な判定基準を設けるのが面倒だっただけです。 興味のある方はdoes_contain_title(song_info)をいじってみてください。

おまけ

他のアーティストに対しても集計してみた。

結果: BUMP OF CHICKENは 38/125 曲が歌詞にタイトルを含みます 0.304000
結果: 水樹奈々は 62/276 曲が歌詞にタイトルを含みます 0.224638
結果: FLOWは 57/176 曲が歌詞にタイトルを含みます 0.323864

作詞者の個性を考察してみると面白いと思います。 その意味ではアーティストごとではなく作詞者ごとに集計したほうがよいですね。

では。

my new gear...

近頃界隈で流行っている自作キーボードですが、私も見事にハマってしまいました。 どれくらいハマっているかというと、メルカリや通販サイトKBDfansを週4, 5回は巡回するほどです。

今回は私が作った3台のキーボードを紹介します、その魅力が少しでも伝われば幸いです。

といいつつ半分くらいは見せびらかしたいだけ

1台目: Quefrency

f:id:eggplant60:20200329182130j:plain:w400

最初に手を出したのはQuefrencyで、遊舎工房さんで購入しました。 選んだポイントは以下の通り。

  • 分割キーボードである
  • 60%相当である*1
  • 矢印キーがついている

分割なので最初は違和感が強いかな?と心配していましたが、私の場合は案外すんなり使うことができました。腕を広げてタイピングするのは新鮮でいいですね。

製作中はパーツを付ける順番を間違えて、70個のキースイッチのハンダを泣きながら剥がすなどのハプニングがありました。 初心者が手を出すにはやや難易度が高い品だったのかもしれません。てかちゃんとビルドガイド読もう。

2台目: TADA68

f:id:eggplant60:20200329182334j:plain:w400

アイボリーのキーキャップがクラシックな感じで非常に良い。こういう見た目が好み。

このキーボードでルブに初挑戦しました。 ルブというのは、キースイッチ1個1個に潤滑油を塗って打鍵感をなめらかにする作業です。 キースイッチ1個に大体1,2分かかるので、どんなに急いでも1時間以上かかります。地道なこった。

ただこのおかげで「コトコト」とした独特の打鍵音になり、満足のいく結果になりました。 軸はGateron Silent Red (軽めの静音リニア軸)だったのですが、 こういったリニア軸はルブの効果を得やすいのかもしれません。

現状、私が所有するキーボードの中で一番いい出来だと思っています。

3台目: GK64

f:id:eggplant60:20200329182419j:plain:w300 f:id:eggplant60:20200329182424j:plain:w300

ただ光らせたかった。それだけ。

昔は光らせる意味なくね?と思っていたんですが、実際やってみると目立つし楽しいです。 今は会社に置いてドヤ顔してます。

でも気になる点もいくつか。

  • 独自のソフトウェアでキーマップを変更する。QMKファームウェアと比較して自由度が低い
  • キーキャップがSAプロファイル *2で背が高いため、軸がぶれやすい

まあしょうがないですね。買う前にきちんと調べましょう。

おわりに

ということで私が所有するキーボードを紹介しました。 最後に、自作キーボードを始めようか迷ってる方にアドバイスします。

  • 制作難易度はものによって差がある。不安なら即売会やリアル店舗の店員さんに聞こう
  • 軸に注目が行きがちだけど、キーキャップのプロファイルも案外重要
  • ごちゃごちゃ書いたがやっぱり見た目は大事。一目惚れしたら買え(思考停止)

偉そうなこと書いてすみません。この記事がいいきっかけになればと思います。

ちなみに記事タイトルにある "gear" は不可算名詞らしいです。 3台紹介したから複数形にするところだった。

この記事はQuefrencyで書きました。

*1:キーボードのサイズのこと。テンキー付きのフルサイズを100%とする。60%はテンキーレスかつファンクションキーなしと思っておけば問題ない

*2:プロファイルとはキーキャップの形状のこと。SA, Cherry, OEM, DSAなどがある。私の推しはOEM

アニメ録画予約アプリを作ってみた① (アーキテクチャ編)

はじめに

はてなブログ初投稿です。最近業務でフロントエンド開発を行うことになりました。

そこで勉強がてらで作ったWebアプリの話をしたいと思います。全4回の記事になりますがよろしくお願いします。

何を作ったか

タイトルにもなっていますが、一言でいうと「TweetDeck風のアニメ録画アプリ」です。 ソースコードgithubにあります。

f:id:eggplant60:20200223181542p:plain
TweetDeck風 (再現度低め)

情報取得先の利用規約の関係で画像にぼかしを入れています。 

1週間先までのアニメ番組が曜日ごとに並んでいて、いいねボタンを押すと各自が所有しているNanseにクエリを投げて録画予約をします。

本アプリを開発したモチベーションとして、新しいクールが始まったときに、できるだけ楽に追いかけるアニメを選定できないか?があります。そのため、予約のたびに設定や確認をごちゃごちゃ出さず、ふぁぼる感覚で予約できるよう意識しています。

加えて、番組タイトルをクリックするとそのタイトルでGoogle画像検索を行う機能もあるので、 キャラデザで予約するかを決めることも可能です。いわゆるパケ買いみたいなもの。

※いまのところ外出先から予約することはできません。

アーキテクチャ

アプリの説明をこれ以上してもしょうがないし実際に触ってもらうのが一番早いと思うので、 以降は技術的な話題にフォーカスします。

アプリの構成要素は下図のとおりになります。ごく一般的なWeb 3層アーキテクチャといえなくもない…のか…?

f:id:eggplant60:20200223182807j:plain

予定よりゴツくなったアーキテクチャ

とくに言うことはありませんが1点だけ補足すると、 CHAN-TORUというサービスを利用してNasneを操作しています。 言い換えれば、本アプリはアニメ録画に特化したCHAN-TORUのラッパーみたいなものです。

誤算

上記の構成に落ち着くまでに3つほど誤算がありましたが、長いので読み飛ばしてもらって構いません。

誤算①:Nasneを直接操作できない

当初はCHAN-TORUを利用する予定はなく、APIを使って直接Nasneを操作できないか検討していました。 NasneのAPIを調査した記事もありましたので。

そこでWiresharkを使ってパケット解析してみたのですが、Nasneには操作によってSOAPとRESTという2種類のAPIが実装されているようです。

Nasne HOME (設定画面)で見られる内容は基本RESTで実装してあるのですが、その他の主要な情報参照やクエリはSOAPで実装されています。(番組表参照や、録画予約済み番組一覧取得などなど)

ということで、本アプリの開発には SOAP の仕様を理解するのが必須、となってしまい、一度は諦めそうになりました。

しかし、その後CHAN-TORUというサービスを見つけ、RESTのみ実装できる目処が立ったので無事解決(これには別の苦労があったのですが、次回書きます)。

なお、SOAPに立ち向かった偉大な先人のブログはこちら: https://moroya.hatenablog.jp/entry/2013/09/04/234513

誤算②:ブラウザからCHAN-TORUAPIを叩くとCORSエラー

これは単に私の知識不足でしたが、フロント側でAjax通信を使って異なるドメインの情報を取得することって、基本できないんですね。セキュリティ上の理由でブラウザがブロックする。

 結論としてはサーバサイドのNodeを用意して、こいつを仲介役にすることで解決しました。

誤算③:外部APIのレスポンスが遅い

 はじめはアプリ画面を開いたときにいちいちCHAN-TORUに番組表情報を取りに行っていましたが、はっきりいってレスポンスが遅く、ユーザビリティが良いものではありませんでした。加えて、画面のリロードのたびにAPIを叩くのではCHAN-TORU側にも負荷がかかります。

そこで、バッチ処理で1日1回だけ番組表を取得し、自前のDBにストアするようにしました。

一方で、予約済みの録画情報については、

1. 番組表よりデータ量の少ないこと

2. 更新タイミングが不定であること

により、バッチでDBに格納せずに毎回CHAN-TORUに問い合わせるようにしています。

 

このような誤算が重なって、はじめはフロントエンド開発の練習のつもりだったのが、 サーバサイドとDBまで用意するはめになってしまいました。まあいい勉強にはなりましたが。

次回以降の予定

今回はアプリの紹介と全体構成を軽く説明しました。 次回以降は技術的詳細についてもっと踏み込んで書く予定です。

  1. アーキテクチャ (本記事)

  2. サーバサイド - 外部APIの解析と利用 -

  3. フロントエンド - MVCアーキテクチャの実践 -

  4. Dockerによる環境構築 - 開発環境と本番環境の分離 -

3日坊主にならないように頑張ります。それでは。