yutaponのブログ

javascript界隈の興味あるネタを備忘録的に残しておく場所

ニコ動のランキング情報をJSONで取得してみる

d3.jsで可視化したら面白そうなデータ無いかなーと思い、
ニコ動なら毎日データが変わって面白そうなのでAPIを調べてみました。

だいぶ汚いけどデータを持ってくるところまでは出来たので、
この次くらいに可視化します。
ということで今回はバックエンドでデータを集めるところまで。


ニコ動APIの使い方

公式ドキュメントに当たるものはニコニコ大百科だけ?なんですかね。
ニコニコ動画APIとは (ニコニコドウガエーピーアイとは) [単語記事] - ニコニコ大百科

ランキング情報の取り方

カテゴリ合算毎時総合ランキングはこれで取れます。

http://www.nicovideo.jp/ranking/fav/hourly/all?rss=2.0

URLの末尾にrssと付いていることからわかりますが、
単にニコ動のランキングページをXMLで取得しています。

幾つかパラメータを指定できて、例えば、ゲームカテゴリの月間閲覧ランキングを取得する場合はこちら。

http://www.nicovideo.jp/ranking/view/monthly/game?rss=2.0


つまり指定方法はこんな感じになります。

http://www.nicovideo.jp/ranking/対象/期間/カテゴリ?rss=2.0


迷ったらニコ動のランキングページを開いて、表示するランキングを変更し、
URLの末尾にrss=2.0つけるのが簡単。

取得できるXMLはこんな感じ。(カテゴリ合算毎時ランキング)

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    
      <title>カテゴリ合算の総合ランキング(毎時)‐ニコニコ動画</title>
      <link>http://www.nicovideo.jp/ranking/fav/hourly/all</link>
      <description>毎時更新</description>
      <pubDate>Sun, 23 Mar 2014 16:06:55 +0900</pubDate>
      <lastBuildDate>Sun, 23 Mar 2014 16:06:55 +0900</lastBuildDate>
      <generator>ニコニコ動画</generator>
      <language>ja-jp</language>
      <copyright>(c) niwango, inc. All rights reserved.</copyright>
      <docs>http://blogs.law.harvard.edu/tech/rss</docs>
    
  <atom:link rel="self" type="application/rss+xml" href="http://www.nicovideo.jp/ranking/fav/hourly/all?rss=2.0"/>

    
                <item>
        <title>第1位:のうりん 第11限「あかるいのうそん」</title>
        <link>http://www.nicovideo.jp/watch/so23136890</link>
        <guid isPermaLink="false">tag:nicovideo.jp,2014-03-23:/watch/so23136890</guid>
        <pubDate>Sun, 23 Mar 2014 16:06:55 +0900</pubDate>
        <description><![CDATA[
                      <p class="nico-thumbnail"><img alt="のうりん 第11限「あかるいのうそん」" src="http://tn-skr3.smilevideo.jp/smile?i=23136890" width="94" height="70" border="0"/></p>
                                <p class="nico-description">田舎生活の知識がない林檎も一緒に、愛生村へ帰省する事になった耕作と農。そんな耕作たちを迎えたのは農の姉妹・士(つかさ)、工(たくみ)、商(あきな)だった。農の実家である中沢家に到着すると何故か農との結納の話が進んでいた!原作ノベル・コミック版が1話無料で読める動画一覧はこちら第10限 watch/1394696383</p>
                                <p class="nico-info"><small><strong class="nico-info-number">11,803</strong>pts.|<strong class="nico-info-length">23:40</strong>|<strong class="nico-info-date">2014年03月23日 12:00:00</strong> 投稿<br/><strong>合計</strong>&nbsp;&#x20;再生:<strong class="nico-info-total-view">31,118</strong>&nbsp;&#x20;コメント:<strong class="nico-info-total-res">6,822</strong>&nbsp;&#x20;マイリスト:<strong class="nico-info-total-mylist">634</strong><br/><strong>毎時</strong>&nbsp;&#x20;再生:<strong class="nico-info-hourly-view">8,650</strong>&nbsp;&#x20;コメント:<strong class="nico-info-hourly-res">1,621</strong>&nbsp;&#x20;マイリスト:<strong class="nico-info-hourly-mylist">119</strong><br/></small></p>
                  ]]></description>
      </item>

(以下略。100位までitemが取れる)


1位から100位までのタイトルとか、動画のID、投稿時間、
descriptionをパースすればマイリスとか取得できますが、
タグなどは取れないようです。
そこで各動画の詳細情報を取得するAPIがあるのでそちらも併用することにします。

動画の詳細情報を取得する

動画の詳細はこちらのAPIで取得できるようです。

http://ext.nicovideo.jp/api/getthumbinfo/動画ID

動画IDというのはsm12345とか、よくURLの末尾についてるあれです。
(実装していてso12345みたいな動画IDがあるのを知りました)

例えばsm9のレッツゴー!陰陽師の詳細を取得してみたら以下のXMLが返ってきました。

