2020年8月21日

Google Apps Script で YouTube 動画コメントを取得する


YouTube API を利用して動画コメントを取得&スプレッドシートに書き出す方法をメモメモ…




動画についたコメントのデータをスクリプトから読み込んでスプレッドシートに出力する方法です。ややこしかったのでメモしておきます。自分もまだ勉強中で間違いがあるかもしれないので、参考程度にご覧ください。ちなみに Google Apps Script で YouTube API を使う方法についてはこちらの記事で解説しています。

というわけで、まずは完成したコードを見てみましょう!長いよ!

  1. // メニュー項目を追加する
  2. function onOpen()
  3. {
  4. const menu = SpreadsheetApp.getUi().createMenu("スクリプト");
  5. menu.addItem("コメント取得開始", "listYouTubeComments");
  6. menu.addToUi();
  7. }
  8.  
  9. // YouTube の動画コメントを取得しシートに出力する
  10. function listYouTubeComments()
  11. {
  12. const videoId = "vBmU5v2EyxM";
  13. const maxComments = 10000;
  14. const outputSheetName = "YouTubeComments";
  15. const commentData = retrieveComments(videoId, maxComments);
  16. outputData(commentData, outputSheetName);
  17. }
  18.  
  19. // 指定した動画のコメントを取得し配列に入れて返す
  20. function retrieveComments(videoId, maxComments)
  21. {
  22. let commentData = [["ID", "ParentID", "PublishedAt", "LikeCount", "AuthorDisplayName", "TextDisplay"]];
  23. let nextPageToken = "";
  24. // コメントの各種データを配列に格納する
  25. // 最大コメント数に到達した場合は false を返す
  26. function pushCommentData(comment)
  27. {
  28. const id = comment.id;
  29. const parent = comment.snippet.parentId;
  30. const time = comment.snippet.publishedAt;
  31. const likes = comment.snippet.likeCount;
  32. const name = comment.snippet.authorDisplayName;
  33. const text = comment.snippet.textDisplay.replace(/\r?\n/g,""); // 改行は削除
  34. commentData.push([id, parent, time, likes, name, text]);
  35. return commentData.length < maxComments;
  36. }
  37. // 指定したコメントに対する返信コメントをすべて取得する
  38. // 途中で最大コメント数に到達してしまったら false を返す
  39. function getReplies(parentId)
  40. {
  41. let nextPageToken = "";
  42. while(nextPageToken != null) {
  43. const replies = YouTube.Comments.list("id, snippet", {
  44. parentId: parentId,
  45. maxResults: 100,
  46. pageToken: nextPageToken,
  47. textFormat: "plainText"
  48. });
  49. for(let i = 0; i < replies.items.length; i++) {
  50. if(!pushCommentData(replies.items[i])) return false;
  51. }
  52. nextPageToken = replies.nextPageToken;
  53. }
  54. return true;
  55. }
  56. // コメントスレッド(ツリー)最上位のコメントを拾っていく
  57. while(nextPageToken != null) {
  58. const commentThreads = YouTube.CommentThreads.list("id, replies, snippet", {
  59. videoId: videoId,
  60. maxResults: 100,
  61. pageToken: nextPageToken,
  62. textFormat: "plainText"
  63. });
  64. const items = commentThreads.items;
  65. for(let i = 0; i < items.length; i++) {
  66. if(!pushCommentData(items[i].snippet.topLevelComment)) return commentData;
  67. if(items[i].replies == null) continue;
  68. // コメントに返信がついている場合はそちらも取得
  69. if(items[i].replies.comments.length == items[i].snippet.totalReplyCount) {
  70. for(let j = 0; j < items[i].replies.comments.length; j++) {
  71. if(!pushCommentData(items[i].replies.comments[j])) return commentData;
  72. }
  73. // 返信が省略されてしまっている場合は Comments.list で取得しなおす
  74. }else {
  75. if(!getReplies(items[i].snippet.topLevelComment.id)) return commentData;
  76. }
  77. }
  78. nextPageToken = commentThreads.nextPageToken;
  79. }
  80. return commentData;
  81. }
  82.  
  83. // 配列に入ったデータをシートに書き込む
  84. function outputData(array, sheetName)
  85. {
  86. const outputSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  87. outputSheet.clearContents();
  88. if(array == null || array.length == 0) return;
  89. outputSheet.getRange(1, 1, array.length, array[0].length).setValues(array);
  90. }


対象となる動画、取得するコメントの上限、出力先のシートなどをlistYouTubeComments関数内で指定して、実際にコメントを取ってくる処理はretrieveComments関数で行っています。ざっと眺めてもらえれば返信コメントに関する処理でかなりの行数を使っていることがわかると思いますが、この返信の扱いについては後述します。

Comments と CommentThreads

