YouTube Data API v3を利用してチャンネルのプロフィール情報を取得する

独自にYouTubeチャンネルのプロフィール情報ページを作成することを想定し、プロフィール情報ページは下記のデータから構成されるものと定義し、APIでこれらのデータを取得することを考えてみる。

  • チャンネルアイコン画像
  • チャンネルアート画像
  • チャンネル名
  • アップロード動画数
  • 登録者数
  • 動画再生回数
  • チャンネルに関連付けられた国
  • チャンネル作成日

これらのデータは、YouTube Data APIの channels リソースに含まれるプロパティと下記のように対応する。

  • snippet
    • snippet.thumbnails:チャンネルアイコン画像
    • snippet.title:チャンネル名
    • snippet.country:チャンネルに関連付けられた国
    • snippet.publishedAt:チャンネル作成日
  • statistics
    • statistics.videoCount:アップロード動画数
    • statistics.subscriberCount:登録者数
    • statistics.viewCount:動画再生回数
  • brandingSettings

これを踏まえて、APIを利用して上記のプロパティを取得するコードの例が下記。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

$client = new Google_Client();
$client->setDeveloperKey("_API_KEY_");
$youtube = new Google_Service_YouTube($client);

$json = file_get_contents("channelIds.json");
$channelIds = json_decode($json, true);

foreach (array_chunk($channelIds, 50) as $ids)
{
  $params = [
    'id' => implode(",", $ids),
    'maxResults' => 50,
  ];

  $response = $youtube->channels->listChannels('snippet,statistics,brandingSettings', $params);

  foreach ($response['items'] as $i => $item)
  {
    echo "#$i\n";
    echo "id         : {$item['id']}\n";
    echo "title      : {$item['snippet']['title']}\n";
    echo "country    : {$item['snippet']['country']}\n";
    echo "publishedAt: {$item['snippet']['publishedAt']}\n";

    echo "viewCount      : {$item['statistics']['viewCount']}\n";
    echo "subscriberCount: {$item['statistics']['subscriberCount']}\n";
    echo "videoCount     : {$item['statistics']['videoCount']}\n";

    foreach ($item['snippet']['thumbnails'] as $key => $img)
    {
      echo "$key: {$img['url']}\n";
    }

    foreach ($item['brandingSettings']['image'] as $key => $imgUrl)
    {
      if (!is_null($imgUrl))
      {
        echo "$key: $imgUrl\n";
      }
    }
  }
  sleep(1);
}

前述のプロフィール情報に必要なプロパティをリクエストするには、listChannels() メソッドの part 引数に snippet、statistics、brandingSettings を指定すれば良い。

フィルタのパラメータとして、maxResults パラメータには、APIレスポンスに含める結果の件数を指定する。デフォルト値は 5、最大値は 50 となる。上記コードでは最大値 50 を指定している。また、id パラメータにチャンネルIDを指定する。カンマ区切りで複数のチャンネルIDを指定できる。上記のコードでは、maxResults パラメータに合わせて、カンマ区切りで一度に最大50のチャンネルIDを指定するようにしている。

YouTube Data API v3を利用してチャンネルの国情報を取得する

なぜか日本語版の公式ドキュメントには記載されていないが、英語版を確認すると、APIを利用してYouTubeチャンネルに関連付けられた国情報を取得できることが分かる。

channels リソースには brandingSettings プロパティがある。このプロパティ内の brandingSettings.channel.country プロパティにはチャンネルに関連付けられた国名コードが入っている。
※後述するが、snippet.country プロパティを参照しても良い。ただし、この値は brandingSettings.channel.country プロパティにセットした値が入っているだけのもの。

APIリクエストを行って、このプロパティを取得するコードの例が下記。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

$client = new Google_Client();
$client->setDeveloperKey("_API_KEY_");
$youtube = new Google_Service_YouTube($client);

$channelIds = [
  "UC_x5XG1OV2P6uZZ5FSM9Ttw", // Google Developers
  "UCrXUsMBcfTVqwAS7DKg9C0Q", // YouTube Japan
];

$params = [
  'id' => implode(",", $channelIds)
];

$response = $youtube->channels->listChannels('brandingSettings', $params);

foreach ($response['items'] as $i => $item)
{
  echo "#$i\n";
  echo "id     : {$item['id']}\n";
  echo "title  : {$item['brandingSettings']['channel']['title']}\n";
  echo "country: {$item['brandingSettings']['channel']['country']}\n\n";
}

listChannels() メソッドを使ってAPIリクエストを行う。part 引数には brandingSettings を指定し、フィルタのパラメータには、id パラメータに取得したいチャンネルIDを指定する。カンマ区切りで複数のIDを指定することができる。

そして、APIレスポンスとして返ってきたデータの中にある brandingSettings.channel.country プロパティの値を取得する。値は文字列で国名コードを表すものが入っている。

ただし、チャンネルによっては、brandingSettings.channel.country プロパティの値がセットされていないようで、NULL 値の場合がある。


次に、snippet.country プロパティを参照するバージョンのコードが下記。snippet.country プロパティの値には brandingSettings.channel.country プロパティにセットした値と同じものが入る。コードも、プロパティ名を変更するだけで本質的に違いはない。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

$client = new Google_Client();
$client->setDeveloperKey("_API_KEY_");
$youtube = new Google_Service_YouTube($client);

$channelIds = [
  "UC_x5XG1OV2P6uZZ5FSM9Ttw", // Google Developers
  "UCrXUsMBcfTVqwAS7DKg9C0Q", // YouTube Japan
];

$params = [
  'id' => implode(",", $channelIds)
];

$response = $youtube->channels->listChannels('snippet', $params);

foreach ($response['items'] as $i => $item)
{
  echo "#$i\n";
  echo "id     : {$item['id']}\n";
  echo "title  : {$item['snippet']['title']}\n";
  echo "country: {$item['snippet']['country']}\n\n";
}

YouTube Data API v3を利用してチャンネルの画像情報を取得する

APIを利用してYouTubeチャンネルに関連した画像情報を取得することができる。チャンネルに関連する主な画像として下記の2つがある。

  • チャンネルアイコン
  • チャンネルアート

チャンネルアイコンは、チャンネルのプロフィール画像で、縦横のサイズが等しい正方形または円形の画像となる。小、中、大の3種類の画像がある。

チャンネルアートは、チャンネルページ上部の背景/バナー画像として表示されるもので、各デバイス向けアプリケーション/画面解像度に合わせて複数の画像がある。

下記のコードは、これらの画像をAPIを利用してリクエストするもの。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

$client = new Google_Client();
$client->setDeveloperKey("_API_KEY_");
$youtube = new Google_Service_YouTube($client);

$channelId = "UCrXUsMBcfTVqwAS7DKg9C0Q"; // YouTube Japan

$params = [
  'id' => $channelId,
];

$response = $youtube->channels->listChannels('snippet,brandingSettings', $params);