<?xml version="1.0" encoding="UTF-8"?>
<nicovideo_thumb_response status="ok">
  <thumb>
    <video_id>sm9</video_id>
    <title>新・豪血寺一族 -煩悩解放 - レッツゴー!陰陽師</title>
    <description>レッツゴー!陰陽師(フルコーラスバージョン)</description>
    <thumbnail_url>http://tn-skr2.smilevideo.jp/smile?i=9</thumbnail_url>
    <first_retrieve>2007-03-06T00:33:00+09:00</first_retrieve>
    <length>5:19</length>
    <movie_type>flv</movie_type>
    <size_high>21138631</size_high>
    <size_low>17436492</size_low>
    <view_counter>13948881</view_counter>
    <comment_num>4195844</comment_num>
    <mylist_counter>149333</mylist_counter>
    <last_res_body>おい字幕wwww腹筋がww 声が変wwww 弾幕注意! GGGGGGGGGGGGGGGGGGGG </last_res_body>
    <watch_url>http://www.nicovideo.jp/watch/sm9</watch_url>
    <thumb_type>video</thumb_type>
    <embeddable>1</embeddable>
    <no_live_play>0</no_live_play>
    <tags domain="jp">
      <tag lock="1">陰陽師</tag>
      <tag lock="1">レッツゴー!陰陽師</tag>
      <tag lock="1">公式</tag>
      <tag lock="1">音楽</tag>
      <tag lock="1">ゲーム</tag>
      <tag>矢部野彦麿</tag>
      <tag>sm9</tag>
      <tag>最古の動画</tag>
      <tag>β時代の英雄</tag>
      <tag>空耳</tag>
    </tags>
    <user_id>4</user_id>
    <user_nickname>運営長の中の人</user_nickname>
    <user_icon_url>http://usericon.nimg.jp/usericon/s/0/4.jpg?1395494265</user_icon_url>
  </thumb>
</nicovideo_thumb_response>

それぞれの要素が何を意味しているかはタグ名から大体わかりますが、大百科を見れば載ってます。


ランキング情報の整形

今回はランキングで100個の動画IDを取得して、その動画IDをもとに詳細情報を取得するAPIを叩くって方法を取ります。
実行環境はnode.jsなので、XMLの取り扱いが面倒です。
そこでxmljsonというモジュールを使ってXMLJSONに変換します。
xmljson

作業ディレクトリに移動したら、expressプロジェクト作ってインストールします。

$ cd path/to/workspace
$ express -c stylus project_name
$ cd project_name && npm install
$ npm install --save xmljson


xmljsonの使い方はこんな感じ。

var xmljson = require('xmljson');
var xml;

// 変数xmlにXMLが入ってるとして 

xmljson.to_json(xml, function(err, json) {
    if (err) {
        // エラーハンドリング
        return err;
    }
    console.log(json);  // jsonに変換されてる
});


これで実装しやすくなったので、ランキング取得→各動画の詳細取得っていうプログラムを作ります。


ランキング情報を取得する

まずはじめに便利モジュールをインストールしておきます。

$ npm install --save async underscore http xmljson


ざーっと書いてみたのがこちら。

// @file nico.js

var _       = require('underscore');
var async   = require('async');
var http    = require('http');
var xmljson = require('xmljson');

/**
 * @class
 */
function Nico() {}

