◀ 4.

YouTube APIの検索|結果ID・ページ送りと割当の確認

▶

YouTube Data API v3のsearch.listは、動画・チャンネル・再生リストへの参照を検索するメソッドです。動画の詳細そのものや、チャンネルの正確な統計を返すメソッドとは分けます。検索条件、結果のid.kind、次ページtokenを一組で確認します。

チャンネル取得と検索で異なるID構造、統計の欠落と次ページtokenの扱い

検索条件の基本

現在の要求ではpart=snippetを指定します。旧文のid、snippetを任意のpart一覧として扱わず、検索リファレンスの要求条件に合わせます。qに検索語、typeにvideo/channel/playlist、orderに並び方、maxResultsにページ当たりの上限を指定できます。typeの複数値はカンマ区切りで、video|channelというパイプ文字の書式ではありません。

検索語「ゲーム」等の非ASCII、&、空白、+を含む値は、利用するHTTPクライアントのクエリ構築機能でエンコードします。文字列連結でURL全体へ埋め込まず、入力の&を別パラメータにしてしまわないようにします。これはキーや任意URLをWeb入力から受け付ける公開プロキシの例ではありません。

videoDurationやvideoCaption等、type=videoを必要とする条件もあります。forMine等はOAuthを含む追加条件が必要です。エラーを消すために条件を外したりscopeを広げたりせず、当該パラメータの組合せを公式資料へ照合します。

返ってきたIDは種類ごとに読む

items[].id.kind読むfield混同しないもの
youtube#videoid.videoIdsnippet.channelIdは投稿者のチャンネル
youtube#channelid.channelId動画IDではない
youtube#playlistid.playlistId再生リスト内の各動画IDではない

公式資料には非既定orderのときtypeの厳密な絞込みに影響し、想定外の種類が含まれる可能性も注記されています。要求のtypeだけを信用して全行へvideoIdを読みに行かず、応答のid.kindと必要なfieldを確認します。未知の種類を既知の種類へ推測変換しません。

取得済みの検索JSONを読むPHP例

共通教材のytReadSearchPage(200, 取得済み本文)は、既知の3種類だけをkindとidへ整理します。未知kindはskippedの件数にし、生の未知値やsnippet本文を出力しません。既知kindなのに対応IDがなければ例外です。チャンネル統計は別関数ytReadChannelPageで読むため、2つの応答形式を混ぜません。

<?php
declare(strict_types=1);

// Offline teaching helpers: no network, credentials, logs or automatic paging.
function ytReadListRoot(int $httpStatus, string $body, string $kind): stdClass
{
    if ($httpStatus !== 200 || strlen($body) > 262144) {
        throw new InvalidArgumentException('Expected bounded HTTP 200 JSON');
    }
    try {
        $root = json_decode($body, false, 16, JSON_THROW_ON_ERROR);
    } catch (JsonException) {
        throw new InvalidArgumentException('Invalid JSON');
    }
    if (!$root instanceof stdClass || property_exists($root, 'error')
        || ($root->kind ?? null) !== $kind || !isset($root->items)
        || !is_array($root->items) || count($root->items) > 50) {
        throw new InvalidArgumentException('Unexpected list shape');
    }
    return $root;
}

function ytReadIdentifier(mixed $value): string
{
    // A local teaching constraint, not proof an ID exists or is authorized.
    if (!is_string($value) || !preg_match('/\A[A-Za-z0-9_-]{1,128}\z/D', $value)) {
        throw new InvalidArgumentException('Unexpected identifier');
    }
    return $value;
}

function ytReadNextPage(stdClass $root): ?string
{
    if (!property_exists($root, 'nextPageToken')) {
        return null;
    }
    $token = $root->nextPageToken;
    if (!is_string($token) || strlen($token) > 1024 || preg_match('/[\x00-\x20\x7f]/', $token)) {
        throw new InvalidArgumentException('Unexpected next page token');
    }
    return $token === '' ? null : $token;
}

function ytReadSearchPage(int $httpStatus, string $body): array
{
    $root = ytReadListRoot($httpStatus, $body, 'youtube#searchListResponse');
    $fields = ['youtube#video' => 'videoId', 'youtube#channel' => 'channelId', 'youtube#playlist' => 'playlistId'];
    $items = [];
    $skipped = 0;
    foreach ($root->items as $item) {
        if (!$item instanceof stdClass || ($item->kind ?? null) !== 'youtube#searchResult'
            || !isset($item->id) || !$item->id instanceof stdClass || !is_string($item->id->kind ?? null)) {
            throw new InvalidArgumentException('Unexpected search item');
        }
        $kind = $item->id->kind;
        if (!isset($fields[$kind])) {
            $skipped++;
            continue;
        }
        $field = $fields[$kind];
        $items[] = ['kind' => $kind, 'id' => ytReadIdentifier($item->id->$field ?? null)];
    }
    return ['items' => $items, 'skipped' => $skipped, 'next_page_token' => ytReadNextPage($root)];
}