YouTube API のドキュメントには、コメント関連で CommentsCommentThreads という2つのリソースが登場します。自分はまずここで少し戸惑ってしまいましたが、両者の違いは次のような図を想像すると理解しやすいかもしれません。

(コメント内容はイメージです)


CommentThread には各スレッドの情報が含まれています。「スレッド」というのは「コメントとそれに対する返信をまとめたもの」ですよね。たとえば今回のスクリプトで使用したデータには


snippet.videoIdこのスレッドがある動画のID
snippet.totalReplyCount返信コメント数
snippet.topLevelComment最初のコメントへの参照


などがあります(参考:プロパティ一覧)。そして、動画のコメント欄は、この CommentThread が集まってできています。それらを取得するのがYouTube.CommentThreads.listメソッドというわけです。第一引数で「どういう種類のデータを返してほしいか」を指定。第二引数で対象となる動画のID(複数指定可)やオプションなどを渡しています。


  1. const commentThreads = YouTube.CommentThreads.list("id, replies, snippet", {
  2. videoId: videoId,
  3. maxResults: 100,
  4. pageToken: nextPageToken,
  5. textFormat: "plainText"
  6. });


一方、その CommentThread に含まれるコメントや返信のひとつひとつが Comment です。今回作成したスクリプトでは次のプロパティを利用しました。


idコメントの固有ID
snippet.parentId返信先のコメントの固有ID
snippet.publishedAt書き込まれた日時
snippet.likeCount高評価数
snippet.authorDisplayNameユーザー名
snippet.textDisplayコメント内容


ほかにもコメントを書き込んだユーザーのアイコンやチャンネルページへのURLなども取得できます。詳しくは公式リファレンスのプロパティ一覧を見てください。とにかく「今参照したいプロパティは Comments のものなのか、それとも CommentThreads のものなのか」ということをしっかり確認しながらコードを書かないと、迷子になります。注意しましょう。

返信コメントの取得方法

今回特にめんどくさかったのが返信コメントの扱いです。CommentThreads にreplies.commentsというプロパティがあったので「これで簡単に取得できるじゃ~ん!」と調子に乗っていたのですが…実はこれは罠でした。というのも、返信コメントが複数ある場合replies.commentsすべてのコメントが含まれていないことがあるからです。このことはリファレンスにも書かれています。

すべての返信コメントが含まれているかどうかは、77行目のようにtotalReplyCountの数値とreplies.commentsの要素数を比べてみればわかります。


  1. if(items[i].replies.comments.length == items[i].snippet.totalReplyCount) {


もし両者の数が一致していればそのままreplies.commentsで取得してOK。一致しなかった場合はYouTube.Comments.listメソッドを利用して取得しなおす必要があります。今回のコードでは42行目からのgetRepliesというローカル関数内で返信コメント取得の処理を行っています。


  1. function getReplies(parentId)
  2. {
  3. let nextPageToken = "";
  4. while(nextPageToken != null) {
  5. const replies = YouTube.Comments.list("id, snippet", {
  6. parentId: parentId,
  7. maxResults: 100,
  8. pageToken: nextPageToken,
  9. textFormat: "plainText"
  10. });
  11. for(let i = 0; i < replies.items.length; i++) {
  12. if(!pushCommentData(replies.items[i])) return false;
  13. }
  14. nextPageToken = replies.nextPageToken;
  15. }
  16. return true;
  17. }


YouTube.Comments.listYouTube.CommentThreads.listとは違い、コメントのIDを渡すとそれに対する返信を取得できるメソッドです。ちなみにリファレンスには「現在はスレッドの最初のコメントに対する返信しかサポートしていないが、将来的には"返信への返信"にも対応するかも」ということが書かれています。そうなるとさらにややこしくなりそうですね…。

それから、何十万件何百万件レベルの大量のコメントを取得したい場合は、サービスの使用量上限にも注意しましょう。Google Apps Script での各処理はユーザーごとに1日の割り当てが決まっています。また YouTube Data API のメソッドの利用にもおそらくコストがかかります(「おそらく」というのは自分はまだ制限にひっかかったことがないからです)。たとえばCommentThreads.listComments.listといったメソッドは呼び出して1ページ分(最大100件)読み込むごとに1コストかかります。処理ごとの制限やコストについては次のページを参照してください。

Quotas for Google Services | Apps Script | Google Developers
https://developers.google.com/apps-script/guides/services/quotas

YouTube Data API (v3) - Quota Calculator | Google Developers
https://developers.google.com/youtube/v3/determine_quota_cost


…ただ、そもそも割り当てが今どれだけ残っているかを確認する方法がさっぱりわからないんですよね… Google Cloud を通していれば専用の確認ページがあるのですが、GAS の場合はどこで見ればいいんだろう…

0 件のコメント:

コメントを投稿