「 立花理香のブログのタイトルの「ボカロ曲感」率を算出する」の裏側

  • 10
    Like
  • 0
    Comment

はじめに

こんにちは。@albno273 です。
このネタを思いついた時に内容が技術2割ネタ8割になるだろうと察し、はてなブログに書くか Qiita に書くか悩んだのですが、最終的に

  • コードを除いた全体の内容をはてなブログで
  • コード等技術っぽいものだけを Qiita で

書くことにしました。
なので「なんだこの記事」という方は
はてなブログの記事
もご覧ください。

きっかけ

(深夜3時の思いつき)

立花理香って誰

  • 京都女子大学大学院博士課程満期退学(と一説には言われている)
  • 関西ローカルでタレントとかモデルとかリポーターやってた
  • 小早川紗枝ちゃん
  • 様子のおかしい関西人 [検索]

「ボカロ曲感」の定義 // 提唱: オレ

  • 広義
    「漢字 or ひらがな ワンワード + カタカナ ワンワード」の枠で「一口で言える語句」
    いーあるふぁんくらぶも含まれるといえば含まれる感じ
  • 狭義
    「漢字 2文字 + カタカナ ワンワード」
    やっぱり真っ先に思いつくのが想像フォレスト

今回はコードの書きやすさを考えて狭義の方で。(広義の方だと割合かなり高くなりそうだったので)

スクリプトを書く

今回も例に漏れず node.js。
スクレイピングしてタイトル取得 → 形態素解析 → 判定 の流れでいく。

環境

Node.js 7.4.0
macOS Sierra 10.12.3

とりあえず20件分走らせてみる

yuki-albno273:ricca-blog yuki$ node exec.js
1   : len: 3    さかさ,ま,グロッケン
2   : len: 3    はらわ,た,キャビン
3   : len: 2    なけなし,スパイラル
4   : len: 4    はじ,か,み,緞子
5   : len: 2    野ざらし,ホイッパー
6   : len: 4    累,ね,て,マイカ
7   : len: 2    忖度,タクティクス
8   : len: 3    ひろう,す,ニュートラル
9   : len: 2    手押し,マーガレット
10  : len: 2    郭公,イニシアティブ
11  : len: 2    点滴,スウィープ
12  : len: 3    ボックス,砂,おろし
13  : len: 2    三椏,ドーリィ
14  : len: 2    雪庇,プラズマティックス
15  : len: 2    くぐもり,セレクション
16  : len: 3    爪先,ジャ,ガード
17  : len: 2    均衡,クレマリー
18  : len: 3    リソース,虎,落笛
19  : len: 2    鈍色,トムヤムクン
20  : len: 3    氷,瀑,ダイナソー

ちょっと険しい。
チョイスする単語が難解すぎて(特にひらがなが)分解しまくっている。「ジャガード」も切られてるし。
そもそも「虎落笛」ってなんだよ。

虎落笛とは / コトバンク

もがり‐ぶえ【虎=落笛】
冬の激しい風が竹垣や柵(さく)などに吹きつけて発する笛のような音。《季 冬》
「一汁一菜垣根が奏づ―/草田男」

知らねえよ!!

方針転換

形態素解析には kuromoji を使っていたが、mecab に切り替えてユーザ辞書をひたすら登録していくことを思いついた。
Mecabとjsバインディングのインストール / Qiita を参考に node-mecab-lite を導入。
mecabユーザ辞書を追加 / Qiita を参考にユーザ辞書を作成。
フォーマットは MeCabでオリジナル辞書を作成する / Qiita の通り。
Qiita の偉大さを思い知らされます。自分ももっと有益な記事が書きたいね……。

とりあえず20件分の単語登録

userDic
氷瀑,*,*,*,名詞,一般,*,*,*,*,氷瀑,ヒョウバク,ヒョウバク
ジャガード,*,*,*,名詞,一般,*,*,*,*,ジャガード,ジャガード,ジャガード
さかさま,*,*,*,名詞,一般,*,*,*,*,さかさま,サカサマ,サカサマ
はらわた,*,*,*,名詞,一般,*,*,*,*,はらわた,ハラワタ,ハラワタ
はじかみ,*,*,*,名詞,一般,*,*,*,*,はじかみ,ハジカミ,ハジカミ
ひろうす,*,*,*,名詞,一般,*,*,*,*,ひろうす,ヒロウス,ヒロウス
虎落笛,*,*,*,名詞,一般,*,*,*,*,虎落笛,モガリブエ,モガリブエ

まだ 20/1000 件しか見てないのに新規単語が7件もあるのを見て、これは対応しきれないぞと悟る。

再び方針転換

形態素解析の結果を見て、
要素の数が
├ 1以下 false
├ 2 ─ 1番目が漢字2文字、2番目がカタカナ true
└ 3以上
   1. 1番目と2番目がどちらも漢字1文字で3番目以降がカタカナオンリー true
   2. 1番目が漢字2文字で2番目以降がカタカナオンリー true