foreach ($response['items'] as $i => $item)
{
  echo "channelId: {$item['id']}\n";

  foreach ($item['snippet']['thumbnails'] as $key => $img)
  {
    echo "$key ({$img['width']}px x {$img['height']}px):\n";
    echo "{$img['url']}\n";
  }

  foreach ($item['brandingSettings']['image'] as $key => $imgUrl)
  {
    if (!is_null($imgUrl))
    {
      // APIと関係ない、単に出力を見やすくするための処理、読み飛ばして良い
      $parts = preg_split('/(?=[A-Z])/', $key, -1, PREG_SPLIT_NO_EMPTY);
      array_pop($parts);
      array_pop($parts);
      $width = preg_replace('/^.+=w([0-9]+).+$/', '$1', $imgUrl);

      echo "$key (".implode(' ', $parts).", width={$width}px):\n";
      echo "$imgUrl\n";
    }
  }
}

チャンネルアイコンの画像情報は、snippet.thumbnails プロパティに格納されている。また、チャンネルアートの画像情報は、brandingSettings.image プロパティに格納されている。

※追記:brandingSettings.image プロパティは2020年9月9日に deprecated となっている。
https://developers.google.com/youtube/v3/revision_history#september-9,-2020

APIリクエストは listChannels() メソッドを使って行い、part引数に上記 snippet と brandingSettings の2つを指定する。フィルタのパラメータ指定では、id パラメータにチャンネルIDを指定すると、そのチャンネルの画像情報を取得できる。カンマ区切りでチャンネルIDを複数指定できる。

snippet.thumbnails プロパティには、default、medium、high の3つキーがあり、それぞれに url、width、height の値が保持されている。

  • default
    • url: 小サイズ(デフォルト)の画像URL, width: 88, height: 88
  • medium
    • url: 中サイズの画像URL, width: 240, height: 240
  • high
    • url: 大サイズの画像URL, width: 800, height: 800

brandingSettings.image プロパティに格納されているデータには、各デバイス向けアプリケーション/画面解像度の画像URLが保持されている。代表的なものを挙げると下記となる。

  • bannerImageUrl
    • ウェブサイトのチャンネルページに表示される画像URL(サイズ 1060px x 175px)
  • bannerMobileImageUrl
    • モバイルアプリケーションのチャンネルページに表示される画像URL(サイズ 640px x 175px)
  • bannerTabletImageUrl
    • タブレットアプリケーションのチャンネルページに表示される画像URL(サイズ 1707px x 283px)
  • bannerTvImageUrl
    • テレビアプリケーションのチャンネルページに表示される画像URL(サイズ 2120px x 1192px)

また、画面解像度別には、モバイルアプリケーション向けを例にすると、bannerMobileMediumHdImageUrl、bannerMobileHdImageUrl のように区分されて画像URLが保持されている。

YouTube Data API v3を利用して最も人気のある動画を取得する

APIを使ってYouTubeで最も人気のある動画を取得することができる。

地域や動画カテゴリを指定して取得することが可能で、下記は日本の音楽カテゴリで最も人気のある動画を取得する例。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

$client = new Google_Client();
$client->setDeveloperKey("_API_KEY_");
$youtube = new Google_Service_YouTube($client);

$params = [
  'chart' => 'mostPopular',
  'regionCode' => 'JP',
  'videoCategoryId' => '10', // 音楽
  'maxResults' => 50,
];

do {
  $response = $youtube->videos->listVideos('snippet', $params);

  foreach ($response['items'] as $i => $item)
  {
    echo "#$i\n";
    echo "id         : {$item['id']}\n";
    echo "channelTitle: {$item['snippet']['channelTitle']}\n";
    echo "title      : {$item['snippet']['title']}\n";
  }

  echo "\n";
  echo "# nextPageToken: {$response['nextPageToken']}\n";
  echo "# prevPageToken: {$response['prevPageToken']}\n";
  echo "# totalResults: {$response['pageInfo']['totalResults']}\n";
  echo "# resultsPerPage: {$response['pageInfo']['resultsPerPage']}\n\n";

  $pageToken = $response['nextPageToken'];
  $params['pageToken'] = $pageToken;
  sleep(1);

} while (!empty($pageToken));

listVideos() を呼ぶときのフィルタのパラメータとして chart パラメータに mostPopular を指定すると最も人気のある動画が返ってくる。上記のコードでは、その他に regionCode に JP、videoCategoryId に 10(音楽)を指定して、リクエスト対象が日本の音楽カテゴリになるようにしている。maxResults は、APIレスポンスに含める結果の動画件数を指定する。

動画タイトルなどの基本情報は listVideos() の part 引数に snippet を指定すると得られる。APIレスポンスとして返ってくる snippet プロパティには下記の動画・基本情報が格納されている。

  • snippet.publishedAt:動画のアップロード日時
  • snippet.channelId:動画のチャンネルID
  • snippet.title:動画のタイトル
  • snippet.description:動画の説明
  • snippet.thumbnails:動画のサムネイル画像情報
  • snippet.channelTitle:動画のチャンネル名
  • snippet.tags:動画に付けられたタグ
    • 配列データ(動画にタグが付けられていない場合は NULL)
  • snippet.categoryId:動画のカテゴリID

上記コードを実行すると、日本の音楽カテゴリで最も人気のある動画が人気上位から順に出力される。

なお、2ページ目以降の結果を取得するには、フィルタに pageToken パラメータを追加し、その値に nextPageToken の値(上記の実行結果の例では CDIQAA)を指定すると得られる。nextPageToken は、次のページ結果がある場合、APIレスポンスに含まれている。

YouTube Data API v3を利用したプレイリストのエクスポート/インポート機能のクォータ使用量

Googleは、APIによる各リクエストにコストを割り当ててクォータの使用量を計算している。コストは操作の種類ごとに下記のように設定されている。

※追記:Googleがクォータ・コストの割り当て方法の簡略化を行い、part パラメータに伴う追加コストが撤廃された。
https://developers.google.com/youtube/v3/revision_history#july-29,-2020

簡略化後は、part引数によらず、単純に各メソッドに割り当てられたコストがかかるだけになる。


※注意:これ以降の内容は、以前のpartパラメータにかかる追加コストを考慮に入れて、コストを計算したもの
  • リソースのIDを取得するだけの単純な読み取り操作のコストは 1 ユニット
  • リソースのパーツ(snippet、status など、ただし id は除く)を取得する読み取り操作のコストはパーツ毎に 2 ユニット
  • リソースの書き込み操作のコストは 50 ユニット
  • 動画のアップロードのコストは 1600 ユニット

ここで、下記のプレイリストのエクスポート機能を想定したAPIリクエストのコストを計算してみる。

$youtube->playlistItems->listPlaylistItems('snippet', $params)

※フィルタのパラメータ $params の maxResults の値に 50 を設定、1回のリクエストで最大50件のプレイリストアイテム(プレイリスト内の動画情報)を取得

読み取り操作には、1 ユニットのコストがかかる。引数 part に snippet だけ指定しているので、読み取るパーツ数は1となる。パーツの読み取り操作は、パーツ毎に 2 ユニットなので、読み取り操作全体のコストは、

1 + 2 * 1
= 3

となり、上記APIリクエストは1回で 3 ユニットのコストがかかることになる。

1日あたりのクォータが 10,000、含まれる動画が50個以内のプレイリストを対象にすると、1日あたり 3,333 (=10,000/3) のプレイリスト情報を取得できる計算になる。


次に、下記のプレイリストのインポート機能を想定したAPIリクエストのコストを計算してみる。

プレイリストのインポートでは、下記2つのAPIリクエストが行われる。

  1. $youtube->playlists->insert('snippet,status', $youTubePlaylist, [])
  2. $youtube->playlistItems->insert('snippet', $playlistItem, [])

