【.NET】Youtube Data Apiを使ってYouTube 動画コメントを全取得する

# はじめに
これは、[Visual Basic Advent Calendar 2020](https://qiita.com/advent-calendar/2020/vb)の22日目の記事となります。

* [【Python】Youtube Data Apiを使ってYouTube 動画コメントを全取得する](https://qiita.com/yaju/items/3bec88dbd544502e1343)
* [【VBA】ExcelでYoutube Data Apiを使ってYouTube 動画コメントを全取得する](https://qiita.com/yaju/items/dd0d8359d8f926c32627)
* [【VBA】Excel for MacでYoutube Data Apiを使ってYouTube 動画コメントを全取得する](https://qiita.com/yaju/items/eb47e20446d7e43d8e55)
* [【GAS】Youtube Data Apiを使ってYouTube 動画コメントを全取得する](https://qiita.com/yaju/items/017b166c89de027759c9)

どんだけ同じネタを使い回すのか、とはいえ、.NET版を作ってなかったんだよね。

# 環境
* Windows 10 Home
* .NET 5
* Visual Studio 2019
* Google.Apis.Youtube.v3 v1.49.0.2173

## 準備
今回のプログラムではYoutube Data Apiを使用するので、Apiキーの有効化を行っていきます。
Apiキーの有効化などは下記サイトを参考にしてください。
[Youtube Data APIを使用して、Youtube Liveのコメントを取得する](https://qiita.com/MCK9595/items/fdbd543ff938febcd136)

### ライブラリーの追加
.NETでYoutube Data Apiを使用するにはGoogle.Apis.Youtube.v3が必要なので、NuGetパッケージマネージャーから追加してください。

1. NuGetパッケージマネージャーを起動します。
2. 参照タブで、Google.Apis.Youtube.v3を検索します。
3. Google.Apis.Youtube.v3をインストールします。

# 仕様
連番を付与しユーザー名と子コメントが取得できるようになっています。

タブ区切りでコメントの改行は半角空白に置換しています。
コメントのフォーマットはhtmlかplain textがあるのですが、plain textになっています。
並び順はrelevanceで評価順が多い順にしています。子コメントについては順不動（並び順を指定しても動画サイトと同じにならない）です。
親連番は4桁、子の連番は3桁にしているので桁数を超えるコメントを取得したい場合は表示する桁数を変更増やすといいでしょう。

※APIの使用回数を減らすため 親コメントはmaxResults=100、子コメントはmaxResults=50とする。
※コメントでは低評価(disLike)数は取得できない。

```
000X    (コメント)  （Like数）   （ユーザー名）   （投稿日時)   （返信数)
000X-00X    (子コメント)   （グッド数）  （ユーザー名）   （投稿日時)
```

【2021/05/28追記】
並び順を評価順(relevance)にした場合、YouTube Data API v3の制約上2000件を超える親コメントが取得できません。
並び順を新しい順(time)にした場合、上限が不明ですが2000件を超える親コメントが取得できます。

# 使用方法
```vb:VisualBasic
' ここにAPI KEYを入力
Private Const API_KEY As String = "(API KEYを入力)"

' ここにVideo IDを入力
Private Const VIDEO_ID As String = "(Video IDを入力)"
```

```csharp:C#
// ここにAPI KEYを入力
private const string API_KEY = "(API KEYを入力)"

// ここにVideo IDを入力
private const string VIDEO_ID = "(Video IDを入力)"
```

本来は、Video IDは外部から変更できるようにしておくといいと思います。

## API_KEYを入力
Youtube Data APIのAPI有効化の認証情報でAPIキーが発行されるので、プログラムのAPI_KEYを入力で発行されたAPIキーに書き換えてください。

## Video IDを入力
例えば、「[https://www.youtube.com/watch?v=oeJ_b0iG9lM](https://www.youtube.com/watch?v=oeJ_b0iG9lM)」であれば、`oeJ_b0iG9lM`がVideo IDとなりますので、プログラムのVideo IDを入力で対象動画のVideo IDに書き換えてください。

# プログラム
## VisualBasic
```vb:VisualBasic
Imports Google.Apis.Services
Imports Google.Apis.YouTube.v3

' ここにAPI KEYを入力
Private Const API_KEY As String = "(API KEYを入力)"

' ここにVideo IDを入力
Private Const VIDEO_ID As String = "(Video IDを入力)"

Public Class CommentInfo
    Public ParentNo As Integer
    Public ChildNo As Integer
    Public Text As String
    Public LikeCount As Long
    Public AuthorName As String
    Public PublishedAt As DateTime
    Public ReplyCount As Long
End Class

Public Class Form1

    'ここにAPI KEYを入力
    Private Const API_KEY As String = "(API KEYを入力)"

    'ここにVideo IDを入力
    Private Const VIDEO_ID As String = "(Video IDを入力)"

    Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim youtubeService = New YouTubeService(New BaseClientService.Initializer() With {
            .ApiKey = API_KEY
        })
        Dim videoId = VIDEO_ID
        Dim commentList As List(Of CommentInfo) = New List(Of CommentInfo)()

        Await GetComment(commentList, videoId, youtubeService, 1, Nothing)

        For Each info In commentList
            Dim line As String

            If info.ChildNo = 0 Then
                line = String.Format("{0:0000}\t{1}\t{2}\t{3}\t{4}\t{5}", info.ParentNo, info.Text.Replace(vbLf, ""), info.LikeCount, info.AuthorName, info.PublishedAt, info.ReplyCount)
            Else
                line = String.Format("{0:0000}-{1:000}\t{2}\t{3}\t{4}\t{5}", info.ParentNo, info.ChildNo, info.Text.Replace(vbLf, ""), info.LikeCount, info.AuthorName, info.PublishedAt)
            End If
            line = line.Replace("\t", vbTab)

            System.Diagnostics.Debug.WriteLine(line)
        Next

        MessageBox.Show("出力完了しました。")
    End Sub

    ' コメント取得
    Public Shared Async Function GetComment(ByVal commentList As List(Of CommentInfo), ByVal videoId As String, ByVal youtubeService As YouTubeService, ByVal no As Integer, ByVal nextPageToken As String) As Task
        Dim request = youtubeService.CommentThreads.List("snippet")
        request.VideoId = videoId
        request.Order = CommentThreadsResource.ListRequest.OrderEnum.Relevance
        request.TextFormat = CommentThreadsResource.ListRequest.TextFormatEnum.PlainText
        request.MaxResults = 100
        request.PageToken = nextPageToken
        Dim response = Await request.ExecuteAsync()

        For Each item In response.Items
            Try
                Dim info As CommentInfo = New CommentInfo()
                info.ParentNo = no
                info.ChildNo = 0
                info.Text = item.Snippet.TopLevelComment.Snippet.TextDisplay
                info.LikeCount = CLng(item.Snippet.TopLevelComment.Snippet.LikeCount)
                info.AuthorName = item.Snippet.TopLevelComment.Snippet.AuthorDisplayName
                info.PublishedAt = JsonDateToDate(item.Snippet.TopLevelComment.Snippet.PublishedAt)
                info.ReplyCount = CLng(item.Snippet.TotalReplyCount)
                Dim parentId As String = item.Snippet.TopLevelComment.Id
                commentList.Add(info)
                If item.Snippet.TotalReplyCount > 0 Then
                    Await GetReplyComment(commentList, youtubeService, parentId, no, 1, Nothing)
                End If
                no += 1
            Catch
            End Try
        Next

        If response.NextPageToken IsNot Nothing Then
            Await GetComment(commentList, videoId, youtubeService, no, response.NextPageToken)
        End If
    End Function

    ' 返信コメント取得
    Public Shared Async Function GetReplyComment(ByVal commentList As List(Of CommentInfo), ByVal youtubeService As YouTubeService, ByVal parentId As String, ByVal no As Integer, ByVal cno As Integer, ByVal nextPageToken As String) As Task
        Dim request = youtubeService.Comments.List("snippet")
        request.TextFormat = CommentsResource.ListRequest.TextFormatEnum.PlainText
        request.MaxResults = 50
        request.ParentId = parentId
        request.PageToken = nextPageToken
        Dim response = Await request.ExecuteAsync()

        For Each item In response.Items
            Try
                Dim info As CommentInfo = New CommentInfo()
                info.ParentNo = no
                info.ChildNo = cno
                info.Text = item.Snippet.TextDisplay
                info.LikeCount = CLng(item.Snippet.LikeCount)
                info.AuthorName = item.Snippet.AuthorDisplayName
                info.PublishedAt = JsonDateToDate(item.Snippet.PublishedAt)
                commentList.Add(info)
                cno += 1
            Catch
            End Try
        Next

        If response.NextPageToken IsNot Nothing Then Await GetReplyComment(commentList, youtubeService, parentId, no, cno, response.NextPageToken)
    End Function

    ' JSON日付変換
    Private Shared Function JsonDateToDate(ByVal dt As String) As DateTime
        Dim result As DateTime
        DateTime.TryParse(dt, result)
        Return result
    End Function
End Class
```

## CSharp
```csharp:C#
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Forms;
using Google.Apis.Services;
using Google.Apis.YouTube.v3;

public class CommentInfo
{
    public int ParentNo;
    public int ChildNo;
    public string Text;
    public long LikeCount;
    public string AuthorName;
    public DateTime PublishedAt;
    public long ReplyCount;
}

namespace WindowsFormsApp
{
    public partial class Form1 : Form
    {
        // ここにAPI KEYを入力
        private const string API_KEY = "(API KEYを入力)"

        // ここにVideo IDを入力
        private const string VIDEO_ID = "(Video IDを入力)"

        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            var youtubeService = new YouTubeService(new BaseClientService.Initializer()
            {
                ApiKey = API_KEY
            });

            var videoId = VIDEO_ID;

            List<CommentInfo> commentList = new List<CommentInfo>();
            await GetComment(commentList, videoId, youtubeService, 1, null);

            foreach(var info in commentList)
            {
                string line;
                if (info.ChildNo == 0)
                    line = string.Format("{0:0000}\t{1}\t{2}\t{3}\t{4}\t{5}", info.ParentNo, info.Text.Replace("\n", ""), info.LikeCount, info.AuthorName, info.PublishedAt, info.ReplyCount);
                else
                    line = string.Format("{0:0000}-{1:000}\t{2}\t{3}\t{4}\t{5}", info.ParentNo, info.ChildNo, info.Text.Replace("\n", ""), info.LikeCount, info.AuthorName, info.PublishedAt);
                System.Diagnostics.Debug.WriteLine(line);
            }

            MessageBox.Show("出力が完了しました。");
        }

        // コメント取得
        static public async Task GetComment(List<CommentInfo> commentList, string videoId, YouTubeService youtubeService, int no, string nextPageToken)
        {
            var request = youtubeService.CommentThreads.List("snippet");
            request.VideoId = videoId;
            request.Order = CommentThreadsResource.ListRequest.OrderEnum.Relevance;
            request.TextFormat = CommentThreadsResource.ListRequest.TextFormatEnum.PlainText;
            request.MaxResults = 100;
            request.PageToken = nextPageToken;

            var response = await request.ExecuteAsync();
            foreach (var item in response.Items)
            {
                try
                {
                    CommentInfo info = new CommentInfo();
                    info.ParentNo = no;
                    info.ChildNo = 0;
                    info.Text = item.Snippet.TopLevelComment.Snippet.TextDisplay;
                    info.LikeCount = (long)item.Snippet.TopLevelComment.Snippet.LikeCount;
                    info.AuthorName = item.Snippet.TopLevelComment.Snippet.AuthorDisplayName;
                    info.PublishedAt = JsonDateToDate(item.Snippet.TopLevelComment.Snippet.PublishedAt);
                    info.ReplyCount = (long)item.Snippet.TotalReplyCount;
                    string parentId = item.Snippet.TopLevelComment.Id;
                    commentList.Add(info);

                    if (item.Snippet.TotalReplyCount > 0)
                        await GetReplyComment(commentList, youtubeService, parentId, no, 1, null);
                    no++;
                }
                catch { }
            }

            if (response.NextPageToken != null)
                await GetComment(commentList, videoId, youtubeService, no, response.NextPageToken);
        }

        // 返信コメント取得
        static public async Task GetReplyComment(List<CommentInfo> commentList, YouTubeService youtubeService, string parentId, int no, int cno, string nextPageToken)
        {
            var request = youtubeService.Comments.List("snippet");
            request.TextFormat = CommentsResource.ListRequest.TextFormatEnum.PlainText;
            request.MaxResults = 50;
            request.ParentId = parentId;
            request.PageToken = nextPageToken;

            var response = await request.ExecuteAsync();

            foreach (var item in response.Items)
            {
                try
                {
                    CommentInfo info = new CommentInfo();
                    info.ParentNo = no;
                    info.ChildNo = cno;
                    info.Text = item.Snippet.TextDisplay;
                    info.LikeCount = (long)item.Snippet.LikeCount;
                    info.AuthorName = item.Snippet.AuthorDisplayName;
                    info.PublishedAt = JsonDateToDate(item.Snippet.PublishedAt);
                    commentList.Add(info);
                    cno++;
                }
                catch { }

            }

            if (response.NextPageToken != null)
                await GetReplyComment(commentList, youtubeService, parentId, no, cno, response.NextPageToken);
        }

        // JSON日付変換
        static private DateTime JsonDateToDate(string dt)
        {
            DateTime result;
            DateTime.TryParse(dt, out result);

            return result;
        }
    }
}
```

# 実行結果
郡司りかさんを御存知でしょうか？
マツコ・デラックスさんと村上信五（関ジャニ∞）さんがMCの「月曜から夜更かし」の中で、運動音痴として注目を浴びた女性です。
郡司りかさんはYoutubeとTwitterをやられており、ツイート画像に「おはよー」を隠すという遊びをしています。
[郡司りかさんのツイート画像の「おはよー」を見つける](https://note.com/yaju/n/n80d0dab9b192)

「[【NiziU】郡司さんMake You Happyにチャレンジ (練習前)](https://www.youtube.com/watch?v=swZVrg3H-8E)」の動画コメントを取得すると、下記のようになります。
![YoutubeCommentDebug.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/27e9067d-2032-9c55-4f05-70f849ea5ece.png)

# 悩んだところ
## 出力画面に表示されない
`Console.WriteLine`を使用したが出力画面に何も表示されなかったので、`Debug.WriteLine`にしました。
WinFormだと出ないんでしたっけ。

## 返信コメントの取得
返信コメントは5件までだったら、`item.snippet.replies.comments`の配下で取得出来るはずなんですが、駄目でした。取得できるならもう少し高速化できたんですけどね。

Google App Scriptでは取得出来るので、.NET版で取れないのかも知れません。
[Google Apps ScriptとYoutubeのAPIを利用して動画のコメントを取得する](https://www.acrovision.jp/service/gas/?p=568)

# 最後に
最初に C# で作成しておき、「[Telerik Code Converter](http://converter.telerik.com/)」のサイトでVisual Basicに変換しました。
[Visual BasicからC#への変換について](https://qiita.com/yaju/items/3afb486bfbbddc1e6aa8)