ニコニコ動画

ニコニコ動画のコメントをJSONで取得する

前略、ニコニコ動画のコメントサーバーからJsonAPIを通じてコメント取得をする方法を説明していきます。

ここで紹介する内容はニコニコ動画のHTML5プレイヤーページを調べて得られる範囲の情報です。

コメント取得までの流れ

  1. ログインする(HTTPクライアントのクッキーにログインセッション情報が必要)
  2. 動画情報ページにアクセスする(ページを文字列解析して動画内部情報を得る)
  3. 動画内部情報を利用してコメントサーバーにコメント情報をリクエストする
  4. コメント情報を解析
  5. (アプリ等でコメント情報を利用する)

ニコ動サービスにログイン

https://secure.nicovideo.jp/secure/login?site=niconico

にメールアドレスとパスワードをPOSTします

C#+.Netの環境ではWindows.Web.HttpClientを使ってログインすることで、それ以降ログインセッションをHttpClientに保ってやり取りできます。

詳しくは別の方の記事を参照してください

【WindowsApp】ニコニコ動画をストリーミング再生してみる - garicchi.com

コメントサーバーへアクセスする

準備

コメントサーバーへのアクセスには以下の値が必要です。

  • thread_id
  • user_id
  • user_key
  • optional_thread_id(sub_thread_id)(チャンネル動画でのみ必要)
  • needs_key (thread_key_required)(チャンネル動画でのみ必要)

動画情報ページにアクセスしてdiv#js-initial-watch-dataの値にある文字列をJSON()として解析する、もしくは

http://flapi.nicovideo.jp/api/getflv/{動画ID}
flapiの応答例
thread_id=1501670250&l=1425&url=http%3A%2F%2Fsmile-com00.nicovideo.jp%2Fsmile%3Fm%3D31677367.87320&ms=http%3A%2F%2Fnmsg.nicovideo.jp%2Fapi%2F&ms_sub=http%3A%2F%2Fnmsg.nicovideo.jp%2Fapi%2F&user_id=53842185&is_premium=0&nickname=tor4kichi&time=1502236339700&done=true&needs_key=1&optional_thread_id=1501670249&ng_rv=5&hms=hiroba.nicovideo.jp&hmsp=2527&hmst=30&hmstk=1502236399.coAsV8y35_jTBX3TbsJZE8yjxh8&userkey=1502238139.%7E1%7EEMBek4KKC3ybpVwOHnOH7f-3ITHLP0ag1CwoDMC_Qp8

をURLパラメータとして解析します。

flapiにアクセスするためには事前に動画情報ページへアクセスする必要があるため、基本的には#js-initial-watch-dataを解析して動画再生の準備と合わせてコメントサーバー情報も取得するという流れで組むと良いかと思います。

古い動画の場合はHTML5ページが準備されていない可能性がある?ので、その場合には上記flapiを利用するもしくは、Flash版ページを解析してdiv#watchAPIDataContainerの中身をjsonとして解析することで情報が得られます。

JsonAPIでコメントを取得する

http://nmsg.nicovideo.jp/api.json/

に対して後述のJSON文字列をPOSTで送ることでコメント情報がJSONとして帰ってきます。

ユーザー動画と公式動画等でリクエストする内容を変える必要がありますが、まずはユーザー動画でのリクエストの例を見ていきましょう。

ユーザー動画でのコメント取得