まず1番目のリクエストのコストから。

これはプレイリストを作成するためのもので、書き込み操作に該当する。よって、playlists->insert() は、書き込み操作として 50 ユニットのコストがかかる。

また、playlists->insert() の書き込み操作は、書き込みだけでなく、APIレスポンスとしてリソースを返す処理も実行される。つまり、リソースを返す処理に伴い読み取り操作のコストもかかることになる。読み取り操作には、1 ユニットのコストがかかる。引数 part に snippet,status を指定しているので、読み取るパーツ数は2となる。よって、playlists->insert() は、読み取り操作として、

1 + 2 * 2
= 5

のコストがかかる。

よって、playlists->insert() にかかる書き込みと読み取り操作のコストを合わせると、

50 + 5
= 55

これが1番目のAPIリクエストのコストとなる。

続いて、2番目のリクエストのコストを計算してみる。

これは作成したプレイリストにプレイリストアイテムを追加していく操作で、書き込み操作として、playlistItems->insert() は 50 ユニットのコストがかかる。これも同様にAPIレスポンスを返す処理に伴い読み取り操作が行われる。引数 part に snippet を指定しているので、読み取るパーツ数は1となることから、playlistItems->insert() は、読み取り操作として、

1 + 2 * 1
= 3

のコストがかかる。playlistItems->insert() にかかる書き込みと読み取り操作のコストを合わせると、

50 + 3
= 53

のコストがかかることになる。これはプレイリストアイテム1個あたりにかかるコストとなる。

そこで、プレイリストに15個のプレイリストアイテムを追加する場合を想定すると、

53 * 15
= 795

これが2番目のAPIリクエストのコストとなる。

プレイリストの作成、プレイリストアイテムの追加(15個)のコストを合わせると

55 + 795
= 850

これが1番目と2番目のAPIリクエストを合わせた総コストとなる。

1日あたりのクォータが 10,000、含まれる動画が10個のプレイリストをインポート対象とすると、1日あたり 11 (=10,000/850) のプレイリストをインポートできる計算になる。


エクスポートとインポートのコストを比較してみると、1日あたりの可能なエクスポート数は 3,333、インポート数は 11 となることから、書き込み操作のコストはかなり高いことがわかる。

クォータの使用量は、Google Cloud Platformの "IAM と管理" → "割り当て" から確認できる。

最後に、クォータ制限に引っかかると下記のようなエラーが返ってくる。これはプレイリスト作成で playlists->insert() による書き込み操作をリクエストしたときのもの。
※クォータ上限は 10,000 のはずなのだが、7700 ぐらいに達したぐらいで書き込み操作を伴うAPIリクエストが一切行えなくなった

Fatal error: Uncaught Google_Service_Exception: {
  "error": {
    "code": 500,
    "message": "Internal error encountered.",
    "errors": [
      {
        "domain": "youtube.api.v3.PlaylistInsertResponse.Error",
        "reason": "RATE_LIMIT_EXCEEDED"
      }
    ],
    "status": "INTERNAL"
  }
}

YouTube Data API v3を利用してプレイリストのインポート機能を実装する

YouTubeにはプレイリスト(再生リスト)のインポート機能がないので、YouTube Data API v3を利用して簡単なものを自作した。

プレイリストのインポート機能を実装するには、APIで挿入 (insert) 操作をリクエストする必要がある。

このAPIリクエストを実行するには、OAuth 2.0認証を行い、アプリケーション(今回の場合は自作インポート機能プログラム)によるユーザーアカウントへのアクセスが許可されなければならない。

また、このアクセス許可は、ユーザーのリソース操作全てを許可するわけではなく、スコープという仕組みを導入して許可する範囲を限定している。

プレイリストのインポート機能に必要な挿入 (insert) 操作を行う場合は、スコープ https://www.googleapis.com/auth/youtube が許可される必要がある。

OAuth 2.0認証のやり取りを行い、承認されたクライアントオブジェクトをセットアップするためのコードが下記になる。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

require_once 'OAuthClient.php';

$CLIENT_ID = '_CLIENT_ID_';
$CLIENT_SECRET = '_CLIENT_SECRET_';
$REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob';
$TOKEN_FILENAME = "token.json";
$CODE = '_CODE_';

try
{
  $gclient = new Google_Client();
  $oauth = new OAuthClient($gclient, $CLIENT_ID, $CLIENT_SECRET, $REDIRECT_URI, $TOKEN_FILENAME, $CODE);
  $oauth->setScopes('https://www.googleapis.com/auth/youtube');
  $client = $oauth->getAuthorizedClient();
}
catch(Exception $e)
{
  switch($e->getCode())
  {
    case OAuthClient::EMPTY_CODE:
    case OAuthClient::INVALID_CODE:
      echo $e->getMessage() . "\n";
      echo $oauth->getAuthUrl() . "\n";
      exit;
    case OAuthClient::INVALID_TOKEN_FILE:
      echo $e->getMessage() . "\n";
      exit;
    default:
      throw $e;
  }
}

$youtube = new Google_Service_YouTube($client);

OAuth 2.0認証の一連の処理は OAuthClient クラスにまとめてある。このクラスの getAuthorizedClient() を呼べば、承認されたクライアントオブジェクトが返ってくる。詳細は前の記事で説明したので、それを参照のこと。
OAuth 2.0 認証で YouTube Data API v3 を利用する


ここからは本題のインポート機能について説明する。

インポート機能は次の3つの処理から成る。

  1. インポートファイルの読み込み
  2. プレイリスト作成
  3. プレイリストに動画を追加

まず、下記はインポートファイルの読み込みとプレイリストの作成を行う部分。


$importFileName = "import.tsv";

$videoIds = extractVideoIds($importFileName);

$playlistName = preg_replace('/^(.+)\.tsv$/', '$1', $importFileName);
echo "creating a playlist \"$playlistName\"...\n";

$playlistSnippet = new Google_Service_YouTube_PlaylistSnippet();
$playlistSnippet->setTitle($playlistName);
$playlistSnippet->setDescription("created with the YouTube Data API v3 [" . date('Y-m-d H:i:s') . "]");

$playlistStatus = new Google_Service_YouTube_PlaylistStatus();
$playlistStatus->setPrivacyStatus('private');

$youTubePlaylist = new Google_Service_YouTube_Playlist();
$youTubePlaylist->setSnippet($playlistSnippet);
$youTubePlaylist->setStatus($playlistStatus);

$playlistResponse = $youtube->playlists->insert('snippet,status', $youTubePlaylist, []);
$playlistId = $playlistResponse['id'];

echo "done.\n\n";


function extractVideoIds($filename)
{
  $videoIds = [];

  $file = file_get_contents($filename);

  if (!$file)
    exit("Error: Can't open file: $filename\n");

  echo "reading \"$filename\"...\n";

  foreach (str_getcsv($file, "\n") as $row)
  {
    if (empty($row))
      continue;

    $cols = str_getcsv($row, "\t");

    $videoId = preg_replace('|^https://www.youtube.com/watch\?v=(.+)$|', '$1', $cols[0]);
    $videoIds[] = $videoId;
  }

  echo "done.\n\n";

  return $videoIds;
}

