Perl
Twitter
スクレイピング
bot
クローラー

ラジオの「(非公式な)オンエア曲 BOT」を気が向いて作った話

More than 1 year has passed since last update.

みなさん、ラジオ聴いてますか? 自分は普段 radiko で聴いてますが。
気になったオンエア曲が Twitter のタイムラインに流れてきてついついいいねしちゃいますよね。自分もついついいいねしちゃいます。
そんな BOT を作ったのである方から「どうやって作ってるんですか?」と聞かれたりしました。
今回はそんな裏側をソースと共に公開したいと思います。もしも自分に何かあった時、誰かがソースをフォークして使えば安心だね!

作ろうとしたきっかけ

作った当時

  • 名古屋の ZIP-FM を聴いていてオンエア曲が気になった
  • Twitter の BOT もある
  • でも息してない、自分としては動画サイトなどのリンクまではいらない
  • でもその要件を満たしてる BOT がない
  • じゃあ作ろう

つい最近

  • そういえば ZIP-FM の BOT は(自分の作ったやつも)あるけど他局(@FMRadio NEO など)は見かけ無いよね
  • じゃあ作ろう

仕組み

  • オンエア曲のページなどからオンエア曲情報を取得する
  • オンエア曲情報を解析してオンエア時間、アーティスト名、曲名を取得する
  • 取得したオンエア時間、アーティスト名、曲名をツイートする
  • これを毎分間隔で動くようにスケジューリングしておく

オンエア曲情報の取得

さて肝心要のオンエア曲情報どこから取得するかです。各局の公式サイトや radiko さん、JFN な放送局では keitai.fm で見ることがありますね。タネ明かしすると自分の場合はデータの扱いやすさから radiko さんや keitai.fm から取得しています。
ちょっとブラウザの開発者コンソール機能を使って @FM の場合 radiko、keitai.fm でどういうふうになっているか見てみましょう。

radiko.jp_console keitai.fm_console
なにやらそれぞれ定期的に XML ファイルを取得していますね。

radiko.jp

いろんな XML ファイルがあるのでちょっと「noa」というキーワードで絞り込んでみます。
radiko.jp_console

どうやら FMAICHI.xml にオンエア曲情報があるみたいです。
radiko.jp_xml

radiko は局 ID をキーにして選局しているのでこの ID を変えれば各 FM 局のオンエア曲情報が取得できそうです。また XML ファイルを取得するとき更新のチェックも自力でする必要がないので楽できそうですね。

post
$file_name = "$station_id.xml";

$ua = LWP::UserAgent->new();
$ua->agent("Mozilla/5.0");
$res = $ua->mirror("http://radiko.jp/v3/feed/pc/noa/$file_name", "$cache_dir/$file_name");
if (!$res->is_success) {
    print "no update\n";
    exit 1;
}

keitai.fm

どうやら noamusic.xml にオンエア曲情報があるみたいです。
keitai.fm_xml

keitai.fm は本局のコールサインをキーにして選択しているのでこのコールサインを変えれば各 FM 局のオンエア曲情報が取得できそうです。でも XML ファイルを取得するとき更新のチェックも自力でする必要があります。

post
$callsign = $station_id;
$file_name = "$station_id.xml";
$callsign =~ s/JO(\S{2})-FM/\1/g;
$callsign = lc($callsign);

$ua = LWP::UserAgent->new();
$ua->agent("Mozilla/5.0");
$res = $ua->mirror("http://www.keitai.fm/search/view/$callsign/xml/noamusic.xml", "$cache_dir/$file_name");
if (!$res->is_success) {
    print "no update\n";
    exit 1;
}

オンエア曲情報を解析

各サイトから取得した XML データをパースして Twitter に投稿するフォーマットに変換しています。それぞれ下2行は個人的に半角英数字に半角スペース、全角カナの方が精神衛生上落ち着くので揃えるようにしています。

radiko.jp

post
$data = XML::Simple->new(keyattr => [])->XMLin("$cache_dir/$file_name");
$noa = $data->{"noa"}->{"item"}[0];

# TIMESTAMP
$time = Time::Piece->strptime($noa->{"stamp"}, '%Y-%m-%d %H:%M:%S');
$noatime = sprintf("%04d/%02d/%02d %02d:%02d", $time->year, $time->mon, $time->mday, $time->hour, $time->minute);

