2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Google Apps ScriptAdvent Calendar 2023

Day 5

【GAS】YouTubeの動画の統計を取る

Posted at

できるもの

設定した時間おきに、動画の「集計日時、再生回数、いいね数、コメント数」が、
スプレッドシートの指定した列に追加されていきます。
一気に複数の動画を集計することも可能です。

動画ID(URLから特定可能)がわかればどんな動画も集計できると思います。

ちなみに、スプレッドシートの機能でデータをグラフにすると見てて楽しいです。

他の方の動画の統計を投稿者の了承なく取っていいのか、またその統計結果を許可を取らずに公開していいのかどうかは、私にはわかりません。
詳しい人いたら教えてください。

また、この記事を参考にしたことで何かあっても責任は取りません。
そこのところはご了承ください。
今のところ特に何もないから大丈夫だとは思うけど...

注意

この記事を書いている間にコードを1から作り直したので、一部作り直す前のコードや説明が入っているかもしれません。

時系列としては、

  1. この記事を書き始める
  2. スプレッドシートもGASのプロジェクトも1から作り直す
  3. 期限が迫っていることに気づいてこの記事の続きを書く

というふうになっています。

そのためミスがある可能性が高いです。
すみません。

新しくプロジェクトを作る

まずは、集計したデータを出力するスプレッドシートを用意します。
このスプレッドシートに動画ののデータが出力されます。

また、このスプレッドシートの「拡張機能」タブからApps Scriptを選択して、
GASのエディタ画面を開いておいてください。

gas_editor.png

YouTube API を入れる

GASの画面で、左側にある「サービス」の隣の+をクリックします。
そしてそこから 「YouTube Data API v3」を選択 します。
バージョンはv3、IDはYouTubeのままで大丈夫です。

サービスのところにYouTubeが追加されていたら成功です。
このYouTubeはオブジェクトになっていて、動画データはこれを使って取ります。

youtube_api.png