以下はユーザー動画向けのコメント取得するためのJSONの例です(途中の//コメントは不要です)

ユーザー動画コメントリクエスト
[{
    "ping": {
            "content": "rs:0"
        }
    }, {
        "ping": {
            "content": "ps:0"
        }
    }, {
        "thread": {
            "thread": "1463483922",
            "version": "20090904",
            "language": 0,
            "user_id": 53842185,
            "with_global": 1,
            "scores": 1,
            "nicoru": 0,
            "userkey": "1502173042.~1~MzCxfaTZL7rDZztXT4fhmR3fXdyv-_24iGol36KOkRA"
        }
    }, {
        "ping": {
            "content": "pf:0"
        }
    }, {
        "ping": {
            "content": "ps:1"
        }
    }, {
        "thread_leaves": {
            "thread": "1463483922",
            "language": 0,
            "user_id": 53842185,
            "content": "0-22:100,1000", //0-2222は分単位の動画時間(秒は切り上げ)
            "scores": 1,
            "nicoru": 0,
            "userkey": "1502173042.~1~MzCxfaTZL7rDZztXT4fhmR3fXdyv-_24iGol36KOkRA"
        }
    }, {
        "ping": {
            "content": "pf:1"
        }
    }, {
        "ping": {
            "content": "rf:0"
        }
}]

threadとthread_leavesの値が変数として書き換えが必要で、pingはシーケンス制御?のためか固定でよさそうです。

以下の値を先ほど動画情報ページアクセスで取得した値に置き換えてPOSTします

  • thread.thread
  • thread.user_id
  • thread.userkey
  • thread_leaves.thread
  • thread_leaves.user_id
  • thread_leaves.content
  • thread_leaves.userkey

thread_leaves.contentは、動画時間に合わせて調整します。(動画時間が21分49秒の動画であれば "0-22:100,1000" 、5分05秒であれば "0-6:100,1000")

そしてレスポンスとして以下のようなJSONが返ってきます(長いのでleafとchatを一部省略)

レスポンス
[{
    "ping": {
        "content": "rs:0"
    }
}, {
    "ping": {
        "content": "ps:0"
    }
}, {
    "thread": {
        "resultcode": 0,
        "thread": "1502018914",
        "server_time": 1502183168,
        "last_res": 3118,
        "ticket": "0x3af37077",
        "revision": 1
    }
}, {
    "leaf": {
        "thread": "1502018914",
        "count": 739
    }
}, {
    "leaf": {
        "thread": "1502018914",
        "leaf": 1,
        "count": 491
    }
}, {
    "leaf": {
        "thread": "1502018914",
        "leaf": 2,
        "count": 219
    }
}, {
    "leaf": {
        "thread": "1502018914",
        "leaf": 3,
        "count": 230
    }
}, {
    "global_num_res": {
        "thread": "1502018914",
        "num_res": 3119
    }
}, {
    "ping": {
        "content": "pf:0"
    }
}, {
    "ping": {
        "content": "ps:1"
    }
}, {
    "thread": {
        "resultcode": 0,
        "thread": "1502018914",
        "server_time": 1502183168,
        "last_res": 3118,
        "ticket": "0x3af37077",
        "revision": 1
    }
}, {
    "chat": {
        "thread": "1502018914",
        "no": 3114,
        "vpos": 17782,
        "leaf": 2,
        "date": 1502182372,
        "date_usec": 812369,
        "anonymity": 1,
        "user_id": "kBsTsJso_Os-H_O6j8xgFvN6RpA",
        "mail": "184",
        "content": "ケンイチは本当にいい熱血漫画でしたね・・・(関係無"
    }
}, {
    "chat": {
        "thread": "1502018914",
        "no": 3115,
        "vpos": 804,
        "date": 1502182766,
        "date_usec": 253520,
        "anonymity": 1,
        "user_id": "JRomz5W87ktOOEi04dXZiMiFacU",
        "mail": "184",
        "content": "遅かったじゃないか……"
    }
}, {
    "chat": {
        "thread": "1502018914",
        "no": 3116,
        "vpos": 86247,
        "leaf": 14,
        "date": 1502182931,
        "date_usec": 46671,
        "anonymity": 1,
        "deleted": 2
    }
}, {
    "chat": {
        "thread": "1502018914",
        "no": 3117,
        "vpos": 4928,
        "date": 1502182981,
        "date_usec": 164100,
        "anonymity": 1,
        "user_id": "GPNKoD4bG83hcwyZxBROyY3QAeI",
        "mail": "184",
        "content": "この人にとっての普通は俺達の常識ではない"
    }
}, {
    "chat": {
        "thread": "1502018914",
        "no": 3118,
        "vpos": 6632,
        "leaf": 1,
        "date": 1502183038,
        "date_usec": 311032,
        "anonymity": 1,
        "user_id": "GPNKoD4bG83hcwyZxBROyY3QAeI",
        "mail": "184 shita red big",
        "content": "尼デウス"
    }
}, {
    "ping": {
        "content": "pf:1"
    }
}, {
    "ping": {
        "content": "rf:0"
    }
}]

レスポンスのthread

"resultcode": 0,        // 0 で成功、それ以外は失敗
"thread": "1502018914",    // スレッドID
"server_time": 1502183168,  // 取得したサーバー時間(UNIX時間)
"last_res": 3118,       // コメント数
"ticket": "0x3af37077",    // コメント投稿するためのチケット
"revision": 1         // ?

レスポンスのchat

"thread": "1502018914", // スレッドID
"no": 168,        // コメント番号
"vpos": 58161,      // コメントの動画時間上の位置 1vpos=10ミリ秒 100vposで1秒
"leaf": 9,         // ?
"date": 1502019822,   // 投稿時間のUNIX時間
"date_usec": 257855,   // 投稿時間の1秒以下の時間 例ではdate+0.257885秒に投稿された
"premium": 1, // コメント投稿ユーザーがプレミアム会員であれば 1
"anonymity": 1,     // 匿名コメント
"user_id": "NOYnNqmzAwmdb6duA4IO0ogncNM", // ユーザーID(匿名の場合は1週間でリセットされる?)
"mail": "184",      // コメントのコマンド
"content": "草生える"   // コメント本文
"deleted": 2       // 1以上で 削除済み、数字は削除理由によって異なる詳細不明

投稿者コメントを含める

投コメの参考動画 http://www.nicovideo.jp/watch/sm32843532

hasOwnerThreadが"1"になっている動画では、先述のJSONの代わりに以下のようなJSONを送ることで投稿者コメントを含んだコメント一覧を取得できます。

[{
    "ping": {
        "content": "rs:0"
    }
}, {
    "ping": {
        "content": "ps:0"
    }
}, {
    "thread": {
        "thread": "1520342061",
        "version": "20090904",
        "language": 0,
        "user_id": "53842185",
        "with_global": 1,
        "scores": 1,
        "nicoru": 0,
        "userkey": "1522458544.~1~14ReXopXIqcFoSdvKoVZ7vng2xYIoMWxQTv1lD0PoZM"
    }
}, {
    "ping": {
        "content": "pf:0"
    }
}, {
    "ping": {
        "content": "ps:1"
    }
}, {
    "thread_leaves": {
        "thread": "1520342061",
        "language": 0,
        "user_id": "53842185",
        "content": "0-15:100,1000",
        "scores": 1,
        "nicoru": 0,
        "userkey": "1522458544.~1~14ReXopXIqcFoSdvKoVZ7vng2xYIoMWxQTv1lD0PoZM"
    }
}, {
    "ping": {
        "content": "pf:1"
    }
}, {
    "ping": {
        "content": "ps:2"
    }
}, {
    "thread": {
        "thread": "1520342061",
        "version": "20061206",
        "fork": 1,
        "user_id": "53842185",
        "res_from": -1000,
        "with_global": 1,
        "scores": 1,
        "nicoru": 0,
        "userkey": "1522458544.~1~14ReXopXIqcFoSdvKoVZ7vng2xYIoMWxQTv1lD0PoZM"
    }
}, {
    "ping": {
        "content": "pf:2"
    }
}, {
    "ping": {
        "content": "rf:0"
    }
}]

先のJSONとの違いは2回目の thread を指定している点です。また2回目の thread では「fork: 1」「res_from: -1000」が追加されている点に注意します。さらに「version: "20061206"」は1回目の thread では「version : "20090904"」と異なっています。

このJSONをコメントサーバーに送信すると「スレッド情報」→「ユーザーコメント郡」→「投稿者コメント郡」というシーケンスのデータを受け取る事ができます。(送信したJSONに対応する形でデータが返送されているようですね)

次の画像は「ユーザーコメント郡」→「投稿者コメント郡」の切り替わる部分

image.png

投稿者コメントとして判定する場合には、前述のシーケンスで判定するか、

if (chat.user_id == null && chat.deleted == null)
{
    // 投コメとして処理
}

と user_id と deleted が null であることを確認する方法があります。(ユーザーコメントでも削除済みの場合は user_id が null になるためこの2条件で判定する必要があります)

(2018/03/31追記)

チャンネル動画でのコメント取得

公式アニメ等のチャンネル動画では needs_key (thread_key_required)のフラグに応じて ThreadKey と force184 の指定を取得する必要があります。

ThreadKeyとforce184の取得方法

それらを取得した上で以下のリクエスト用JSONを作成してPOSTします。

チャンネル動画等でのリクエスト
[{
    "ping": {
        "content": "rs:0"
    }
    }, {
        "ping": {
            "content": "ps:0"
        }
    }, {
        "thread": {
            "thread": "1501742473",
            "version": "20090904",
            "language": 0,
            "user_id": 53842185,
            "with_global": 1,
            "scores": 1,
            "nicoru": 0,
            "userkey": "1502173804.~1~fwFqcTlwtEbO4ggddXkZLdbowXV9TrcE_NTbhDTmFlo"
        }
    }, {
        "ping": {
            "content": "pf:0"
        }
    }, {
        "ping": {
            "content": "ps:1"
        }
    }, {
        "thread_leaves": {
            "thread": "1501742473",
            "language": 0,
            "user_id": 53842185,
            "content": "0-13:100,1000",
            "scores": 1,
            "nicoru": 0,
            "userkey": "1502173804.~1~fwFqcTlwtEbO4ggddXkZLdbowXV9TrcE_NTbhDTmFlo"
        }
    }, {
        "ping": {
            "content": "pf:1"
        }
    }, {
        "ping": {
            "content": "ps:2"
        }
    }, {
        "thread": {
            "thread": "1501742474",
            "version": "20090904",
            "language": 0,
            "user_id": 53842185,
            "force_184": "1",
            "with_global": 1,
            "scores": 1,
            "nicoru": 0,
            "threadkey": "1502173806.e_qPpM9yX3kUgW80nVYo32EdDCU"
        }
    }, {
        "ping": {
            "content": "pf:2"
        }
    }, {
        "ping": {
            "content": "ps:3"
        }
    },
    {
        "thread_leaves": {
            "thread": "1501742474",
            "language": 0,
            "user_id": 53842185,
            "content": "0-13:100,1000",
            "scores": 1,
            "nicoru": 0,
            "force_184": "1",
            "threadkey": "1502173806.e_qPpM9yX3kUgW80nVYo32EdDCU"
        }
    }, {
        "ping": {
            "content": "pf:3"
        }
    }, {
        "ping": {
            "content": "rf:0"
        }
    }
]

thread+thread_leaves を2回繰り返しています。最初の1セットはユーザー動画と同じくuser_keyを指定したもの、2セット目はforce_184とthreadkeyを指定していてuserkeyは指定していない点に注意します。

スレッドIDの設定順が、1セット目のthreadにはoptional_thread_idまたはsub_thread_idを、2セット目のthreadにはthread_idを設定します。(理由は不明ですが公式動画でそうしているので、という程度の理解です)

force184だけフラグ値が文字列になっている点に注意します。

チャンネル動画でのコメントレスポンスはユーザー動画とほぼ同じのようです。(詳しく見てないですが同一の解析機で対処可能でした)

参考

新コメサーバーのJsonApiリクエストとレスポンスのまとめ
https://gist.github.com/tor4kichi/c7658bd2fbb3ef15cb28b41b85c06315

ニコニコ動画・ニコニコ生放送のコメント取得 備忘録 - まぢぽん製作所
http://blog.livedoor.jp/mgpn/archives/51886270.html