インポートできるファイルは、下記のようなYouTubeの動画URLとタイトルが TSV(タブ区切り)形式で保存されたファイルを想定している。


YouTube動画URL1	動画タイトル1
YouTube動画URL2	動画タイトル2
YouTube動画URL3	動画タイトル3
...

このファイルを読み込んで動画IDだけ抽出する。

続いてプレイリストの作成では、まず、snippet プロパティと status プロパティの値を保持する PlaylistSnippet と PlaylistStatus オブジェクトをそれぞれ作成する。

PlaylistSnippet オブジェクトの作成では、snippet プロパティ内の snippet.title プロパティと snippet.description(プレイリストの説明)の値を setTitle() と setDescription() でそれぞれセットする。上記のコードの例では、title にはインポートファイルのファイル名、description には文字列 created with the YouTube Data API v3 とそれにタイムスタンプを付加したものをセットしている。

PlaylistStatus オブジェクトの作成では、status プロパティ内の status.privacyStatus プロパティの値をセットする。有効な値は、public、private、unlisted の3つで、デフォルトは public となる。上記のコードの例では、private をセットし、作成するプレイリストは非公開のものとして設定している。

PlaylistSnippet と PlaylistStatus オブジェクトを作成したら、それらを Playlist オブジェクト $youTubePlaylist に setSnippet() と setStatus() でそれぞれセットする。そして、$youtube->playlists->insert('snippet,status', $youTubePlaylist, []) でAPIリクエストを行い、プレイリストの作成が完了する。insert() の引数 'snippet,status' は、書き込み操作対象となるプロパティを特定するものである。

APIレスポンスに作成されたプレイリストIDが含まれているのでそれを取得する。

次に、このIDを使って、プレイリストに動画をインポートしてプレイリストを完成させていく。


foreach ($videoIds as $i => $videoId)
{
  echo "$i: videoId: $videoId ";

  $resourceId = new Google_Service_YouTube_ResourceId();
  $resourceId->setVideoId($videoId);
  $resourceId->setKind('youtube#video');

  $playlistItemSnippet = new Google_Service_YouTube_PlaylistItemSnippet();
  $playlistItemSnippet->setPlaylistId($playlistId);
  $playlistItemSnippet->setResourceId($resourceId);

  $playlistItem = new Google_Service_YouTube_PlaylistItem();
  $playlistItem->setSnippet($playlistItemSnippet);
  $playlistItemResponse = $youtube->playlistItems->insert('snippet', $playlistItem, []);

  echo "inserted.\n";
}

echo "\ndone.\n";

プレイリストはプレイリストアイテムから構成され、インポートする動画一つ一つに対応するプレイリストアイテムを作成して、それらをプレイリストに挿入していくのが主な処理となる。

プレイリストアイテムの作成手順は前述のプレイリスト作成と同様で、プレイリストアイテムの snippet プロパティの値を保持する PlaylistItemSnippet オブジェクトを作成し、それを PlaylistItem オブジェクトの setSnippet() に渡してセットし、PlaylistItem オブジェクト $playlistItem をセットアップする。そして、$youtube->playlistItems->insert('snippet', $playlistItem, []) でAPIリクエストを行い、プレイリストにプレイリストアイテムを挿入し、動画一つのインポートが完了する。

違う点は、プレイリストアイテムとプレイリストの紐付け、また、プレイリストアイテムと動画IDを紐付けるために ResourceId オブジェクトを作成する必要があるというところ。

プレイリストアイテムとプレイリストの紐付けは、PlaylistItemSnippet オブジェクトの setPlaylistId() にプレイリストIDを渡して行う。

プレイリストアイテムと動画IDの紐付けは、ResourceId オブジェクトを作成し、setVideoId() で動画IDをセット、setKind() でリソースタイプをセットする。リソースタイプは動画の場合、youtube#video となる。そして、ResourceId オブジェクトを PlaylistItemSnippet オブジェクトの setResourceId() に渡し、プレイリストアイテムと動画IDが紐付けられる。

OAuth 2.0 認証で YouTube Data API v3 を利用する

OAuth 2.0認証を行ってYouTube Data API v3を利用するための手順は、

  • OAuth同意画面の設定
  • OAuthクライアントIDの作成
  • Googleの承認サーバーURLにアクセスして、アプリケーションにAPIの実行を許可&承認コードを取得
  • 承認コードを使ってアクセストークンを取得
  • 期限が切れた場合、更新トークンを使って新しいアクセストークンを取得

となる。

まず、Google Cloud Platform上でOAuth同意画面を設定する。

Google Cloud Platformの "APIとサービス" → "OAuth同意画面" ページに移動して下記の設定を行う。
※この設定は、プレイリストのインポート機能の開発テスト用にAPIを利用することを前提にしている

  1. ユーザーの種類を選択する。
    G Suiteを利用していれば "内部" の選択が可能。利用していなければ "外部" を選択することになる。
    ※G Suiteのアカウントから作成されたものであってもYouTubeブランドアカウントはG Suiteの組織内ユーザーとみなされない。よって、YouTubeブランドアカウントのデータをAPIから操作したい場合は "外部" を選択する必要がある。
  2. アプリケーション名を入力する
  3. スコープの追加
    ./auth/youtube を追加する。(APIでプレイリストの作成をリクエストするために必要)
  4. 保存

次に、Google Cloud Platform上でOAuthクライアントIDを作成する。

Google Cloud Platformの "APIとサービス" → "OAuth同意画面" ページに移動する。

  1. "+認証情報を作成" をクリック
  2. "OAuth クライアント ID" を選択
  3. アプリケーションの種類 "デスクトップアプリ" を選択
  4. 作成

以上で、Google Cloud Platform上で行う設定は完了。


続いて、承認コードを取得する。承認コードを取得するには、Googleの承認サーバーURLにアクセスする必要がある。

下記は、Googleの承認サーバーURLを表示するためのコード。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

$CLIENT_ID = '_CLIENT_ID_';
$CLIENT_SECRET = '_CLIENT_SECRET_';
$REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob';

$client = new Google_Client();
$client->setClientId($CLIENT_ID);
$client->setClientSecret($CLIENT_SECRET);
$client->setRedirectUri($REDIRECT_URI);
$client->setAccessType('offline');
$client->setScopes('https://www.googleapis.com/auth/youtube');

echo $client->createAuthUrl() . "\n";

上記コードで行っているのは、クライアントライブラリを使い、Googleの承認サーバーURLに含めるリクエストパラメータを設定し、最後に createAuthUrl() でそのURLを表示している。

setClientId() と setClientSecret() には、Google Cloud Platform上で作成したOAuthクライアントIDとクライアントシークレットをそれぞれセットする。

setRedirectUri() には、Googleの承認サーバーが承認コードを返す先を指定する。urn:ietf:wg:oauth:2.0:oob を指定すると、コードはブラウザのタイトルバーに返される。
※実際はGoogleの承認サーバーのページ上にも表示される(後述)

setAccessType() で、リクエストパラメータ access_type の値をセットする。offline は、Googleの承認サーバーがアクセストークンを返す際に一緒に更新トークンも返すことを指定するもの(後述)。

setScopes() で、リクエストパラメータ scope の値をセットする。https://www.googleapis.com/auth/youtube は、Google Cloud Platform上のOAuth同意画面の設定で追加したスコープ ./auth/youtube に該当する。

