sociallinks.dev

ブログ / 開発者ガイド

ハンドルからYouTubeのチャンネルIDを取得する方法(逆方向も)

クリエイターはYouTubeチャンネルを@handleの形で共有しますが、YouTube Data APIのほぼすべてはチャンネルIDをキーにしています。ハンドルだけを保存すると名前の変更でレコードが壊れ、IDだけを保存するとユーザーに見慣れた名前を表示できません。このガイドでは、1回のAPI呼び出しでハンドルをチャンネルIDに変換する方法、IDからハンドルへ50件ずつ戻す方法、古い/user/や/c/のURLの扱い方、そしてデフォルトの割り当て量の範囲に収める方法を紹介します。

ハンドル、チャンネルID、旧形式の名前

YouTubeチャンネルには、最大4種類の識別子でアクセスできます。そのうち安定が保証されているのは1つだけです。

識別子例URLの形メモ
チャンネルIDUC_x5XG1OV2P6uZZ5FSM9Ttwyoutube.com/channel/UC...YouTubeがチャンネルの識別に使うID。大文字と小文字を区別する。主キーとして使う。
ハンドル@GoogleDevelopersyoutube.com/@GoogleDevelopers一意で、大文字と小文字を区別せず、持ち主がYouTube Studioで変更できる。
旧形式のユーザー名GoogleDevelopersyoutube.com/user/GoogleDevelopers以前のユーザー名の仕組みのもの。APIのforUsernameフィルタで変換できる。
旧形式のカスタムURLGoogleDevelopersyoutube.com/c/GoogleDevelopers以前のカスタムURL。YouTubeによれば引き続き使える。変換できるAPIフィルタはない。

チャンネルIDをキーとして保存し、ハンドルは更新していく表示用の値として持ちましょう。このガイドの残りは、ユーザーから受け取ったものをそのIDに変換する方法についてです。

APIキーを用意し、割り当て量を理解する

  1. Step 1Google Cloudプロジェクトを作成する

    Google Cloud consoleで、プロジェクトを作成するか既存のものを選びます。

  2. Step 2YouTube Data API v3を有効にする

    「APIとサービス」を開き、ライブラリでYouTube Data API v3を見つけて、プロジェクトで有効にします。

  3. Step 3APIキーを作成する

    「認証情報」でAPIキーを作成します。ハンドルやIDによるchannels.listのような公開チャンネルのデータには、OAuthではなくキーだけが必要です。キーはYouTube Data APIに限定しておきます。

  4. Step 4キーをコードに書かない

    YOUTUBE_API_KEYなどの環境変数に入れ、実行時に読み込みます。下の例はまさにそうしています。

  5. Step 5割り当て量を確認する

    コンソールのそのAPIの「割り当て」ページに、1日の割り当て量と使用量が表示されます。

Googleのドキュメントによる、重要な数値は次のとおりです。

つまり、デフォルトの割り当て量で1日約10,000件のハンドル検索ができ、IDの検索ならその最大50倍が可能です。1回の呼び出しで、同じ1ユニットでIDによる50チャンネルを取得できるからです。

forHandleでハンドルからチャンネルIDへ

channels.listが受け付けるフィルタは1つだけです。forHandleフィルタはYouTubeのハンドルを受け取り、リファレンスによれば値の先頭に@記号を付けてもよいので、GoogleDevelopersでも@GoogleDevelopersでも動きます。IDだけが必要ならpart=idを指定しましょう。コストは同じで、レスポンスが小さくなります。

cURL

curl -s "https://www.googleapis.com/youtube/v3/channels?part=id&forHandle=@GoogleDevelopers&key=$YOUTUBE_API_KEY"

下のNodeの例では、リクエストを1つのヘルパーlistChannelsにまとめ、ガイドの残りでも再利用します。一致するものがなければ空のリストを返し、無効なキーや割り当て量の超過などの本当のエラーでは例外を投げます。

Node.js

const API = "https://www.googleapis.com/youtube/v3/channels";
async function listChannels(params) {
  const key = process.env.YOUTUBE_API_KEY;
  if (!key) throw new Error("Set YOUTUBE_API_KEY");
  const url = new URL(API);
  url.search = new URLSearchParams({ ...params, key }).toString();
  const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
  const data = await res.json();
  if (!res.ok) {
    const reason = data.error?.errors?.[0]?.reason;
    if (reason === "channelNotFound") return [];
    throw new Error(`YouTube API ${res.status} ${reason ?? ""}: ${data.error?.message}`);
  }
  return data.items ?? []; // no match: items is empty or missing
}

/** "@GoogleDevelopers" or "GoogleDevelopers" -> "UC_x5XG1OV2P6uZZ5FSM9Ttw" or null */
export async function channelIdFromHandle(handle) {
  const items = await listChannels({ part: "id", forHandle: handle.trim() });
  return items[0]?.id ?? null;
}

/** Legacy username from a youtube.com/user/<name> URL -> channel ID or null */
export async function channelIdFromUsername(username) {
  const items = await listChannels({ part: "id", forUsername: username.trim() });
  return items[0]?.id ?? null;
}

