ニコニコ生放送のコメントを取得して色々するための第一歩（前編：JavaScript版）

# はじめに
Pythonでデータ分析ごっこはやってきたけど、通信については全くわからん。。。
けど、ニコニコ生放送の情報を使って色々やるwebアプリが作りたい。
そう思って色々調べて、何とかコメントをリアルタイムで取ってくるところまでは出来ました。
コードの実行は自己責任でお願いします。
[Pythonで取ってくる続きの記事を書きました。](https://qiita.com/fingerprint216/items/e4b24484c5f7eae0a3d1)

# 現時点でできたソースコード
[このページ](https://qiita.com/tor4kichi/items/4df5b11ec564bb8f8d16)と[このページ](https://qiita.com/pasta04/items/33da06cf3c21e34fc4d1)を大いに参考にさせていただきました。
適当な放送中のニコ生のページをChromeで開き、以下のJavaScriptコードをChromeのデベロッパーツール（WindowsだとF12押すと出るやつ）のコンソールにコピペして実行すると、コメントが流れてきます。
このソースコードはログインしてから実行してください。
[ログインしない場合](https://qiita.com/fingerprint216/items/7938835a8ff69d73a465#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%97%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88)

```niconama_comment_chrome.js
// アクセスした生放送のページのhtmlから情報を取ってくる
const embeddedData = JSON.parse(document.getElementById("embedded-data").getAttribute("data-props"));
const url_system = embeddedData.site.relive.webSocketUrl;
const user_id = embeddedData.user.id

// websocketでセッションに送るメッセージ
const message_system_1 = '{"type":"startWatching","data":{"stream":{"quality":"abr","protocol":"hls","latency":"low","chasePlay":false},"room":{"protocol":"webSocket","commentable":true},"reconnect":false}}';
const message_system_2 ='{"type":"getAkashic","data":{"chasePlay":false}}'

// コメントセッションへWebSocket接続するときに必要な情報
let uri_comment
let threadID
let threadkey
let mes_comment

/*視聴セッションのWebSocket関係の関数*/ 
// 視聴セッションとのWebSocket接続関数の定義
function connect_WebSocket_system()
{
  websocket_system = new WebSocket(url_system);
  websocket_system.onopen = function(evt) { onOpen_system(evt) };
  websocket_system.onclose = function(evt) { onClose_system(evt) };
  websocket_system.onmessage = function(evt) { onMessage_system(evt) };
  websocket_system.onerror = function(evt) { onError_system(evt) };
}

// 視聴セッションとのWebSocket接続が開始された時に実行される
function onOpen_system(evt)
{
  console.log("CONNECTED TO THE SYSTEM SERVER");
  doSend_system(message_system_1);
  doSend_system(message_system_2);
}

// 視聴セッションとのWebSocket接続が切断された時に実行される
function onClose_system(evt)
{
  console.log("DISCONNECTED FROM THE SYSTEM SERVER");
  websocket_comment.close(); // コメントセッションとのWebSocket接続を切る
}

// 視聴セッションとのWebSocket接続中にメッセージを受け取った時に実行される
function onMessage_system(evt)
{
  console.log('RESPONSE FROM THE SYSTEM SERVER: ' + evt.data);
  is_room = evt.data.indexOf("room")
  is_ping = evt.data.indexOf("ping")

  // コメントセッションへ接続するために必要な情報が送られてきたら抽出してWebSocket接続を開始
  if(is_room>0){

      // 必要な情報を送られてきたメッセージから抽出
      evt_data_json = JSON.parse(evt.data);
      uri_comment = evt_data_json.data.messageServer.uri
      threadID = evt_data_json.data.threadId
      threadkey = evt_data_json.data.yourPostKey
      message_comment = '[{"ping":{"content":"rs:0"}},{"ping":{"content":"ps:0"}},{"thread":{"thread":"'+threadID+'","version":"20061206","user_id":"'+user_id+'","res_from":-150,"with_global":1,"scores":1,"nicoru":0,"threadkey":"'+threadkey+'"}},{"ping":{"content":"pf:0"}},{"ping":{"content":"rf:0"}}]'
      // コメントセッションとのWebSocket接続を開始
      connect_WebSocket_comment();
  }

  // pingが送られてきたらpongとkeepseatを送り、視聴権を獲得し続ける
  if(is_ping>0){
    doSend_system('{"type":"pong"}');
    doSend_system('{"type":"keepSeat"}');
  }
}

// 視聴セッションとのWebSocket接続中にエラーメッセージを受け取った時に実行される
function onError_system(evt)
{
  console.log('ERROR FROM THE SYSTEM SERVER: ' + evt.data);
}

// 視聴セッションへメッセージを送るための関数
function doSend_system(message)
{
  console.log("SENT TO THE SYSTEM SERVER: " + message);
  websocket_system.send(message);
}

/*コメントセッションのWebSocket関係の関数*/ 
// コメントセッションとのWebSocket接続関数の定義
function connect_WebSocket_comment()
{
  websocket_comment = new WebSocket(uri_comment, 'niconama', {
    headers: {
      'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
      'Sec-WebSocket-Protocol': 'msg.nicovideo.jp#json',
    },
  });
  websocket_comment.onopen = function(evt) { onOpen_comment(evt) };
  websocket_comment.onclose = function(evt) { onClose_comment(evt) };
  websocket_comment.onmessage = function(evt) { onMessage_comment(evt) };
  websocket_comment.onerror = function(evt) { onError_comment(evt) };
}

// コメントセッションとのWebSocket接続が開始された時に実行される
function onOpen_comment(evt)
{
  console.log("CONNECTED TO THE COMMENT SERVER");
  doSend_comment(message_comment);
}

// コメントセッションとのWebSocket接続が切断された時に実行される
function onClose_comment(evt)
{
  console.log("DISCONNECTED FROM THE COMMENT SERVER");
}

// コメントセッションとのWebSocket接続中にメッセージを受け取った時に実行される
function onMessage_comment(evt)
{
  console.log('RESPONSE FROM THE COMMENT SERVER: ' + evt.data);
}

// コメントセッションとのWebSocket接続中にエラーメッセージを受け取った時に実行される
function onError_comment(evt)
{
  console.log('ERROR FROM THE COMMENT SERVER: ' + evt.data);
}

// コメントセッションへメッセージを送るための関数
function doSend_comment(message)
{
  console.log("SENT TO THE COMMENT SERVER: " + message);
  websocket_comment.send(message);
}

// 視聴セッションとのWebSocket接続開始
connect_WebSocket_system();
```
# 実行例
随時コメントを取得してConsoleに表示していきます。
[使わせていただいた配信ページ。](https://live2.nicovideo.jp/watch/lv330265691)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451017/a26f1598-2d9d-5f23-d5ad-9fa121e8823c.png)

# ソースコードと全体の解説
まず、2021年1月以前はgetplayerstatusというAPIを使ってコメントを取得することができました。[(このページの例のように)](https://qiita.com/59naga/items/0a22e30f019aaef683e4)
しかし廃止されてしまいましたので、コメントの取得のためにはWebSocketの仕組みを利用する必要が出てきました。
WebSocket歴2週間なので明言は避けますが、WebSocketはサーバーとクライアントで自由に通信し合える道路のようなものと認識しています。

普通にニコ生をブラウザで見る時は、ブラウザが裏で勝手にWebSocketを開通してくれているお陰で、配信やコメントがどんどん送られて来るということですね。

ニコ生のコメントを取得するためには、ブラウザが裏でやってくれている操作を自分でソースコードを書いて実行すればいいわけです。
流れとしては

1. 対象とするニコ生のページのhtmlファイルから、<font color="FireBrick">視聴用のWebSocket</font>開通の為に必要な情報を抽出する
2. その情報を元に、<font color="FireBrick">視聴用のWebSocket</font>を開通する
3. <font color="FireBrick">視聴用のWebSocket</font>に、「視聴を開始します」という旨のメッセージを送る
4. <font color="FireBrick">視聴用のWebSocket</font>から、<font color="MediumBlue ">コメント用のWebSocket</font>の開通の為に必要な情報が送られて来る
5. その情報を元に、<font color="MediumBlue ">コメント用のWebSocket</font>を開通する
6. <font color="MediumBlue ">コメント用のWebSocket</font>に、「コメントをください」という旨のメッセージを送る
7. コメントがどんどん送られて来る

となります。
それではソースコードを順番に解説していきます。

## htmlからの情報抽出
Chromeのデベロッパーツールで配信ページのhtmlを見ると「script id="embedded-data"」というタグに、重要そうな情報が色々書かれていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451017/255ce159-9c8c-65f6-440a-bc7d32608b2f.png)

視聴用のWebSocket開通に必要な情報はこのタグに書かれていますので、以下のコードで取ってきて変数に代入します。（「document」というのは、このhtmlを指します。念のため。。。）

```js
// アクセスした生放送のページのhtmlから情報を取ってくる
const embeddedData = JSON.parse(document.getElementById("embedded-data").getAttribute("data-props"));
const url_system = embeddedData.site.relive.webSocketUrl;
const user_id = embeddedData.user.id
```
ここで必要なのは「url_system」と「user_id」で、それぞれWebSocket開通用のurlと、ページを視聴しているユーザーのIDです。

## 視聴用のWebSocket開通
一番上の方の変数の定義が行われた後、次に実行されるのは一番下のこの部分です。

```js
// 視聴セッションとのWebSocket接続開始
connect_WebSocket_system();
```
この関数の中身は以下の通りです。


```js
/*視聴セッションのWebSocket関係の関数*/ 
// 視聴セッションとのWebSocket接続関数の定義
function connect_WebSocket_system()
{
  websocket_system = new WebSocket(url_system);
  websocket_system.onopen = function(evt) { onOpen_system(evt) };
  websocket_system.onclose = function(evt) { onClose_system(evt) };
  websocket_system.onmessage = function(evt) { onMessage_system(evt) };
  websocket_system.onerror = function(evt) { onError_system(evt) };
}
```

「websocket_system = new WebSocket(url_system)」で、さっき取ってきたurlを元に視聴用のWebSocketを開通させます。

その下の4行は「開通した時に実行される関数」「切断された時に実行される関数」「メッセージを受け取った時に実行される関数」「エラーを受け取った時に実行される関数」を定義しています。
具体的な中身は下の方で定義しています。

# 「視聴を開始します」という旨のメッセージを送る
先ほどwebsocket_systemオブジェクトの「websocket_system.onopen」は、WebSocketが開通した時に実行されます。

```js
// 視聴セッションとのWebSocket接続が開始された時に実行される
function onOpen_system(evt)
{
  console.log("CONNECTED TO THE SYSTEM SERVER");
  doSend_system(message_system_1);
  doSend_system(message_system_2);
}

// 視聴セッションへメッセージを送るための関数
function doSend_system(message)
{
  console.log("SENT TO THE SYSTEM SERVER: " + message);
  websocket_system.send(message);
}
```
「message_system_1」と「message_system_2」は、視聴開始時にサーバーへ送るメッセージで、その中身はそれぞれ

```
{"type":"startWatching","data":{"stream":{"quality":"abr","protocol":"hls","latency":"low","chasePlay":false},"room":{"protocol":"webSocket","commentable":true},"reconnect":false}}
```

```
{"type":"getAkashic","data":{"chasePlay":false}}
```

というjsonのような形をした構造体を文字列にしたものです。

デベロッパーツールで「Network」→「WS」→「Name」（の2行目）と辿るとブラウザが送受信したメッセージを見ることができますが、全く同じメッセージが最初に送信されていることが分かります。
（開いたまま更新すればメッセージのやり取りを最初から見ることができます。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451017/89953c9e-a103-a02e-d1a6-cb2d1c0e1e18.png)

ちなみに「Name」には3つありますが、長い文字列のものは視聴用のWebSocketで、「WebSocket」という名前のものはコメント用のWebSocketでのやり取りを見ることができます。

## コメント用のWebSocketの開通
視聴開始メッセージを送信したあと「{"type":"room",...」で始まるメッセージが送られて来ることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451017/c94e9e83-cce2-972e-2493-7cd3c494b7ea.png)

この中にコメント用のWebSocket開通のための情報が記載されていますので、以下のコードで取得しています。

```js
// 視聴セッションとのWebSocket接続中にメッセージを受け取った時に実行される
function onMessage_system(evt)
{
  console.log('RESPONSE FROM THE SYSTEM SERVER: ' + evt.data);
  is_room = evt.data.indexOf("room")
  is_ping = evt.data.indexOf("ping")

  // コメントセッションへ接続するために必要な情報が送られてきたら抽出してWebSocket接続を開始
  if(is_room>0){

      // 必要な情報を送られてきたメッセージから抽出
      evt_data_json = JSON.parse(evt.data);
      uri_comment = evt_data_json.data.messageServer.uri
      threadID = evt_data_json.data.threadId
      threadkey = evt_data_json.data.yourPostKey
      message_comment = '[{"ping":{"content":"rs:0"}},{"ping":{"content":"ps:0"}},{"thread":{"thread":"'+threadID+'","version":"20061206","user_id":"'+user_id+'","res_from":-150,"with_global":1,"scores":1,"nicoru":0,"threadkey":"'+threadkey+'"}},{"ping":{"content":"pf:0"}},{"ping":{"content":"rf:0"}}]'

      // コメントセッションとのWebSocket接続を開始
      connect_WebSocket_comment();
  }

  // pingが送られてきたらpongとkeepseatを送り、視聴権を獲得し続ける
  if(is_ping>0){
    doSend_system('{"type":"pong"}');
    doSend_system('{"type":"keepSeat"}');
  }
}
```

送られてきたメッセージの中身は「evt」に入っていますので、これをjson形式に整形して取り出しています。

開通に必要なのは「uri_comment」で、「threadID」と「threadkey」は開通後のメッセージ送信に必要な情報です。
（message_commentをここで定義していますが、これを次に送信します。）

connect_WebSocket_comment()の中身は以下の通りです。

```js
/*コメントセッションのWebSocket関係の関数*/ 
// コメントセッションとのWebSocket接続関数の定義
function connect_WebSocket_comment()
{
  websocket_comment = new WebSocket(uri_comment, 'niconama', {
    headers: {
      'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
      'Sec-WebSocket-Protocol': 'msg.nicovideo.jp#json',
    },
  });
  websocket_comment.onopen = function(evt) { onOpen_comment(evt) };
  websocket_comment.onclose = function(evt) { onClose_comment(evt) };
  websocket_comment.onmessage = function(evt) { onMessage_comment(evt) };
  websocket_comment.onerror = function(evt) { onError_comment(evt) };
}
```

参照したページによるとヘッダーをこのように設定する必要があるそうです。

## コメント用のWebSocketに、「コメントをください」という旨のメッセージを送る

コメント用のWebSocketが開通したら、メッセージを送ります。

```js
// コメントセッションとのWebSocket接続が開始された時に実行される
function onOpen_comment(evt)
{
  console.log("CONNECTED TO THE COMMENT SERVER");
  doSend_comment(message_comment);
}

// コメントセッションへメッセージを送るための関数
function doSend_comment(message)
{
  console.log("SENT TO THE COMMENT SERVER: " + message);
  websocket_comment.send(message);
}
```

送るメッセージの中身は先ほど定義した「message_comment」です。
具体的には以下の通りです。（なんで「[」と「]」で囲まれているんでしょうかね？）

```
[{"ping":{"content":"rs:0"}},{"ping":{"content":"ps:0"}},{"thread":{"thread":"「threadID」","version":"20061206","user_id":"「user_id」","res_from":-150,"with_global":1,"scores":1,"nicoru":0,"threadkey":"「threadkey」"}},{"ping":{"content":"pf:0"}},{"ping":{"content":"rf:0"}}]
```

「threadID」「user_id」「threadkey」には取得した値が代入されます。
threadIDとthreadkeyはWebSocketが開通するたびに変わります。

これの送信ができると、コメントがどんどん流れてくるようになります。

## その他
視聴用のWebSocketにおいて定期的にpingが送られて来るので、このソースコードではpongとksspSeatを返しています。
これをやらないと見てないと判断されて切断されてしまいます。

```js 
// pingが送られてきたらpongとkeepseatを送り、視聴権を獲得し続ける
  if(is_ping>0){
    doSend_system('{"type":"pong"}');
    doSend_system('{"type":"keepSeat"}');
  }
```
本来ならkeepSeatはpongと同時ではなく、特定の秒数に一回（30秒など）という仕様にすべきなので要改良ですね。
このスパンは一応指定されていて、メッセージで送られています。

また、視聴用のWebSocketは配信が終了すると切断されるのですが、その際に以下のコードでコメント用のWebSocketも切断するようにしています。

```js
// 視聴セッションとのWebSocket接続が切断された時に実行される
function onClose_system(evt)
{
  console.log("DISCONNECTED FROM THE SYSTEM SERVER");
  websocket_comment.close(); // コメントセッションとのWebSocket接続を切る
}
```
# ログインしない場合
実はログインしなくてもさっきのコードは実行できます。
user_idを"guest"にすることと、threadkeyを送らないようにすればいいだけです。

```niconama_comment_chrome.js
// アクセスした生放送のページのhtmlから情報を取ってくる
const embeddedData = JSON.parse(document.getElementById("embedded-data").getAttribute("data-props"));
const url_system = embeddedData.site.relive.webSocketUrl;

// websocketでセッションに送るメッセージ
const message_system_1 = '{"type":"startWatching","data":{"stream":{"quality":"abr","protocol":"hls","latency":"low","chasePlay":false},"room":{"protocol":"webSocket","commentable":true},"reconnect":false}}';
const message_system_2 ='{"type":"getAkashic","data":{"chasePlay":false}}'

// コメントセッションへWebSocket接続するときに必要な情報
let uri_comment
let threadID
let mes_comment

/*視聴セッションのWebSocket関係の関数*/ 
// 視聴セッションとのWebSocket接続関数の定義
function connect_WebSocket_system()
{
  websocket_system = new WebSocket(url_system);
  websocket_system.onopen = function(evt) { onOpen_system(evt) };
  websocket_system.onclose = function(evt) { onClose_system(evt) };
  websocket_system.onmessage = function(evt) { onMessage_system(evt) };
  websocket_system.onerror = function(evt) { onError_system(evt) };
}

// 視聴セッションとのWebSocket接続が開始された時に実行される
function onOpen_system(evt)
{
  console.log("CONNECTED TO THE SYSTEM SERVER");
  doSend_system(message_system_1);
  doSend_system(message_system_2);
}

// 視聴セッションとのWebSocket接続が切断された時に実行される
function onClose_system(evt)
{
  console.log("DISCONNECTED FROM THE SYSTEM SERVER");
  websocket_comment.close(); // コメントセッションとのWebSocket接続を切る
}

// 視聴セッションとのWebSocket接続中にメッセージを受け取った時に実行される
function onMessage_system(evt)
{
  console.log('RESPONSE FROM THE SYSTEM SERVER: ' + evt.data);
  is_room = evt.data.indexOf("room")
  is_ping = evt.data.indexOf("ping")

  // コメントセッションへ接続するために必要な情報が送られてきたら抽出してWebSocket接続を開始
  if(is_room>0){

      // 必要な情報を送られてきたメッセージから抽出
      evt_data_json = JSON.parse(evt.data);
      uri_comment = evt_data_json.data.messageServer.uri
      threadID = evt_data_json.data.threadId
      message_comment = '[{"ping":{"content":"rs:0"}},{"ping":{"content":"ps:0"}},{"thread":{"thread":"'+threadID+'","version":"20061206","user_id":"guest","res_from":-150,"with_global":1,"scores":1,"nicoru":0}},{"ping":{"content":"pf:0"}},{"ping":{"content":"rf:0"}}]'
      // コメントセッションとのWebSocket接続を開始
      connect_WebSocket_comment();
  }

  // pingが送られてきたらpongとkeepseatを送り、視聴権を獲得し続ける
  if(is_ping>0){
    doSend_system('{"type":"pong"}');
    doSend_system('{"type":"keepSeat"}');
  }
}

// 視聴セッションとのWebSocket接続中にエラーメッセージを受け取った時に実行される
function onError_system(evt)
{
  console.log('ERROR FROM THE SYSTEM SERVER: ' + evt.data);
}

// 視聴セッションへメッセージを送るための関数
function doSend_system(message)
{
  console.log("SENT TO THE SYSTEM SERVER: " + message);
  websocket_system.send(message);
}

/*コメントセッションのWebSocket関係の関数*/ 
// コメントセッションとのWebSocket接続関数の定義
function connect_WebSocket_comment()
{
  websocket_comment = new WebSocket(uri_comment, 'niconama', {
    headers: {
      'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
      'Sec-WebSocket-Protocol': 'msg.nicovideo.jp#json',
    },
  });
  websocket_comment.onopen = function(evt) { onOpen_comment(evt) };
  websocket_comment.onclose = function(evt) { onClose_comment(evt) };
  websocket_comment.onmessage = function(evt) { onMessage_comment(evt) };
  websocket_comment.onerror = function(evt) { onError_comment(evt) };
}

// コメントセッションとのWebSocket接続が開始された時に実行される
function onOpen_comment(evt)
{
  console.log("CONNECTED TO THE COMMENT SERVER");
  doSend_comment(message_comment);
}

// コメントセッションとのWebSocket接続が切断された時に実行される
function onClose_comment(evt)
{
  console.log("DISCONNECTED FROM THE COMMENT SERVER");
}

// コメントセッションとのWebSocket接続中にメッセージを受け取った時に実行される
function onMessage_comment(evt)
{
  console.log('RESPONSE FROM THE COMMENT SERVER: ' + evt.data);
}

// コメントセッションとのWebSocket接続中にエラーメッセージを受け取った時に実行される
function onError_comment(evt)
{
  console.log('ERROR FROM THE COMMENT SERVER: ' + evt.data);
}

// コメントセッションへメッセージを送るための関数
function doSend_comment(message)
{
  console.log("SENT TO THE COMMENT SERVER: " + message);
  websocket_comment.send(message);
}

// 視聴セッションとのWebSocket接続開始
connect_WebSocket_system();
```


# 終わりと次にやりたいこと
今回はあくまでChromeのデベロッパーツールでコメントの受信ができるようになっただけです。
次はNode.jsとかPHPとかPythonとかで何かしらのスクリプトを記述して、同じコードを動かして、コメントをデータベースに保存する仕組みを作りたいと思います。
[Pythonで取ってくる続きの記事を書きました。](https://qiita.com/fingerprint216/items/e4b24484c5f7eae0a3d1)

# 参考サイト
- [ニコ生のコメント送受信をWebSocket+JSONでやる方法ざっくり解説](https://qiita.com/tor4kichi/items/4df5b11ec564bb8f8d16)(仕様が現在のものと結構変わっていますが、大枠の考え方はかなり参考になりました。)
- [ニコ生チャット取得](https://qiita.com/pasta04/items/33da06cf3c21e34fc4d1)
- [websocket.org](https://www.websocket.org/echo.html)(WebSocketのおためしができます。)