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

# はじめに
[前回の記事](https://qiita.com/fingerprint216/items/7938835a8ff69d73a465)の続きです。
取ってきたコメントを手軽扱うためにPythonで取ってくる手順を考えました。
コードの実行は自己責任でお願いします。

# 現時点でできたソースコード
取得したい放送のIDを書いてお好きなコンソールで実行してください。
jupyter notebookで実行するとasyncioのせいでエラーが出るかもしれません。（[参考記事](https://qiita.com/sin_tanaka/items/56eb1b8a677fb9b50f7b)）

```niconama_comment_python.py
### 各種ライブラリのインポート
import requests
import json
import time
import asyncio
import websockets
from bs4 import BeautifulSoup

### コメントを取得したい放送のURLを指定
live_id = "lv330006911"
#live_id = "co3000390" # コミュニティIDを指定すると放送中のものを取ってきてくれる
url = "https://live2.nicovideo.jp/watch/"+live_id

### htmlを取ってきてWebSocket接続のための情報を取得
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser") 
embedded_data = json.loads(soup.find('script', id='embedded-data')["data-props"])
url_system = embedded_data["site"]["relive"]["webSocketUrl"]

### websocketでセッションに送るメッセージ
message_system_1 = {"type":"startWatching",
                    "data":{"stream":{"quality":"abr",
                                      "protocol":"hls",
                                      "latency":"low",
                                      "chasePlay":False},
                            "room":{"protocol":"webSocket",
                                    "commentable":True},
                            "reconnect":False}}
message_system_2 ={"type":"getAkashic",
                   "data":{"chasePlay":False}}
message_system_1 = json.dumps(message_system_1)
message_system_2 = json.dumps(message_system_2)

### コメントセッション用のグローバル変数
uri_comment = None
message_comment = None

### 視聴セッションとのWebSocket接続関数
async def connect_WebSocket_system():
    global url_system
    global uri_comment
    global message_comment

    ### 視聴セッションとのWebSocket接続を開始
    async with websockets.connect(url_system) as websocket:

        ### 最初のメッセージを送信
        await websocket.send(message_system_1)
        await websocket.send(message_system_2) # これ送らなくても動いてしまう？？
        print("SENT TO THE SYSTEM SERVER: ",message_system_1)
        print("SENT TO THE SYSTEM SERVER: ",message_system_2)

        ### 視聴セッションとのWebSocket接続中ずっと実行
        while True:
            message = await websocket.recv()
            message_dic = json.loads(message)
            print("RESPONSE FROM THE SYSTEM SERVER: ",message_dic)

            ### コメントセッションへ接続するために必要な情報が送られてきたら抽出してグローバル変数へ代入
            if(message_dic["type"]=="room"):
                uri_comment = message_dic["data"]["messageServer"]["uri"]
                threadID = message_dic["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"}}]
                message_comment = json.dumps(message_comment)

            ### pingが送られてきたらpongとkeepseatを送り、視聴権を獲得し続ける
            if(message_dic["type"]=="ping"):
                pong = json.dumps({"type":"pong"})
                keepSeat = json.dumps({"type":"keepSeat"})
                await websocket.send(pong)
                await websocket.send(keepSeat)
                print("SENT TO THE SYSTEM SERVER: ",pong)
                print("SENT TO THE SYSTEM SERVER: ",keepSeat)

### コメントセッションとのWebSocket接続関数
async def connect_WebSocket_comment():
    loop = asyncio.get_event_loop()

    global uri_comment
    global message_comment

    ### 視聴セッションがグローバル変数に代入するまで1秒待つ
    await loop.run_in_executor(None, time.sleep, 1)

    ### コメントセッションとのWebSocket接続を開始
    async with websockets.connect(uri_comment) as websocket:

            ### 最初のメッセージを送信
        await websocket.send(message_comment)
        print("SENT TO THE COMMENT SERVER: ",message_comment)

        ### コメントセッションとのWebSocket接続中ずっと実行
        while True:
            message = await websocket.recv()
            message_dic = json.loads(message)
            print("RESPONSE FROM THE COMMENT SERVER: ",message_dic)

### asyncioを用いて上で定義した2つのWebSocket実行関数を並列に実行する
loop = asyncio.get_event_loop()
gather = asyncio.gather(
    connect_WebSocket_system(),
    connect_WebSocket_comment())
loop.run_until_complete(gather)
```
# 実行例
取得したコメントがプロンプトに流れていきます。（右側）
使わせていただいた配信ページ。[(https://live2.nicovideo.jp/watch/lv330006911)](https://live2.nicovideo.jp/watch/lv330289881)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451017/243214ac-81a7-9ebd-78fd-ebe708bb53ae.png)



# ソースコードの解説
基本的な流れは前回と同じです。
[前回の記事](https://qiita.com/fingerprint216/items/7938835a8ff69d73a465)を参考にしてください。
## htmlからの情報抽出
まずはhtmlから視聴用のWebSocket開通に必要な情報を取ってきます。
requestライブラリでhtmlを取ってきて、BeautifulSoupでタグを検索して、jsonで整形しています。

```python
### 各種ライブラリのインポート
import requests
import json
from bs4 import BeautifulSoup

### コメントを取得したい放送のURLを指定
live_id = "lv330006911"
#live_id = "co3000390" # コミュニティIDを指定すると放送中のものを取ってきてくれる
url = "https://live2.nicovideo.jp/watch/"+live_id

### htmlを取ってきてWebSocket接続のための情報を取得
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser") 
embedded_data = json.loads(soup.find('script', id='embedded-data')["data-props"])
url_system = embedded_data["site"]["relive"]["webSocketUrl"]
```

## 視聴用のWebSocket開通
情報を取ってきたら視聴用のWebSocketを開通させます。
関係する部分は以下の関数です。

```python
### 視聴セッションとのWebSocket接続関数
async def connect_WebSocket_system():
    global url_system
    global uri_comment
    global message_comment

    ### 視聴セッションとのWebSocket接続を開始
    async with websockets.connect(url_system) as websocket:

        ### 最初のメッセージを送信
        await websocket.send(message_system_1)
        await websocket.send(message_system_2) # これ送らなくても動いてしまう？？
        print("SENT TO THE SYSTEM SERVER: ",message_system_1)
        print("SENT TO THE SYSTEM SERVER: ",message_system_2)

        ### 視聴セッションとのWebSocket接続中ずっと実行
        while True:
            message = await websocket.recv()
            message_dic = json.loads(message)
            print("RESPONSE FROM THE SYSTEM SERVER: ",message_dic)

            ### コメントセッションへ接続するために必要な情報が送られてきたら抽出してグローバル変数へ代入
            if(message_dic["type"]=="room"):
                uri_comment = message_dic["data"]["messageServer"]["uri"]
                threadID = message_dic["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"}}]
                message_comment = json.dumps(message_comment)

            ### pingが送られてきたらpongとkeepseatを送り、視聴権を獲得し続ける
            if(message_dic["type"]=="ping"):
                pong = json.dumps({"type":"pong"})
                keepSeat = json.dumps({"type":"keepSeat"})
                await websocket.send(pong)
                await websocket.send(keepSeat)
                print("SENT TO THE SYSTEM SERVER: ",pong)
                print("SENT TO THE SYSTEM SERVER: ",keepSeat)
```
asyncとかawaitは、視聴用のWebSocketとコメント用のWebSocketを並列で実行するために必要な文法です。

さっき取ってきたURLを用いて接続し、繋がったら最初に視聴開始メッセージを送ります。
その後はwhileの中でメッセージが送られて来るのを待ち、room情報が来たらグローバル変数に代入します。
pingが送られてきたらpongとkeepSeatを送り返して座席を守り続けます。

## コメント用のWebSocketの開通
視聴用のWebSocketでコメントサーバーの情報が送られてきたら視聴用のWebSocketを開通させます。
関係する部分は以下の関数です。

```python
### コメントセッションとのWebSocket接続関数
async def connect_WebSocket_comment():
    loop = asyncio.get_event_loop()

    global uri_comment
    global message_comment

    ### 視聴セッションがグローバル変数に代入するまで1秒待つ
    await loop.run_in_executor(None, time.sleep, 1)

    ### コメントセッションとのWebSocket接続を開始
    async with websockets.connect(uri_comment) as websocket:

            ### 最初のメッセージを送信
        await websocket.send(message_comment)
        print("SENT TO THE COMMENT SERVER: ",message_comment)

        ### コメントセッションとのWebSocket接続中ずっと実行
        while True:
            message = await websocket.recv()
            message_dic = json.loads(message)
            print("RESPONSE FROM THE COMMENT SERVER: ",message_dic)

### asyncioを用いて上で定義した2つのWebSocket実行関数を並列に実行する
loop = asyncio.get_event_loop()
gather = asyncio.gather(
    connect_WebSocket_system(),
    connect_WebSocket_comment())
loop.run_until_complete(gather)
```

基本的には視聴用のWebSocketとコメント用のWebSocketを同時に動かす仕組みなのですが、先に視聴用の方が動いてコメントサーバーの情報を取ってきてほしいので、コメントの方を1秒だけ待たせています。（ちょっと頭が悪いですね...）
接続をしてコメント取得のメッセージを送るという流れは今までと同じです。

## 気になる点
- WebSocket開通時に今回はheaderを指定していませんが開通してしまいました。後で動かなくなるかも？
- message_system_2を送らなくても動いてしまいました。
- 放送が終了したら視聴用の方のWebSocketがサーバー側から切断され、プログラムの実行がエラーで終了するようになっています。ちゃんとした終了のためにはもう一工夫が必要。

# まとめ
やりたかったコメント取得は出来たので、ここから色々と作っていきたいと思います。
恐らく少ししたら仕様はまた変わるでしょうので、その時はまた記事にするかもしれません。

# 参考サイト
- [Web初心者のためのWebSocket概要と各言語におけるサンプルコード](https://myenigma.hatenablog.com/entry/2020/10/10/154022#Python%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%B3%E3%83%BC%E3%83%89)
- [asyncioでPythonの非同期処理を書いてみる](https://dev.classmethod.jp/articles/python-asyncio/)
- [websocketsドキュメント](https://websockets.readthedocs.io/en/stable/api.html)