/** Any number of channel IDs -> Map(id -> { title, customUrl }) */
export async function channelsById(ids) {
  const unique = [...new Set(ids)];
  const found = new Map();
  for (let i = 0; i < unique.length; i += 50) {
    const batch = unique.slice(i, i + 50);
    const items = await listChannels({ part: "snippet", id: batch.join(","), maxResults: "50" });
    for (const ch of items) {
      found.set(ch.id, { title: ch.snippet.title, customUrl: ch.snippet.customUrl ?? null });
    }
  }
  return found; // IDs missing from the map were not found
}

Python

import os

import requests

API = "https://www.googleapis.com/youtube/v3/channels"


def list_channels(**params):
    key = os.environ.get("YOUTUBE_API_KEY")
    if not key:
        raise RuntimeError("Set YOUTUBE_API_KEY")
    response = requests.get(API, params={**params, "key": key}, timeout=10)
    data = response.json()
    if not response.ok:
        errors = data.get("error", {}).get("errors") or [{}]
        reason = errors[0].get("reason")
        if reason == "channelNotFound":
            return []
        raise RuntimeError(f"YouTube API {response.status_code} {reason}: {data.get('error', {}).get('message')}")
    return data.get("items", [])  # no match: items is empty or missing


def channel_id_from_handle(handle):
    """'@GoogleDevelopers' or 'GoogleDevelopers' -> 'UC_x5XG1OV2P6uZZ5FSM9Ttw' or None"""
    items = list_channels(part="id", forHandle=handle.strip())
    return items[0]["id"] if items else None


def channel_id_from_username(username):
    """Legacy username from a youtube.com/user/<name> URL -> channel ID or None"""
    items = list_channels(part="id", forUsername=username.strip())
    return items[0]["id"] if items else None


def channels_by_id(ids):
    """Any number of channel IDs -> {id: {"title", "customUrl"}}; missing IDs were not found"""
    unique = list(dict.fromkeys(ids))
    found = {}
    for i in range(0, len(unique), 50):
        batch = unique[i:i + 50]
        for ch in list_channels(part="snippet", id=",".join(batch), maxResults=50):
            found[ch["id"]] = {
                "title": ch["snippet"]["title"],
                "customUrl": ch["snippet"].get("customUrl"),
            }
    return found

ハンドルは、URLのパーセントエンコーディングをデコードしたうえで、ユーザーが書いたとおりに渡します。ハンドルにはASCIIだけでなくさまざまな文字体系の文字を使えますが、URLSearchParamsもrequestsもパラメータをエンコードしてくれます。

旧形式のユーザー名と/c/のカスタムURL

クリエイターのプロフィールやスプレッドシートにある古いリンクは、ハンドル導入前の形式を使っていることがよくあります。

チャンネルIDからハンドルへ、50件ずつ

逆方向には、カンマ区切りのチャンネルIDのリストを受け取るidフィルタを使います。part=snippetを指定し、snippet.customUrlを読みます。上のchannelsByIdはIDの重複を除き、50件ずつ送り、IDをキーにしたマップを返します。マップに含まれないIDは、見つからなかったか、削除されたか、APIから見えないものです。

見つからない場合、エラー、キャッシュ

何も一致しない検索はエラーではありません。forHandleとforUsernameではレスポンスに項目がないだけなので、items配列が空か存在しないかを確認します。channels.listのドキュメントにあるエラーは次のとおりです。

エラーの理由HTTP発生する場面対応
200のレスポンスにitemsがない200ハンドルやユーザー名が存在しないnullを返し、見つからなかった結果を短時間キャッシュする
channelNotFound404idパラメータのチャンネルが見つからない上のヘルパーと同じく、見つからなかったものとして扱う
invalidCriteria400リクエストに複数のフィルタがあるforHandle、forUsername、idのうち1つだけを送る
channelForbidden403チャンネルがそのリクエストに対応していないか、適切に認可されていないログに残してそのチャンネルを飛ばす
quotaExceeded403プロジェクトが1日の割り当て量を使い切った止めて太平洋時間の午前0時以降に再試行するか、割り当て量の追加を申請する
badRequest(APIキーが無効)400キーがない、または間違っている設定を直す。再試行しない

割り当て量と待ち時間を節約するために結果をキャッシュしましょう。ただし、データの保存に関するYouTubeのルールは守ってください。YouTube API Services Developer Policies(III.E.4項)によれば、認可されていないAPIデータを保存できるのは最長30暦日で、その後は削除するか更新する必要があります。小文字のハンドルをキーにし、TTLを30日より十分短くしたキャッシュで、効果の大半は得られます。

Node.jsのキャッシュ

import { channelIdFromHandle } from "./youtube.mjs";

const TTL_MS = 7 * 24 * 60 * 60 * 1000; // refresh well inside YouTube's 30-day limit
const cache = new Map(); // lowercase handle -> { id, at }

export async function cachedChannelId(handle) {
  const key = handle.trim().replace(/^@/, "").toLowerCase();
  const hit = cache.get(key);
  if (hit && Date.now() - hit.at < TTL_MS) return hit.id;
  const id = await channelIdFromHandle(handle);
  cache.set(key, { id, at: Date.now() }); // caches misses (null) too
  return id;
}