上記コードを実行すると下記のようなURLが表示される。

https://accounts.google.com/o/oauth2/auth?response_type=code&access_type=offline&client_id=xxx&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&state&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fyoutube&approval_prompt=auto

このURLにWebブラウザでアクセスし、承認コードを取得する。

  1. アクセス権を付与するアカウントを選択
  2. "許可" をクリック
    ※「このアプリは確認されていません」と表示される場合(OAuth同意画面の設定でユーザーの種類に "外部" を選択した場合)
    • 左下の "詳細を表示" をクリック
    • "<OAuth同意画面の設定で入力したアプリケーション名>(安全ではないページ)に移動" をクリック
    • "許可" をクリック
  3. 「このコードをコピーし、アプリケーションに切り替えて貼り付けてください。」とページ上に表示されている承認コードをコピー

続いて、取得した承認コードを使ってアクセストークンを取得する。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

$CLIENT_ID = '_CLIENT_ID_';
$CLIENT_SECRET = '_CLIENT_SECRET_';
$REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob';
$CODE = '_CODE_'; // ここに承認コードを貼り付ける
$TOKEN_FILENAME = "token.json";

$client = new Google_Client();
$client->setClientId($CLIENT_ID);
$client->setClientSecret($CLIENT_SECRET);
$client->setRedirectUri($REDIRECT_URI);
$client->setAccessType('offline');
$client->setScopes('https://www.googleapis.com/auth/youtube');

$tokenArray = $client->fetchAccessTokenWithAuthCode($CODE);
$tokenJson = json_encode($tokenArray);
file_put_contents($TOKEN_FILENAME, $tokenJson);

$youtube = new Google_Service_YouTube($client);
// 以降、$youtubeを使ってAPIリクエストを行える

fetchAccessTokenWithAuthCode($CODE) で承認コードを使ってアクセストークンを取得する。このメソッドは短期間有効なアクセストークンと更新トークンを含む配列を返す。
※承認コードを取得する際、リクエストパラメータ access_type に offline を指定したので更新トークンも取得されることに留意

このトークンはAPIリクエストを行う際に毎回必要になるので、今後の開発テスト実行時のためにJSON形式でファイルに保存しておく。
※トークンを保存しておかないと、OAuth 2.0の同意フローを再度行ってアクセストークンを取得しなければならない


今後の実行時には、ファイルに保存したトークンのリストア、また、アクセストークンは定期的に期限切れになるので、更新トークンを使った新しいアクセストークンの取得が必要になる。

下記がそれを行うコードになる。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

$CLIENT_ID = '_CLIENT_ID_';
$CLIENT_SECRET = '_CLIENT_SECRET_';
$REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob';
$CODE = '_CODE_';
$TOKEN_FILENAME = "token.json";

$client = new Google_Client();
$client->setClientId($CLIENT_ID);
$client->setClientSecret($CLIENT_SECRET);
$client->setRedirectUri($REDIRECT_URI);
$client->setAccessType('offline');
$client->setScopes('https://www.googleapis.com/auth/youtube');

$tokenJson = file_get_contents($TOKEN_FILENAME);

$client->setAccessToken($tokenJson);

if ($client->isAccessTokenExpired())
{
  $newTokenArray = $client->fetchAccessTokenWithRefreshToken();
  $newTokenJson = json_encode($newTokenArray);
  file_put_contents($TOKEN_FILENAME, $newTokenJson);
}

$youtube = new Google_Service_YouTube($client);
// 以降、$youtubeを使ってAPIリクエストを行える

ファイルから読み込んだJSON形式のトークン情報は、setAccessToken() を使ってクライアント上にリストアできる。

アクセストークンの期限切れは isAccessTokenExpired() で確認できる。期限切れの場合は、fetchAccessTokenWithRefreshToken() を使う。このメソッドにより、更新トークンを使って新しいアクセストークンが取得することができる。

トークンが更新された場合、また次回実行時のためにJSON形式でファイルに保存する。


最後に、上記3つのコードを OAuthClient クラスとして書き直したものが下記。今後、実際に利用していくのはこのコードになる。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

$CLIENT_ID = '_CLIENT_ID_';
$CLIENT_SECRET = '_CLIENT_SECRET_';
$REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob';
$TOKEN_FILENAME = "token.json";
$CODE = '_CODE_';


class OAuthClient
{
  const EMPTY_CODE = 1;
  const INVALID_CODE = 2;
  const INVALID_TOKEN_FILE = 3;

  private $client;
  private $code;
  private $tokenFileName;

  public function __construct(Google_Client $client, $clientId, $clientSecret, $redirectUri, $tokenFileName = "token.json", $code = "")
  {
    $this->client = $client;
    $this->client->setClientId($clientId);
    $this->client->setClientSecret($clientSecret);
    $this->client->setRedirectUri($redirectUri);
    $this->client->setAccessType('offline');

    $this->tokenFileName = $tokenFileName;
    $this->code = $code;
  }

  public function setScopes($scope_or_scopes)
  {
    $this->client->setScopes($scope_or_scopes);
  }

  public function getAuthUrl()
  {
    return $this->client->createAuthUrl();
  }

  public function getAuthorizedClient()
  {
    if (empty($this->code))
    {
      throw new Exception("Get a code. (Remove the \"{$this->tokenFileName}\" file if it exists.)", self::EMPTY_CODE);
    }

    if (!file_exists($this->tokenFileName))
    {
      $tokenArray = $this->client->fetchAccessTokenWithAuthCode($this->code);

      if (!isset($tokenArray['access_token']))
      {
        throw new Exception("Invalid code. Get a new code.", self::INVALID_CODE);
      }

      $this->storeAccessToken($tokenArray);
    }
    else
    {
      $this->restoreAccessToken($this->tokenFileName);
    }

    return $this->client;
  }

  private function restoreAccessToken()
  {
    $tokenJson = file_get_contents($this->tokenFileName);
    $tokenArray = json_decode($tokenJson, true);

    if (!isset($tokenArray['access_token']))
    {
      throw new Exception( "Invalid file: \"{$this->tokenFileName}\". Remove the file, then execute the program again.", self::INVALID_TOKEN_FILE);
    }

    $this->client->setAccessToken($tokenJson);

    if ($this->client->isAccessTokenExpired())
    {
      $newTokenArray = $this->client->fetchAccessTokenWithRefreshToken();
      $this->storeAccessToken($newTokenArray);

      fwrite(STDERR, "# " . __METHOD__ . ": Token updated.\n");
    }
  }

  private function storeAccessToken(array $token)
  {
    $tokenJson = json_encode($token);
    file_put_contents($this->tokenFileName, $tokenJson);

    fwrite(STDERR, "# " . __METHOD__ . ": Token stored.\n");
  }
}


try
{
  $gclient = new Google_Client();
  $oauth = new OAuthClient($gclient, $CLIENT_ID, $CLIENT_SECRET, $REDIRECT_URI, $TOKEN_FILENAME, $CODE);
  $oauth->setScopes('https://www.googleapis.com/auth/youtube');
  $client = $oauth->getAuthorizedClient();
}
catch(Exception $e)
{
  switch($e->getCode())
  {
    case OAuthClient::EMPTY_CODE:
    case OAuthClient::INVALID_CODE:
      echo $e->getMessage() . "\n";
      echo $oauth->getAuthUrl() . "\n";
      exit;
    case OAuthClient::INVALID_TOKEN_FILE:
      echo $e->getMessage() . "\n";
      exit;
    default:
      throw $e;
  }
}