function ytReadCount(stdClass $stats, string $field): ?string
{
    if (!property_exists($stats, $field)) {
        return null;
    }
    $value = $stats->$field;
    if (!is_string($value) || !preg_match('/\A[0-9]{1,40}\z/D', $value)) {
        throw new InvalidArgumentException('Unexpected count string');
    }
    return $value;
}

function ytReadChannelPage(int $httpStatus, string $body): array
{
    $root = ytReadListRoot($httpStatus, $body, 'youtube#channelListResponse');
    $items = [];
    foreach ($root->items as $item) {
        if (!$item instanceof stdClass || ($item->kind ?? null) !== 'youtube#channel') {
            throw new InvalidArgumentException('Unexpected channel item');
        }
        $id = ytReadIdentifier($item->id ?? null);
        $stats = $item->statistics ?? new stdClass();
        if (!$stats instanceof stdClass) {
            throw new InvalidArgumentException('Unexpected statistics');
        }
        $hidden = $stats->hiddenSubscriberCount ?? null;
        if (property_exists($stats, 'hiddenSubscriberCount') && !is_bool($stats->hiddenSubscriberCount)) {
            throw new InvalidArgumentException('Unexpected visibility flag');
        }
        $items[] = [
            'id' => $id,
            'view_count' => ytReadCount($stats, 'viewCount'),
            'video_count' => ytReadCount($stats, 'videoCount'),
            'hidden_subscriber_count' => $hidden,
            'subscriber_count' => $hidden === false ? ytReadCount($stats, 'subscriberCount') : null,
        ];
    }
    return ['items' => $items, 'next_page_token' => ytReadNextPage($root)];
}

このコードはネットワーク/APIキー/ログ/HTML生成/自動再送/自動ページ取得を行いません。入力は262,144byte/JSON深さ16/items最大50件、IDのASCII受入規則とカウント/次ページtokenの上限は教材独自の制限です。Googleの全schema・ID存在・認可や、Content-Type/受信途中の容量/時間/JSON重複キーは検証しません。戻り値や例外を扱う側でも、秘密値を出力しないエラー処理とHTMLエスケープが必要です。

ページ送りはtotalResultsで数えない

pageInfo.totalResultsは概算で、公式資料では最大1,000,000とされています。これを割って「全ページ数」を作り、順に呼び出す設計にはしません。次ページの有無はnextPageToken、前ページはprevPageTokenを確認します。新教材は次ページだけを返す限定例です。

maxResultsは0〜50の範囲で指定でき、既定値は5です。1ページが上限より少ない、あるいはitemsが空ということだけで終端を推測せず、応答のtokenを見ます。tokenは不透明な値のまま同じ条件の要求へ渡し、デコード/加工/採番しません。総ページ数・実行時間/回数・同じtokenの反復等の停止条件は別に設けます。

並び方と結果の変動

order=date等を指定しても、複数回の検索やページ間で同じ結果が固定される保証はありません。公開状態や検索対象の更新等もあり得ます。元記事にある結果変動の観察は保守記録として残しますが、原因不明を特定の障害と断定したり、未検証の対処を追加したりしません。大量取得の完全性を確認する検体でもありません。

割当・応答の安全な扱い

2026年10月26日に確認した公式表ではsearch.listは独立した検索枠で1呼出し=1、既定100呼出し/日です。他操作の10,000 units/日と一律に割り算しません。追加ページや無効な要求も割当を消費し得るため、検証のための実検索を連打しません。枠不足を別キー/プロジェクトへ回して回避しません。

200でもitemsが空なら、必要な対象が見つかったという意味ではありません。検索結果のタイトル/説明やURLも外部データとして扱います。今回はそれらをHTMLへ挿入せず、IDの限定抽出だけにしました。実際に表示する場合もHTMLを文字列連結せず、安全なテキスト出力を使います。

元の構文・URL・応答例

以下の旧表・2017年の応答例を保持しています。旧JSONは構文有効で、掲載された動画の現在の存在/再生可否・数値を確認したものではありません。表ごとの注意と、新しい読み取り例を分けて参照してください。

旧表の注意:旧要点のtype=video|channelは候補の表記で、実際の複数指定はカンマ区切りです。要求だけでなく応答のid.kindも確認します。

この記事の要点
  • YouTube Data API (v3) で動画やチャンネルを検索
  • エンドポイント: https://www.googleapis.com/youtube/v3/search
  • 主要パラメータ: q (検索語) / part=snippet / type=video|channel / maxResults
  • order=date / relevance / viewCount で並び替え

 

これはYouTube Data API (v3)を使用した動画やチャンネルを検索する方法に関する記事です。

 