本番環境では、同じロジックをRedisかexpires_at列を持つデータベースの表に置き、新しく作られたハンドルをすぐ拾えるよう、見つからなかった結果のTTLは見つかった結果より短くします。

APIキーなしの代替手段:チャンネルページを読む

公開されているチャンネルページのHTMLにはチャンネルIDが含まれています。現在、https://www.youtube.com/@GoogleDevelopersをリクエストすると、<link rel="canonical">がhttps://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttwを指すページが返ってきます。/c/や/user/のページも同様なので、推測せずに/c/の名前を変換できる唯一の方法になります。

Node.js

// Unofficial fallback: reads the public channel page. Fragile, prefer the API.
export async function channelIdFromPage(handle) {
  const url = `https://www.youtube.com/@${encodeURIComponent(handle.replace(/^@/, ""))}`;
  const res = await fetch(url, {
    headers: { "Accept-Language": "en-US" },
    signal: AbortSignal.timeout(10_000),
  });
  if (!res.ok) return null;
  const html = await res.text();
  const match = html.match(/<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/(UC[A-Za-z0-9_-]{22})"/);
  return match?.[1] ?? null;
}

Python

import re
from urllib.parse import quote

import requests

CANONICAL = re.compile(r'<link rel="canonical" href="https://www\.youtube\.com/channel/(UC[A-Za-z0-9_-]{22})"')


def channel_id_from_page(handle):
    """Unofficial fallback: reads the public channel page. Fragile, prefer the API."""
    url = "https://www.youtube.com/@" + quote(handle.lstrip("@"), safe="._-")
    response = requests.get(url, headers={"Accept-Language": "en-US"}, timeout=10)
    if not response.ok:
        return None
    match = CANONICAL.search(response.text)
    return match.group(1) if match else None

この方法は控えめに、APIでは対応できない場合だけ使いましょう。非公式で壊れやすい方法です。

方法の比較

方法入力コストメリットデメリット
forHandleを使ったchannels.listハンドル(@の有無は問わない)ハンドル1件につき1ユニット公式、完全一致、どのハンドルにも使える1回の呼び出しで1ハンドル
forUsernameを使ったchannels.list旧形式の/user/の名前名前1件につき1ユニット公式、完全一致旧形式のユーザー名専用
idを使ったchannels.list最大50件のチャンネルIDID50件につき1ユニット公式、一括処理、タイトルとcustomUrlを返すcustomUrlがないことがある
search.list自由なテキストデフォルトで1日100回の別枠の上限名前でチャンネルを探せる完全一致ではなく順位付きの推測。上限が小さい
チャンネルページのcanonicalリンク/c/を含むあらゆるチャンネルURLAPIの割り当て量を使わないキーなしで使える。/c/の名前を変換できる非公式、壊れやすい、ブロックされることがある、規約に抵触するおそれ

ほとんどのプロダクトにとって、正しい構成はシンプルです。forHandleでハンドルを変換し、チャンネルIDを保存し、idフィルタで50件ずつハンドルを更新し、まれな/c/のリンク用にページを読む代替手段をフラグの後ろに置いておきます。クリエイターの他のプラットフォームのアカウントも必要なら、sociallinks.devは、Instagram、TikTok、YouTube、XのプロフィールURLを1つ受け取り、そのクリエイターの他のプラットフォームのアカウントを、確信度スコアと一致ごとの根拠付きで返すAPIです。

よくある質問

forHandleにハンドルを渡すとき、@は必要ですか?

必要ありません。channels.listのリファレンスによれば、forHandleの値の先頭に@記号を付けてもよく、例としてGoogleDevelopersと@GoogleDevelopersの両方が挙げられています。

ハンドルの検索で割り当て量はどのくらい使いますか?

1ユニットです。channels.listの呼び出しはすべて1ユニットで、デフォルトの割り当ては1日10,000ユニット、太平洋時間の午前0時にリセットされます。無効なリクエストにも最低1ユニットかかります。

1回の呼び出しで複数のハンドルを検索できますか?

できません。forHandleが受け取るハンドルは1つです。カンマ区切りのリストを受け付けるのはidフィルタだけなので、逆方向(IDからハンドル)は一括処理にし、順方向はキャッシュしましょう。

チャンネルIDが変わることはありますか?

チャンネルIDは、YouTubeがチャンネルを一意に識別するために使う識別子です。ハンドルやタイトルを変えても変わりません。だからこそ主キーに適しています。ハンドル、タイトル、カスタムURLはどれも変わる可能性があります。

snippet.customUrlが空で返ってくるのはなぜですか?

YouTube自身の改訂履歴にあるとおり、すべてのチャンネルがカスタムURLを持っているわけではありません。この項目は任意として扱い、代わりに/channel/UC...のURLを表示しましょう。

結果はどのくらいの期間キャッシュできますか?

YouTubeのDeveloper Policiesでは、保存したAPIデータは30暦日以内に削除するか更新する必要があります。見つかった結果のTTLを1週間以下、見つからなかった結果をそれより短くすれば、十分にその範囲内に収まります。

クリエイターのアカウントを探す