Nico.prototype = {
    /**
     * ニコニコ動画のランキング情報を取得する
     * @public
     * @param  {Object} options
     * @return {Objext}
     */
    getRanking : function getRanking(options, callback) {
        var self = this;
        var type     = options.type     || 'fav',
            category = options.category || 'all',
            period   = options.period   || 'hourly';

        async.waterfall([
            function getNicoRanking(next) {
                self._getRankingAll({
                    type     : type,
                    period   : period,
                    category : category
                }, function (err, res) {
                    next(err, res);
                });
            },
            function getNicoRankingDetail(data, next) {
                self._getRankingDetail({ data: data }, function (err, res) {
                    next(err, res);
                });
            }
        ], function (err, res) {
            if (err) {
                throw err;
            }
            callback(err, res);
        });
    },

    /**
     * 大まかなランキング情報を取得する
     * @private
     * @param  {Object}   options
     * @param  {Function} callback
     */
    _getRankingAll : function _getRankingAll(options, callback) {
        async.waterfall([
            function getXML(next) {
                var type     = options.type,
                    period   = options.period,
                    category = options.category;
                var params = {
                    host : 'www.nicovideo.jp',
                    port : 80,
                    path : '/ranking/' + type + '/' + period + '/' + category + '?rss=2.0'
                };

                http.get(params, function (response) {
                    var xml = '';
                    response.setEncoding('utf8');
                    response
                        .on('data', function (data) {
                            xml += data;
                        })
                        .on('end', function () {
                            next(null, xml);
                        });
                }).on('error', function (e) {
                    next(e);
                });
            },
            function parseJSON(xml, next) {
                xmljson.to_json(xml, function (err, json) {
                    next(err, json);
                });
            }
        ], function (err, res) {
            callback(err, res);
        });
    },

    /**
     * 動画IDをもとに動画の詳細を取得する
     * @private
     * @param  {Object}   options
     * @param  {Function} callback
     */
    _getRankingDetail : function _getRankingDetail(options, callback) {
        var items = options.data.rss.channel.item;
        var i,
            len;
        var regexp = /(sm|so)[0-9]+$/; // 正規表現をキャッシュ
        var link;
        var ids = [];
        var tasks;

        // 動画のIDを抽出する
        for (i = 0, len = 100; i < len; i++) {
            link = items[i].link;
            ids.push(link.match(regexp)[0]);
        }

        // task生成
        tasks = this._createDetailTask(ids);

        // タスク実行
        async.parallel(tasks, function (err, res) {
            callback(null, res);
        });
    },

    /**
     * 動画の詳細を取得してJSONにパースするタスクを作成する
     * @private
     * @param  {Array} ids 動画IDのリスト
     * @return {Array}     XML取得からJSONへパースする一連のタスクのリスト
     */
    _createDetailTask : function _createDetailTask(ids) {
        var tasks   = [];

        _.each(ids, function (id, i) {

            // TODO: ネストを浅くする
            tasks.push(function (next) {
                async.waterfall([
                    function getXML(_next) {
                        var options = {
                            host : 'ext.nicovideo.jp',
                            port : 80,
                            path : '/api/getthumbinfo/' + id
                        };
                        http.get(options, function (response) {
                            var xml = '';
                            response.setEncoding('utf8');
                            response
                                .on('data', function (data) {
                                    xml += data;
                                })
                                .on('end', function () {
                                    _next(null, xml);
                                });
                        }).on('error', function (e) {
                            _next(e);
                        });
                    },
                    function parseJSON(xml, _next) {
                        xmljson.to_json(xml, function (err, json) {
                            _next(err, json);
                        });
                    },
                    function buidObject(json, _next) {
                        var detail = json.nicovideo_thumb_response.thumb;
                        var tags   = detail.tags.tag;
                        var result;

                        result = _.extend(detail, {
                            rank : i + 1,
                            tags : _.values(tags)   // オブジェクトの値のみからなる配列を生成
                        });

                        _next(null, result);
                    },
                ], function (err, res) {
                    next(err, res);
                });
            });
        });

        return tasks;
    }
};

module.exports = new Nico();


このモジュールの呼び出し方はこれ。

var nico = require('path/to/nico');

//オプションが空オブジェクトだとカテゴリ合算毎時ランキングを取得になる
nico.getRanking({}, function (err, json) {
    console.log(json);
});


このときのjsonの内容はこちら。

[ { video_id: 'so23136890',
    title: 'のうりん 第11限「あかるいのうそん」',
    description: '田舎生活の知識がない林檎も一緒に、愛生村へ帰省する事になった耕作と農。そんな耕作たちを迎えたのは農の姉妹・士(つかさ)、工(たくみ)、商(あきな)だった。農の実家である中沢家に到着すると何故か農との結納の話が進んでいた!原作ノベル・コミック版が1話無料で読める動画一覧はこちら第10限 watch/1394696383',
    thumbnail_url: 'http://tn-skr3.smilevideo.jp/smile?i=23136890',
    first_retrieve: '2014-03-23T12:00:00+09:00',
    length: '23:40',
    movie_type: 'mp4',
    size_high: '147838127',
    size_low: '49080380',
    view_counter: '74859',
    comment_num: '0',
    mylist_counter: '1158',
    last_res_body: '',
    watch_url: 'http://www.nicovideo.jp/watch/so23136890',
    thumb_type: 'video',
    embeddable: '1',
    no_live_play: '0',
    tags: [
      { _: 'アニメ', '$': { category: '1', lock: '1' } },
      { _: 'のうりん', '$': { lock: '1' } },
      { _: 'GA文庫', '$': { lock: '1' } },
      { _: '浅沼晋太郎', '$': { lock: '1' } },
      { _: '田村ゆかり', '$': { lock: '1' } },
      { _: '花澤香菜', '$': { lock: '1' } },
      'ゆかたん王国',
      '士農工商',
      '孤独のグルメ',
      '長良川鉄道'
    ],
    ch_id: '2577243',
    ch_name: 'のうりん',
    ch_icon_url: 'http://icon.nimg.jp/channel/s/ch2577243.jpg?1395318214',
    rank: 1 },
  
  (2件目以降は省略)


これでデータの抽出はできました。


おわりに

タグのリストに入ってるものがオブジェクト型と文字列型が混在してて気持ち悪いですね。
こういう部分をもっときれいに整形していかないといけないです。

あとはDBに保存して、データを可視化するまでを目標としています。
DBはRedis、可視化はd3.jsの予定。