基本構文は以下の通りです。

旧表の注意:これは検索のエンドポイントの旧表です。動画の詳細/統計取得とは別の操作です。

構文

https://www.googleapis.com/youtube/v3/search

 

基本構文に対してパラメータを付与する必要があります。

旧表の注意:現在のsearch.listではpart=snippetを指定します。旧文のidとsnippetを任意のpart候補として扱いません。

必須パラメータ

part

part名によってレスポンスの内容を変更することが出来ます。

partは以下を指定することが出来ます。

現在の要求はpart=snippetです。IDは応答のid.kindと、その種類に対応するfieldから読みます。

snippetを指定すると詳細な情報を取得できます。

 

旧表の注意:指定条件の組合せやOAuthの要否は各パラメータの公式仕様を確認します。typeだけで応答の種類が常に保証されると考えません。

その他省略可能なパラメータ

type, order, q, ...

typeにはchannel, playlist, videoを指定できます。typeを指定することでレスポンスが動画なのかチャンネルなのかが変わります。

orderは表示順を指定できます。

viewCountを指定すれば再生回数の多い順、dateを指定すれば作成日の新しい順に取得できます。

元記事にはtype=channelで結果が変動したという観察があります。これを再現試験済みの特定障害とは扱いません。現在の公式注記と、検索結果を固定できない点を先に説明しています。

qには検索ワードを指定できます。

 

以下、例文です。

ゲームというキーワードで動画を取得しています。

旧表の注意:旧URLへ実キーを直接入れず、検索語等をクエリ構築機能でエンコードします。要求を送るたびに割当を消費し得るため、この記事の検証で実検索は行っていません。

例文

https://www.googleapis.com/youtube/v3/search
?part=snippet
&maxResults=1
&q=ゲーム
&type=video
&key=【APIキー】

 

以下、例文のレスポンスです。

旧表の注意:これは2017年の検索結果の記録で、現在の動画の存在/数値を確認したものではありません。totalResultsは概算であり、ページ数の計算には使いません。

レスポンス
{
 "kind": "youtube#searchListResponse",
 "etag": "\"ld9biNPKjAjgjV7EZ4EKeEGrhao/4dI2RGjjdR8Xj1QdYiubgg5cVoM\"",
 "nextPageToken": "CAEQAA",
 "regionCode": "JP",
 "pageInfo": {
  "totalResults": 1000000,
  "resultsPerPage": 1
 },
 "items": [
  {
   "kind": "youtube#searchResult",
   "etag": "\"ld9biNPKjAjgjV7EZ4EKeEGrhao/p16ohTtnXJkVcAwAG7SytmUSYRM\"",
   "id": {
    "kind": "youtube#video",
    "videoId": "1XMWpkjIpXU"
   },
   "snippet": {
    "publishedAt": "2017-10-26T13:49:19.000Z",
    "channelId": "UCvnqLqG_V2EU-JAqgs2icGg",
    "title": "このすば この欲深いゲームに審判を!  ウィズは膝枕+耳かきの豪華なご褒美",
    "description": "この欲深いゲームに審判を! 『PS4』 ウィズCV:#堀江由衣 自分の中の代表作アニメで例えると、『とらドラ!』 歌は『Stand Up!、インモラリストetc....",
    "thumbnails": {
     "default": {
      "url": "https://i.ytimg.com/vi/1XMWpkjIpXU/default.jpg",
      "width": 120,
      "height": 90
     },
     "medium": {
      "url": "https://i.ytimg.com/vi/1XMWpkjIpXU/mqdefault.jpg",
      "width": 320,
      "height": 180
     },
     "high": {
      "url": "https://i.ytimg.com/vi/1XMWpkjIpXU/hqdefault.jpg",
      "width": 480,
      "height": 360
     }
    },
    "channelTitle": "クウェイス Qweyse",
    "liveBroadcastContent": "none"
   }
  }
 ]
}

 

指定できるパラメータのリストはこちらを参照してください。

確認範囲と関連情報

2026年10月26日確認。旧表/応答例/元リンクと全履歴を保持し、PHP例は合成データと取得済み旧JSONでオフライン検査しています。実キー/HTTP通信、Google側の認証/認可、現在のID/動画/統計値、実ブラウザや全field/schemaを検証したものではありません。キー/token/応答全文をログへ出さず、無条件の自動再送は行いません。

親ページから子ページを選択できます。チャンネル情報と検索を用途で選び、エラーの切り分け、キーの扱い、API有効化も参照してください。

公式資料:channels.list、チャンネルの統計/field、search.listとページ情報、操作別の割当。

子ページ

子ページはありません

同階層のページ
  1. APIキー取得方法
  2. APIの有効化
  3. チャンネル情報の取得
  4. 動画やチャンネルの検索
  5. エラー一覧

最近更新/作成されたページ