$youtube = new Google_Service_YouTube($client);
// 以降、$youtubeを使ってAPIリクエストを行える

YouTube Data API v3を利用してプレイリストをエクスポートする

YouTubeには、プレイリスト(再生リスト)をエクスポートする機能がないので、YouTube Data API v3を利用して簡単なエクスポート機能を自作した。

この機能を実装するには、

  1. APIリクエストを行うオブジェクトを作成
  2. プレイリストIDを指定して動画情報を取得

を行えば良い。

まず、下記は Google_Service_YouTube オブジェクトを作成する部分のコード。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

$client = new Google_Client();
$client->setDeveloperKey("_YOUR_API_KEY_");
$youtube = new Google_Service_YouTube($client);

※APIキーは、Google Cloud Platform の "API とサービス" → "認証情報" ページに移動して、"認証情報を作成" ボタンをクリックして、プルダウンメニューから "API キー" を選択すれば作成できる。


次に、プレイリストIDを指定して動画情報を取得していく。


// https://www.youtube.com/playlist?list=PLpjK416fmKwQKmatriVu3rdwv7g4ZJSfD
$playlistId = "PLpjK416fmKwQKmatriVu3rdwv7g4ZJSfD";

$filename = "export.tsv";
$fh = fopen($filename, "w");
if (!$fh)
  exit("Error: Can't open file: $filename\n");

$pageToken = "";
$cnts = 0;

$params = [
  'playlistId' => $playlistId,
  'maxResults' => 50
];

do {
  $response = $youtube->playlistItems->listPlaylistItems('snippet', $params);

  foreach ($response['items'] as $item)
  {
    $snippet = $item['snippet'];

    $url = "https://www.youtube.com/watch?v={$snippet['resourceId']['videoId']}";
    $title = $snippet['title'];

    $line = "$url\t$title\n";

    $cnts++;
    echo "$cnts:$line";

    fwrite($fh, $line);
  }

  echo "\n";
  echo "# nextPageToken: {$response['nextPageToken']}\n";
  echo "# prevPageToken: {$response['prevPageToken']}\n";
  echo "# totalResults: {$response['pageInfo']['totalResults']}\n";
  echo "# resultsPerPage: {$response['pageInfo']['resultsPerPage']}\n";
  echo "\n";

  $pageToken = $response['nextPageToken'];
  $params['pageToken'] = $pageToken;

  sleep(1);
} while (!empty($pageToken));

fclose($fh);

echo "done.\n";

ここでは、まずプレイリストIDが必要になるので事前に調べておく。プレイリストIDは、"再生リストの全体を見る" ページのURLから調べられる。

"再生リストの全体を見る"ページのURLの例
https://www.youtube.com/playlist?list=PLpjK416fmKwQKmatriVu3rdwv7g4ZJSfD

上記URLの list=に指定されているパラメータ PLpjK416fmKwQKmatriVu3rdwv7g4ZJSfD がプレイリストIDに該当する。

プレイリストIDが分かったら、$youtube->playlistItems->listPlaylistItems() を使ってAPIリクエストを行う。

このメソッドにはAPIレスポンスに含めるリソースのプロパティとフィルタのパラメータを指定する。リソースのプロパティには snippet を指定する。snippet プロパティを指定すると、タイトルなどのアイテム(今回の場合は動画)の基本情報をレスポンスとして返してくれる。フィルタのパラメータには、下記のようにプレイリストIDとAPIレスポンスに含めるアイテム件数を指定する。


$params = [
  'playlistId' => $uploadsListId,
  'maxResults' => 50
];

上記の設定でリクエストを送るとリクエスト毎に最大50件のアイテムが返ってくる。各アイテムは snippet プロパティを持っているので、このプロパティを参照してエクスポートに必要な動画IDと動画タイトルの値を取得してファイルに書き出していく。

また、コードの下記の箇所については、


  $pageToken = $response['nextPageToken'];
  $params['pageToken'] = $pageToken;

2ページ目以降のAPIレスポンス取得には、pageToken を指定する必要があり、上記コードはその値をセットしている箇所となる。つまり、2ページ目以降は、APIリクエストのフィルタに下記のパラメータを設定してリクエストを行っている。


$params = [
  'playlistId' => $uploadsListId,
  'maxResults' => 50,
  'pageToken' => $pageToken
];

コードを実行すると、エクスポートファイル export.tsv が作成され、タブ区切りで下記のように出力される。


YouTube動画URL1	動画タイトル1
YouTube動画URL2	動画タイトル2
YouTube動画URL3	動画タイトル3
...

YouTube Data API v3が返す snippet.publishedAt の値を日本時間に変換する

YouTube Data APIがレスポンスとして返すリソース情報の snippet.publishedAt プロパティには ISO 8601 (YYYY-MM-DDThh:mm:ss.sZ) 形式で日時を表す文字列が入っている。

snippet.publishedAt に入っている文字列の例
2020-07-01T00:57:09Z

これを日本時間に変換するには DateTime オブジェクトにタイムゾーン "Asia/Tokyo" を設定すれば良い。


//...省略...

  $response = $youtube->playlistItems->listPlaylistItems('snippet', $params);

  foreach ($response['items'] as $item)
  {
    $snippet = $item['snippet'];

    echo "publishedAt: {$snippet['publishedAt']}\n";
    /* 出力
    publishedAt: 2020-07-01T00:57:09Z
    */

    $dt = new DateTime($snippet['publishedAt']);
    $dt->setTimezone(new DateTimeZone('Asia/Tokyo'));
    echo $dt->format("Y-m-d H:i:s e") . "\n";
    /* 出力
    2020-07-01 09:57:09 Asia/Tokyo
    */
  }

//...省略...

YouTube Data API v3でチャンネルのユーザー名からチャンネルIDを取得する

下記はYouTube Data APIを利用したチャンネルのユーザー名からチャンネルIDを取得するコードの例。

下記の例のようにリソースのプロパティに id、フィルタに forUsername パラメータを指定して $youtube->channels->listChannels() でAPIリクエストを行えばチャンネルIDを取得できる。

(ユーザー名のURLの例)
https://www.youtube.com/user/YouTubeJapan
(IDに基づくURLの例)
https://www.youtube.com/channel/UCrXUsMBcfTVqwAS7DKg9C0Q

require_once '/path/to/google-api-php-client/vendor/autoload.php';

$client = new Google_Client();
$client->setDeveloperKey("_YOUR_API_KEY_");
$youtube = new Google_Service_YouTube($client);

$user = "YouTubeJapan"; // https://www.youtube.com/user/YouTubeJapan
$response = $youtube->channels->listChannels('id', ['forUsername' => $user]);

$channelId = "";
if (!empty($response['items']))
{
  $channelId = $response['items'][0]['id'];
  var_dump($channelId);
  /* var_dump 出力
  string(24) "UCrXUsMBcfTVqwAS7DKg9C0Q"
  */
}
else
{
  exit("Error: Failed to get channel ID.\n");
}