と判定するようにした。カタカナとそれ以外が混在してるのに形態素解析の結果が一緒になるのはほぼ考えられないので、各要素の先頭の文字を見て判断することにする。

完成

exec.js
var client = require('cheerio-httpcli'),
    morph = require('./morph.js'),
    async = require('async'),
    judge = require('./judgekhk.js');

// コンソール出力の色分け用
var red = '\u001b[31m',
    blue = '\u001b[34m',
    reset = '\u001b[0m';

var tcount = 0,
    fcount = 0;

for (var i = 1; i <= 50; i++) {
    var url = `http://ameblo.jp/ricca0227/entrylist-${i}.html`;
    client.fetch(url, function (err, $, res, body) {
        // console.log(url);
        for (var j = 0; j < 20; j++) {
            var title = $('.contentTitleArea').eq(j).text()
                .trim().replace(/NEW !/g, "");
            // console.log(j + ': ' + title);
            async.waterfall([
                function (callback) {
                    morph(title, function (err, res) {
                        if (err) callback(err, null);
                        else callback(null, res)
                    });
                },
                function (title, callback) {
                    judge(title, function (res) {
                        if (res) {
                            console.log(red + ++tcount + '\t: ' + title + reset);
                        } else {
                            console.log(blue + ++fcount + '\t: ' + title + reset);
                        }
                        callback(null);
                    });
                }
            ], function (err, res) { if (err) console.log(err) });
        }
    });
}
morph.js
Mecab = require('../meow/node-mecab-lite/lib/mecab-lite.js');

var mecab = new Mecab();

module.exports = function (title, callback) {
    var words_arr = [];

    mecab.wakatigaki(title, function (err, items) {
        if (err) return callback(err, null);
        else {
            items.forEach(function (value, index, array) {
                words_arr.push(value);
            });
            return callback(null, words_arr);
        }
    });
}
judgekhk.js
var text_1 = ['点滴', 'スウィープ'], // true
    text_2 = ['氷', '瀑', 'ダイナソー'], // true
    text_3 = ['経口', 'エビ', 'カレー'], // true
    text_4 = ['手押し', 'マーガレット'], // false
    text_5 = ['リソース', '虎落笛'], // false
    text_6 = ['突撃', '☆', 'となり', 'の', 'ネイル', '屋', 'さん', '!']; //false 

module.exports = function (text_array, callback) {
    if (text_array.length < 2)
        return callback(false);
    else {
        return callback(main(text_array));
    }
}

function main(text_array) {
    if (text_array.length == 2) {
        // Text 1
        var flg_front = isKanji(text_array[0].substr(0, 1));
        var flg_behind = isKatakana(text_array[1].substr(0, 1));

        if (flg_front && flg_behind && text_array[0].length == 2)
            return true;
        else
            return false;
    } else {
        var flg_center = isKanji(text_array[1].substr(0, 1));
        if (flg_center) {
            // Text 2
            var flg_front = isKanji(text_array[0].substr(0, 1))
                && isKanji(text_array[1].substr(0, 1));
            var flg_behind = true;
            for (var i = 2; i < text_array.length; i++) {
                flg_behind = flg_behind && isKatakana(text_array[i].substr(0, 1));
            }

            if (flg_front && flg_behind &&
                text_array[0].length + text_array[1].length == 2)
                return true;
            else
                return false;
        } else {
            // Text 3
            var flg_front = isKanji(text_array[0].substr(0, 1));
            var flg_behind = true;
            for (var i = 1; i < text_array.length; i++) {
                flg_behind = flg_behind && isKatakana(text_array[i].substr(0, 1));
            }

            if (flg_front && flg_behind && text_array[0].length == 2)
                return true;
            else
                return false;
        }
    }
}

function isKanji(c, callback) { // c:判別したい文字
    var unicode = c.charCodeAt(0);
    if ((unicode >= 0x4e00 && unicode <= 0x9fcf) || // CJK統合漢字
        (unicode >= 0x3400 && unicode <= 0x4dbf) || // CJK統合漢字拡張A
        (unicode >= 0x20000 && unicode <= 0x2a6df) || // CJK統合漢字拡張B
        (unicode >= 0xf900 && unicode <= 0xfadf) || // CJK互換漢字
        (unicode >= 0x2f800 && unicode <= 0x2fa1f))  // CJK互換漢字補助
        return true;

    return false;
}

function isKatakana(c, callback) {
    var unicode = c.charCodeAt(0);
    if (unicode >= 0x30a0 && unicode <= 0x30ff)
        return true;

    return false;
}

[javascript] javascriptで日本語文字の種類(漢字・ひらがな・カタカナ)を判別するためのメモAdd Star / はてなダイアリー を参考にさせていただいた。

結果

全件分のデータは Gist に載せておいた。

ボカロ曲っぽいタイトル: 241
っぽくないタイトル: 759

割合: 24.1%

1000件目のブログが2013年4月に書かれているので、過去3年9ヶ月に関してはおよそ4件に1件がボカロ曲フォーマットになっていることがわかる。

まとめ

思いつきで夜を越してしまって猛烈な後悔に襲われています。