$make_value = sprintf("%s - %s", $noa->{"artist"}, $noa->{"title"});
$str = encode('utf-8', HTML::Entities::decode($make_value));
$str = Unicode::Japanese->new($str)->z2h->get;
$str = Unicode::Japanese->new($str)->h2zKana->get;

radiko で取得したオンエア曲情報は過去のオンエア曲も数曲分入っているので一番上のデータだけ取り出しています。

keitai.fm

post
$noa = XML::Simple->new(keyattr => [])->XMLin("$cache_dir/$file_name");

if (ref($noa->{"artist_name"}) eq "HASH") {
    print "artist_name is hash\n";
    exit 1;
}

# TIMESTAMP
$time = Time::Piece->strptime($noa->{"onair_time"}, '%Y-%m-%dT%H:%M:%S+09:00');
$noatime = sprintf("%04d/%02d/%02d %02d:%02d", $time->year, $time->mon, $time->mday, $time->hour, $time->minute);

$make_value = sprintf("%s - %s", $noa->{"artist_name"}, $noa->{"music_name"});
$str = HTML::Entities::decode($make_value);
$str = Unicode::Japanese->new($str)->z2h->get;
$str = Unicode::Japanese->new($str)->h2zKana->get;

keitai.fm で取得したオンエア曲情報は取得時の分のみなのですが、たまに変にパースするところがあるのでそこを検知したら何もせずプログラムが終わるようにしています。

ツイートする

post
# コンシューマ セット
my %INIT_PARAMS = (
    consumer_key    => $param->{"CONSUMER_PARAM.CONSUMER_KEY"},
    consumer_secret => $param->{"CONSUMER_PARAM.CONSUMER_SECRET"},
    ssl             => 1,
);

my $t = Net::Twitter::Lite::WithAPIv1_1->new(%INIT_PARAMS);

# トークンをセットする
$t->access_token($param->{"ACCESS_TOKEN.ACCESS_TOKEN"});
$t->access_token_secret($param->{"ACCESS_TOKEN.TOKEN_SECRET"});

binmode(STDOUT, ":utf8");

# 投稿
# delete strings
$str = decode('utf-8', $str);
my $tweet = sprintf("[%s] %s", $noatime, $str);
if(length($tweet) > $max_length) {
    $tweet = substr($tweet, 0, $max_length - 3) . "...";
}

# post to Twitter
my ($status, $statuses, $tmp_status, $ret);
eval {
    # check duplicate tweet
    $statuses = $t->user_timeline({ count => 1 });
    for my $user_tl (@$statuses) {
        $tmp_status = $user_tl->{text};
    }
    $tmp_status =~ s|^\[(\d+)/(\d+)/(\d+) (\d+):(\d+)\] ||;
    print "duplicate?[".$str."][$tmp_status]\n";
    if($str eq $tmp_status){ print "duplicate![".$str."][$tmp_status]\n"; exit -1; }

    $status = $t->update({ status => $tweet });
};
if($@) {
    print STDERR "Tweet WARNING: $@\n";
    $ret = 1;

} else {
    print "Tweet posted.[$tweet]\n";
    $ret = 0;
}
exit $ret;

CONSUMER_KEY や ACCESS_TOKEN は基本別ファイルに保存しています。ツイートする際の文字数制限や重複チェックはここで行っています。

ここまで見たのでソースを出します

GitHub には上に書いてある内容以上にやっていることがあるので詳しくは各リポジトリを見てください(ぉ

その他注意点

  • 共通して言えること
    • BOT を動かす場合、各サイトに迷惑をかけない程度に。
  • GitHub でフォークせずに使う場合
    • UNIX 系 OS(BSD / Linux)で動く環境で作っています。もしかしたら Windows でも Perl やその他モジュールを導入すれば動くかもしれません。(未確認事項)
    • (BOT なので)24時間365日動かせられる PC で使ってください。
    • (もちろんですが)Web 上で動かす想定で作っていません。
  • GitHub でフォークして使う場合
    • セキュリティはしっかり配慮してください。(TOKEN が漏れる、最悪乗っ取られるなど)
    • ↑さえ守っていただければ煮るなり焼くなり美味しくいただいてください。

おまけ

  • Twilog や twisave などの各サービスを登録しておけばアーカイブが見れたり、最近どんな曲が多くかかってるか見れたりします。
  • たまに BOT アカウントのフォロワーを見ると公式のアカウントがフォローしていて中の人は大変驚きながら恐縮しています。 他局さんでも公式の NOA BOT があったりするのでぜひ公式でも NOA BOT を作ってください。なんでもs(ry