なお、APIを利用しなくても、Google検索でチャンネル名を検索して、検索結果のリンクからチャンネルのページに行けば、チャンネルIDに基づくURLに飛ばしてくれることが多い。

ただ、チャンネルIDに基づくURLに辿り着けない場合もあるので、APIを利用したチャンネルIDの取得方法を知っておくと役立つ。

YouTube Data API v3を利用して、アップロード動画一覧を取得する

YouTube Data APIを使ってチャンネルにアップロードされた動画一覧を取得するには、

  1. APIリクエストを行うオブジェクトを作成
  2. 対象チャンネルの uploads プレイリストIDを取得
  3. uploads プレイリストIDを指定してリスト内の動画情報を取得

を行う必要がある。

APIリクエストを行うオブジェクトの作成は次のように行う。


require_once '/path/to/google-api-php-client/vendor/autoload.php';

// 1. APIリクエストを行うオブジェクトを作成

$client = new Google_Client();
$client->setDeveloperKey("_YOUR_API_KEY_");
$youtube = new Google_Service_YouTube($client);

Google_Client オブジェクトを作成して、APIキーをセットする。メソッド名が setDeveloperKey となっていて紛らわしいが、普通にAPIキーをセットすれば良い。

APIキーは、Google Cloud Platform の "API とサービス" → "認証情報" ページに移動して、"認証情報を作成" ボタンをクリックして、プルダウンメニューから "API キー" を選択すれば作成できる。

作成した Google_Client オブジェクトを使って、Google_Service_YouTube オブジェクトを作成する。Google_Service_YouTube オブジェクトを利用すると、YouTubeサービスが提供する機能をAPI経由で操作できるようになる。


次に、対象チャンネルの uploads プレイリストIDを取得する。


// 2. "uploads" リストのIDを取得

// https://www.youtube.com/channel/UCrXUsMBcfTVqwAS7DKg9C0Q
$channelId = "UCrXUsMBcfTVqwAS7DKg9C0Q"; // YouTube Japan
$response = $youtube->channels->listChannels('contentDetails', ['id' => $channelId]);

$item = $response['items'][0];
$uploadsListId = $item['contentDetails']['relatedPlaylists']['uploads'];

対象チャンネルの uploads プレイリスト情報を取得するには、そのチャンネルのチャンネルIDを指定してAPIリクエストを送る必要がある。

チャンネルIDとは、チャンネルURLの末尾の英数字から成る文字列のことで、例えば、

https://www.youtube.com/channel/UCrXUsMBcfTVqwAS7DKg9C0Q

というURLの場合、UCrXUsMBcfTVqwAS7DKg9C0Q がチャンネルIDとなる。

APIレスポンスに含める channel リソースのプロパティには contentDetails、フィルタのパラメータにはチャンネルIDを指定し、$youtube->channels->listChannels() でリクエストを実行する。

レスポンスとして返ってきた channel リソースには contentDetails.relatedPlaylists.uploads プロパティが含まれていて、そのプロパティの値が uploads リストのIDに該当する。

参考:
チャンネル URL について - YouTube ヘルプ
Channels: list | YouTube Data API | Google Developers
Channels | YouTube Data API | Google Developers # channelsリソースの形式と各プロパティの説明


最後に、uploads プレイリストIDを指定してプレイリスト内の動画情報を取得する。


// 3. "uploads" リスト内のアイテムを取得

$pageToken = "";

$params = [
  'playlistId' => $uploadsListId,
  'maxResults' => 10
];

do {
  $response = $youtube->playlistItems->listPlaylistItems('snippet', $params);

  foreach ($response['items'] as $item)
  {
    $snippet = $item['snippet'];

    echo "videoId: {$snippet['resourceId']['videoId']}\n";
    // https://www.youtube.com/watch?v={$snippet['resourceId']['videoId']}

    echo "title: {$snippet['title']}\n";
    echo "publishedAt: {$snippet['publishedAt']}\n";

    echo "----\n";
  }

  echo "\n";
  echo "nextPageToken: {$response['nextPageToken']}\n";
  echo "prevPageToken: {$response['prevPageToken']}\n";
  echo "totalResults: {$response['pageInfo']['totalResults']}\n";
  echo "resultsPerPage: {$response['pageInfo']['resultsPerPage']}\n";
  echo "\n";

  $pageToken = $response['nextPageToken'];
  $params['pageToken'] = $pageToken;

  sleep(1);
} while (!empty($pageToken));

APIレスポンスに含める playlistItem リソースのプロパティに snippet を指定して $youtube->playlistItems->listPlaylistItems() でリクエストを行い、プレイリスト内のアイテム(この場合は動画の情報)を取得する。

その際、APIリクエストのフィルタのパラメータは下記のように設定する。


$params = [
  'playlistId' => $uploadsListId,
  'maxResults' => 10
];

playlistId パラメータにプレイリストID、maxResults パラメータには、APIレスポンスに含める playlistItems リソースのアイテム数を指定する。デフォルト値は 5 で、最大 50 まで指定できる。

2ページ目以降など特定のページの結果を取得するには、pageToken を指定する。pageToken には、APIレスポンスに含まれている nextPageToken や prevPageToken の値を指定する。pageToken に何も指定しないと1ページ目の結果が返される。


$params = [
  'playlistId' => $uploadsListId,
  'maxResults' => 10,
  'pageToken' => $pageToken
];

maxResults に10を指定した場合、リクエスト毎に最大10件のアイテムが返ってくる。各アイテムは snippet プロパティを持ち、このプロパティを参照することで、アイテムの基本情報(動画ID、動画タイトル、追加日時など)を取得できる。

参考:
PlaylistItems: list | YouTube Data API | Google Developers
PlaylistItems | YouTube Data API | Google Developers # playlistItemsリソースの形式と各プロパティの説明

論理型の値を保持する配列の先頭要素を取得する際の注意点

過去の記事 で reset() を使って配列の先頭要素を取得する方法について書いたが、論理型の値を保持する配列に対しては注意が必要である。

論理型の配列データの先頭要素が false の場合、reset() は false を返すが、reset() は空の配列に対しても false を返すため、どちらも同じ false も得てしまう。


function head($array)
{
  return reset($array); // 配列の先頭要素を取得
}

$array1 = [false, true, false];
$array2 = [];

$v1 = head($array1);
var_dump($v1);
/* var_dump 出力
bool(false)
*/
$v2 = head($array2);
var_dump($v2);
/*
bool(false)
*/
var_dump($v1 === $v2);
/*
bool(true)
*/

どちらも false を得てしまう問題を回避するには、reset() を呼ぶ際に count() で配列の要素数を確認するか、key() の返り値が null でないことを確認する必要がある。


function head($array)
{
  return reset($array); // 配列の先頭要素を取得
}

$array1 = [false, true, false];
$array2 = [];

$v1 = count($array1) ? head($array1) : null;
$v2 = count($array2) ? head($array2) : null;
var_dump($v1 === $v2);
/*
bool(false)
*/

$v1 = !is_null(key($array1)) ? head($array1) : null;
$v2 = !is_null(key($array2)) ? head($array2) : null;
var_dump($v1 === $v2);
/*
bool(false)
*/

配列から空の要素を削除する

まず、変数が空であるかどうかのチェックを empty() に委ねる場合を考える。empty() では次のような値は空とみなされる。

  • "" (空文字列)
  • 0 (整数 の 0)
  • 0.0 (浮動小数点数の 0)
  • "0" (文字列 の 0)
  • NULL
  • FALSE
  • array() (空の配列)