詳しくは自分で調べてください(無責任

データを取得する

引数に動画IDを取る、fetchDataという関数を作ります。

動画IDとは、たとえば https://www.youtube.com/watch?v=aaaaaaaaaa というURLの動画あったとしたら、動画IDはaaaaaaaaaaです。
URLのv=以降の部分を抜き出してください。

以下の関数を実装してください。

function fetchData(videoId) {
    // データを取得
    const data = YouTube.Videos.list('statistics', { id: videoId });
    return data.items[0];
}

これでこの関数を実行すると動画の情報が取れます。

2行目のYouTube.Videos.list('statistics', {id: videoId});という部分で、YouTube Data APIを使っています。
第二引数のidプロパティには、動画IDを入れるみたいです。

どんな関数があるのかなどは、以下のリファレンスを参考にしてください。


一応fetchDataをテストしておきます。

function test() {
  console.log(fetchData('4WXs3sKu41I'));
}

以上のようにtest関数を実装して、実行してみてください。
この際権限がリクエストされますが、承認してください。

それっぽいデータがログに流れたら成功です。

listの第一引数

さて、fetchData関数の以下の行↓ですが、

const data = YouTube.Videos.list('snippet,statistics', { id: videoId});

なにやらlist関数の第一引数に'snippet,statistics'を入れています。
これは何かというと、必要なデータのみを取得できるように設定するやつみたいです。

まず、data.items[0]には下のリファレンスにあるデータ構造が入っています。

このデータ構造の中にはsnippet,contentDetails,statusなどのプロパティが入っています。
そして、この中で必要なデータのプロパティのみを第一引数に登録することで、
なんか転送するデータ量が削減できる的な感じですかね...?(わかってない)

例えば、以下のような感じです。

// この場合、
const data = YouTube.Videos.list('snippet', { id: '4WXs3sKu41I' });
// data.items[0]には、
/*
{ id: '4WXs3sKu41I',
  etag: 'O4vzIBxJ9AM91JQlVW1wr63XHvk',
  kind: 'youtube#video',
  snippet: 
   { title: 'Google — Year in Search 2022',
     thumbnails: { 略 },
     channelTitle: 'Google',
     tags:  [ 略 ],
     channelId: 'UCK8sQmJBp8GCxrOtXWBpyEA',
     liveBroadcastContent: 'none',
     defaultAudioLanguage: 'en-US',
     publishedAt: '2022-12-08T10:09:46Z',
     categoryId: '28',
     localized: { 略 },
     description: 略 } }
*/
// みたいにsnippetプロパティが入ってる

////////////////////////////////////////////////

// この場合、
const data = YouTube.Videos.list('statistics', { id: '4WXs3sKu41I' });
// data.items[0]には、
/*
{ kind: 'youtube#video',
  etag: 'RnuQby7YcD8Sc_1kPh7RhlMWlm4',
  id: '4WXs3sKu41I',
  statistics: 
   { viewCount: '296846910',
     favoriteCount: '0',
     commentCount: '4513',
     likeCount: '75733' } }
*/
// のようにstatisticsプロパティが入ってる

また、カンマ(,)で区切ることによって複数指定可能です。

ここに使える文字列はリファレンスにわかりやすく書いてありますが、
一応まとめておきます。

文字列 意味
snippet 動画に関する基本情報
タイトル、説明、カテゴリなど
contentDetails 動画コンテンツに関する情報
動画の長さや、字幕が使用可能かどうかなど
status ステータスに関する情報
動画の公開状況、ライセンスなど
statistics 動画に関する統計情報
再生回数、いいね数、コメント数など
player 動画を埋め込む際の情報
埋め込みコードなど
topicDetails 動画のトピック(カテゴリ?)に関する情報
wikipediaのリンクなど
recordingDetails 撮影された動画の情報、{}が返ってくることも
撮影日時、撮影場所など
fileDetails 動画ファイルに関する情報
動画の所有者のみ取得可能
processingDetails 動画アップロードに関する進捗状況
動画の所有者のみ取得可能
suggestions 動画の品質やメタデータの改善に関する情報
動画の所有者のみ取得可能
liveStreamingDetails ライブに関する情報
ライブの開始時刻や同時視聴者数など
ライブ動画でのみ取得可能
localizations 動画のメタデータの翻訳版
翻訳されたタイトルや説明など?

シートに追加する

動画IDとシートの範囲(range)を入れると、データがシートに入る関数を作ります。

function addDataToRange(videoId, range) {
    const data = fetchData(videoId).statistics; // データを取得
    const dataArray = [
        new Date(), // 現在時刻
        data.viewCount, // 再生回数
        data.likeCount, // 高評価数
        data.commentCount // コメント数
    ];
    range.setValues([dataArray]); // []で囲むことで二次元配列にする
}

この関数は、内部でfetchData()を実行しています。
そして、取得したデータをsetValuesで範囲にセットしています。

一応テストします。

function test() {
    addDataToRange('4WXs3sKu41I', SpreadsheetApp.getActiveSheet().getRange('A3:D3'));
}

この関数を実行すると権限がリクエストされますが、承認してください
シートのA3からD3にそれっぽいデータがセットされたら成功です。

最終行を返す関数を作る

そしたら今度は指定した列の最終行にデータを追加していきます。
...と思ったのですが、それをやる関数が調べても出てこなかったため、とりあえず指定した列の最終行を返す関数を作ります。

// 指定した列の最終行を返す関数
function getLastRow(column, sheet) {
  // getLastRowは空白を含む最終行を返す、シートの長さを調べるのに使ってる
  const range = sheet.getRange(`${column}1:${column}${sheet.getLastRow()}`);

  // getNextDataCellで最終行を取得している
  const result = range.getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
  
  if(range.getValues()[1][0] == '') {
    // 最終行が1だとうまくいかなかったため
    return 1;
  } else {
    return result;
  }
}

まず、range.getNextDataCell(SpreadsheetApp.Direction.DOWN)を使って、
データがある最終行のセルを取得しています。
次に、getRow()を使って、そのセルの行数(=最終行の行数)を取得しています。

以下のサイトを参考にしました。
このサイトの方がわかりやすいので、詳しくはこちらを見てください。


この関数も一応テストします。

まず、スプレッドシートの内容を以下のようにします。

A
1 aaa
2 bbb

そして、test関数を以下のようにします。

function test() {
    console.log(lastRow('A', SpreadsheetApp.getActiveSheet()));
}

そしたらこの関数を実行してください。
2」と出力されたらうまく動いています。

テンプレートを入れる関数を作る

例えば以下のようなシートがあったとします。

A B C D
1
2
3

ここのA1セルにvideoId:triggerという文字を入れます。

A B C D
1 4WXs3sKu41I:1d
2
3

そしたら以下のように表示される機能を、今から作ります。

A B C D
1 4WXs3sKu41I:1d videoId = 4WXs3sKu41I trigger = 1d
2 日時 再生回数 いいね数 コメント数
3

上の例だと、4WXs3sKu41Iという動画IDの動画を、1dつまり1日おきに集計してくれるようになります。
というかこれからします。

1行目にvideoId:triggerという形式に沿った文字を入力すると、上のように文字を挿入する機能を、ここでは(なぜか)テンプレートと呼んでいます。

実装はこんな感じです。

// onEditはスプレッドシートにユーザーが変更を加えた時に実行される
function onEdit(e) {
  if(e.range.getRow() === 1) { // 変更されたセルの列が1行目なら
    const cellValues = e.range.getValue()?.split(':'); // [videoId, trigger]
    if(cellValues.length < 2) return; // 形式に沿っていなければreturn
    
    // 隣に動画IDとtriggerを表示
    const nextRange = e.range.getSheet().getRange(e.range.getRow(), e.range.getColumn() + 1, 1, 2);
    nextRange.setValues([['videoId = ' + cellValues[0], 'trigger = ' + cellValues[1]]]);

    // 下に日時等を表示
    const bottomRange = e.range.getSheet().getRange(e.range.getRow()+1, e.range.getColumn(), 1, 4);
    bottomRange.setValues([['日時', '再生回数', 'いいね数', 'コメント数']]);
  }
}

onEdit関数は少し特殊で、スプレッドシートにユーザーが変更を加えた時に実行されます。
第一引数にはイベントオブジェクトが入ります。...多分()

あとはコメントの通り、隣に動画IDとトリガーを示す文字を入れて、下に日時や再生回数という文字を入れています。
この[日時,再生回数,略]というセルの下に、集計されたデータが入ります。

メイン処理を作る

ということで、ようやく実際に動かす処理を作ります。
...と言いたいところなのですが、実はちょっと違います。
今から作るのは、スプレッドシートから該当するトリガーの動画IDを探して、その通りにデータを取ってきてシートに入れる関数です。

// おかしいな...この変数どの時点で定義してたっけ...
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

// 関数名は何も考えていなかった模様
function action(triggerStr) { // triggerStrには、例えば1dや15mといった文字が入る
  for (const sheet of spreadsheet.getSheets()) {
    // これはA列のRangeオブジェクト
    const rowA = sheet.getRange(1,1,1,sheet.getLastColumn());
    
    for(const [column, str] of rowA.getValues()[0].entries()) {
      if(!str.includes('videoId = ')) continue;
      
      const trigger = rowA.getCell(1, column+2).getValue().replace('trigger = ', '');
      if(trigger !== triggerStr) continue; // トリガーが該当しなかったらさようなら
        
      // 動画のデータをセルに挿入
      const videoId = str.replace('videoId = ', '');
      const range = sheet.getRange(getLastRow(column, sheet)+1, column, 1, 4);
      addDataToRange(videoId, range);
    };
  }
}

この関数ではまず、テンプレートで追加されているはずのvideoId = 何かtrigger = なにかというセルを(なぜか)探しています。

そして、そのセルが見つかったら、そこからどのトリガーで実行するかの文字列を抽出して、引数のtriggerStrと一致するかを調べます。

一致する場合はaddDataToRange関数でシートにデータを追加しています。

トリガーにする関数を作る

上のaction関数は以下のように使います。

function oneMinutes() { // oneMinutes = 1分
    action('1m'); // 1m = 1分
}

上の関数を追加して、別画面からトリガーを「時間ベースのトリガー>1分」に設定すると、videoId:1mとなっているところの動画IDでデータが追加されます。

A B C D
1 4WXs3sKu41I:1m videoId = 4WXs3sKu41I trigger = 1m
2 日時 再生回数 いいね数 コメント数
3 20xx/xx/xx x:xx:xx xxxxxxx xxxxxx xxxxxx
4 (上の1分後) xxxxxxx(多分増えてる) xxxxxx xxxxxx
5 以下略

というわけで、こんな感じの関数を量産します。

function oneMinutes ()  {
  action('1m');
}
 
function fiveMinutes ()  {
  action('5m');
}
 
function fifteenMinutes ()  {
  action('15m');
}
 
function thirtyMinutes ()  {
  action('30m');
}
 
function oneHour ()  {
  action('1h');
}
 
function sixHour ()  {
  action('6h');
}
 
function oneDay ()  {
  action('1d');
}

なんの変哲もない関数たちです。
上のはコピペでいいと思います。手打ちだとかなり面倒だし。

そしてここから、上で追加した関数にすべてトリガーを設定するという苦行が始まります。

トリガーを設定する

ここまで来たら、あとはトリガーを設定するだけで集計が始まります。
といってもここに書くのは面倒なので、以下のページを参考にしてください。

先ほど作った関数を、時間ベースのトリガーで実行するようにしてください。
例えばfiveMinutesなら5分おき、oneDayなら1日おきです。
これが結構面倒ですが、まあ頑張ってください。

これができたら多分集計ができるようになっていると思います。
この記事のコードは十中八九バグってると思うので、もしバグ等を見つけたら教えてくださると助かります。

参考

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?