これらは boolean に変換すると全て false とみなされる値となる。

また、array_filter() は、コールバック関数が与えられなかった場合、配列要素の中で boolean に変換して false に等しいものを削除してくれる。

よって、empty() で空とみなされる値を削除したい場合は、array_filter() を使って配列から空の要素を削除することができる。


// booleanに変換すると全てfalseとみなされる値
$array = ["", 0, 0.0, "0", NULL, false, []];

foreach ($array as $v)
{
  var_dump((bool)$v);
/*
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
*/
}

// 配列要素の中でbooleanに変換してfalseに等しいものを削除
$array = array_filter($array);
var_dump($array);
/*
array(0) {
}
*/

次に、変数が空であるかどうかを empty() だけでチェックできない場合を考える。例えば、数値や論理値、文字列の "0" を空とみなさない場合である。

独自に変数の値が空かどうかを判定する blank() を定義する。この関数の定義は、Laravelのヘルパー関数 blank() を参考にした。
framework/helpers.php at master · laravel/framework · GitHub

そして、blank() の返り値の逆を返す not_blank() を定義し、array_filter() のコールバック関数に指定すると、blank() で空とみなされる要素が削除された配列を得ることができる。


function blank($value)
{
  if (is_null($value)) {
    return true;
  }

  if (is_string($value)) {
    return trim($value) === '';
  }

  // 数値や論理値は空とみなさない
  if (is_numeric($value) || is_bool($value)) {
    return false;
  }

  return empty($value);
}

function not_blank($value)
{
  return !blank($value);
}

$array = ["", 0, 0.0, "0", NULL, false, []];

// blank()で空とみなす要素を削除
$array = array_filter($array, "not_blank");
var_dump($array);
/*
blank()では数値や論理値、文字列の"0"を空とみなさないので、削除されない
array(4) {
  [1]=>
  int(0)
  [2]=>
  float(0)
  [3]=>
  string(1) "0"
  [5]=>
  bool(false)
}
*/

配列から特定の値を持つ要素を検索して削除する

配列から特定の値を持つ要素を検索して削除する場合、複数の方法が考えられる。下記は、foreach を使って配列の値を確認していく一番シンプルな方法の例。


$array = [
  ['id' => 1000, 'name' => "aaa"],
  ['id' => 1001, 'name' => "bbb"],
  ['id' => 1002, 'name' => "ccc"],
];

$del_id = 1001;

foreach ($array as $i => $v)
{
  if ($array[$i]["id"] == $del_id)
    unset($array[$i]);
}

var_dump($array);
/* var_dump 出力
array(2) {
  [0]=>
  array(2) {
    ["id"]=>
    int(1000)
    ["name"]=>
    string(3) "aaa"
  }
  [2]=>
  array(2) {
    ["id"]=>
    int(1002)
    ["name"]=>
    string(3) "ccc"
  }
}
*/

また、別の方法として array_search() を使う方法がある。array_search() は、指定した値を配列内から探し、最初に見つかった要素のキーを返す関数である。

ただし、サンプルコードのような多次元配列に対して array_search() を使う場合、1つ工夫が必要で array_column() を使って検索対象にしたいデータを抜き出す必要がある。


// array_search()の例

$array = [
  ['id' => 1000, 'name' => "aaa"],
  ['id' => 1001, 'name' => "bbb"],
  ['id' => 1002, 'name' => "ccc"],
];

$del_id = 1001;

var_dump(array_column($array, "id"));
/*
array_columnを使うと、多次元配列から"id"キーに対応するデータを抜き出すことができる
array(3) {
  [0]=>
  int(1000)
  [1]=>
  int(1001)
  [2]=>
  int(1002)
}
*/

// "id"キーに対応する要素の値が$del_idを持つものを検索
$i = array_search($del_id, array_column($array, "id"));

if ($i)
  unset($array[$i]);

var_dump($array);
/*
array(2) {
  [0]=>
  array(2) {
    ["id"]=>
    int(1000)
    ["name"]=>
    string(3) "aaa"
  }
  [2]=>
  array(2) {
    ["id"]=>
    int(1002)
    ["name"]=>
    string(3) "ccc"
  }
}
*/

さらに別の方法として、array_keys() を使う方法がある。array_keys() は、2番目の引数を指定するとその指定した値を持つ要素のキーのみを返してくれる。array_keys() の返り値は、配列であることに注意。

また、array_search() のときと同様に、多次元配列に対しては、array_column() を使って検索対象にしたいデータを抜き出す必要がある。


// array_keys()の例

$array = [
  ['id' => 1000, 'name' => "aaa"],
  ['id' => 1001, 'name' => "bbb"],
  ['id' => 1002, 'name' => "ccc"],
];

$del_id = 1001;

$keys = array_keys(array_column($array, "id"), $del_id);
foreach ($keys as $i)
{
  unset($array[$i]);
}

var_dump($array);
/*
array(2) {
  [0]=>
  array(2) {
    ["id"]=>
    int(1000)
    ["name"]=>
    string(3) "aaa"
  }
  [2]=>
  array(2) {
    ["id"]=>
    int(1002)
    ["name"]=>
    string(3) "ccc"
  }
}
*/

元の配列データを変更せずに先頭の要素を取得する

配列の先頭の要素を取得する関数として array_shift() がある。

ただし、array_shift() は先頭の要素を配列から取り除いてしまうので、「元の配列データを変更せずに」という制約がある場合、array_shift() は使えない。

このような場合、reset() が使える。関数名からは想像が付かないが、reset() は、配列の内部ポインタを先頭の要素に戻し、そして、「配列の最初の要素の値」を返してくれる。

reset() を使えば、元の配列データを変更せず、また、先頭のキーが不明な場合でも、先頭の要素を取得できる。


$array = [
  'status_130134' => ['text' => "hi",  'date' => "20200101"]
];

$v = reset($array); // 配列の先頭の要素を取得

var_dump($v);
/* var_dump 出力
array(2) {
  ["text"]=>
  string(2) "hi"
  ["date"]=>
  string(8) "20200101"
}
*/
var_dump($array);
/* var_dump 出力
元の配列データが変更されていないことを確認
array(1) {
  ["status_130134"]=>
  array(2) {
    ["text"]=>
    string(2) "hi"
    ["date"]=>
    string(8) "20200101"
  }
}
*/

reset() という関数名と実際の処理の意図が合っていないことで、コードの可読性を懸念する場合、reset() を適当な名前を付けた関数でラップしてもよいだろう。


// reset()をheadという名前を付けた関数でラップ
function head($array)
{
  return reset($array);
}

$array = [
  'status_130134' => ['text' => "hi",  'date' => "20200101"]
];

$v = head($array); // 配列の先頭の要素を取得
var_dump($v);
/*
array(2) {
  ["text"]=>
  string(2) "hi"
  ["date"]=>
  string(8) "20200101"
}
*/

実際、Laravelのヘルパー関数 head()(配列の先頭の要素を返す関数)は、reset() の返り値を返す関数として実装されている。Laravelのヘルパー関数のソースコードを確認したい場合は、下記のリンクからソースコードを読むことができる。
framework/helpers.php at master · laravel/framework · GitHub