ブログ / 開発者ガイド
ハンドルからYouTubeのチャンネルIDを取得する方法(逆方向も)
クリエイターはYouTubeチャンネルを@handleの形で共有しますが、YouTube Data APIのほぼすべてはチャンネルIDをキーにしています。ハンドルだけを保存すると名前の変更でレコードが壊れ、IDだけを保存するとユーザーに見慣れた名前を表示できません。このガイドでは、1回のAPI呼び出しでハンドルをチャンネルIDに変換する方法、IDからハンドルへ50件ずつ戻す方法、古い/user/や/c/のURLの扱い方、そしてデフォルトの割り当て量の範囲に収める方法を紹介します。
ハンドル、チャンネルID、旧形式の名前
YouTubeチャンネルには、最大4種類の識別子でアクセスできます。そのうち安定が保証されているのは1つだけです。
| 識別子 | 例 | URLの形 | メモ |
|---|---|---|---|
| チャンネルID | UC_x5XG1OV2P6uZZ5FSM9Ttw | youtube.com/channel/UC... | YouTubeがチャンネルの識別に使うID。大文字と小文字を区別する。主キーとして使う。 |
| ハンドル | @GoogleDevelopers | youtube.com/@GoogleDevelopers | 一意で、大文字と小文字を区別せず、持ち主がYouTube Studioで変更できる。 |
| 旧形式のユーザー名 | GoogleDevelopers | youtube.com/user/GoogleDevelopers | 以前のユーザー名の仕組みのもの。APIのforUsernameフィルタで変換できる。 |
| 旧形式のカスタムURL | GoogleDevelopers | youtube.com/c/GoogleDevelopers | 以前のカスタムURL。YouTubeによれば引き続き使える。変換できるAPIフィルタはない。 |
チャンネルIDをキーとして保存し、ハンドルは更新していく表示用の値として持ちましょう。このガイドの残りは、ユーザーから受け取ったものをそのIDに変換する方法についてです。
APIキーを用意し、割り当て量を理解する
- Step 1Google Cloudプロジェクトを作成する
Google Cloud consoleで、プロジェクトを作成するか既存のものを選びます。
- Step 2YouTube Data API v3を有効にする
「APIとサービス」を開き、ライブラリでYouTube Data API v3を見つけて、プロジェクトで有効にします。
- Step 3APIキーを作成する
「認証情報」でAPIキーを作成します。ハンドルやIDによる
channels.listのような公開チャンネルのデータには、OAuthではなくキーだけが必要です。キーはYouTube Data APIに限定しておきます。 - Step 4キーをコードに書かない
YOUTUBE_API_KEYなどの環境変数に入れ、実行時に読み込みます。下の例はまさにそうしています。 - Step 5割り当て量を確認する
コンソールのそのAPIの「割り当て」ページに、1日の割り当て量と使用量が表示されます。
Googleのドキュメントによる、重要な数値は次のとおりです。
channels.listの呼び出しは、どのフィルタを使っても1ユニットです。- プロジェクトには、
search.listとvideos.insert以外のすべてのエンドポイント向けに、デフォルトで1日10,000ユニットが割り当てられます。この2つには現在、それぞれ1日100回という独自の上限があります。 - 無効なリクエストも含めて、すべてのリクエストに最低1ユニットかかります。
- 1日の割り当て量は太平洋時間の午前0時にリセットされます。
つまり、デフォルトの割り当て量で1日約10,000件のハンドル検索ができ、IDの検索ならその最大50倍が可能です。1回の呼び出しで、同じ1ユニットでIDによる50チャンネルを取得できるからです。
forHandleでハンドルからチャンネルIDへ
channels.listが受け付けるフィルタは1つだけです。forHandleフィルタはYouTubeのハンドルを受け取り、リファレンスによれば値の先頭に@記号を付けてもよいので、GoogleDevelopersでも@GoogleDevelopersでも動きます。IDだけが必要ならpart=idを指定しましょう。コストは同じで、レスポンスが小さくなります。
curl -s "https://www.googleapis.com/youtube/v3/channels?part=id&forHandle=@GoogleDevelopers&key=$YOUTUBE_API_KEY"下のNodeの例では、リクエストを1つのヘルパーlistChannelsにまとめ、ガイドの残りでも再利用します。一致するものがなければ空のリストを返し、無効なキーや割り当て量の超過などの本当のエラーでは例外を投げます。
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
}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
クリエイターのプロフィールやスプレッドシートにある古いリンクは、ハンドル導入前の形式を使っていることがよくあります。
- `youtube.com/user/<name>`:上の
channelIdFromUsernameで、名前をforUsernameに渡します。ほかのchannels.listの呼び出しと同じく1ユニットです。 - `youtube.com/c/<name>`:カスタムURL用の
channels.listフィルタはありません。多くのチャンネルは古いカスタム名と同じハンドルを選んでいるので、同じ文字列でforHandleを試すとうまくいくことがありますが、一致しても同じチャンネルである証拠にはなりません。頼る前に、タイトルを確認したり、ページのcanonicalリンクを読んだり(後述)して、別の方法で確認しましょう。 - `search.list`はクエリに対する順位付きの結果を返すもので、完全一致の検索ではなく、デフォルトの上限は1日100回です。識別子の変換には使わないようにしましょう。
チャンネルIDからハンドルへ、50件ずつ
逆方向には、カンマ区切りのチャンネルIDのリストを受け取るidフィルタを使います。part=snippetを指定し、snippet.customUrlを読みます。上のchannelsByIdはIDの重複を除き、50件ずつ送り、IDをキーにしたマップを返します。マップに含まれないIDは、見つからなかったか、削除されたか、APIから見えないものです。
- 一括処理の件数。 リファレンスは
idをカンマ区切りのリストとし、maxResultsの上限を50としています。IDの数に別の上限は書かれていないので、コードは1回に50件を送り、デフォルトの5で一括処理が切り捨てられないようmaxResults=50を設定しています。 - 順序。 結果が送った順に返ってくるとは考えないでください。レスポンスは
idで索引付けします。 - `customUrl`は任意。 リファレンスはこれをチャンネルのカスタムURLとしか説明しておらず、2016年の改訂履歴には、すべてのチャンネルにあるわけではないと書かれています。null許容として扱い、先頭に
@があれば取り除き、これを前提に何か作る前に、知っているいくつかのチャンネルで形式を確認しましょう。 - タイトルはハンドルではない。
snippet.titleは表示名です。一意ではなく、スペースや絵文字を含むこともあります。
見つからない場合、エラー、キャッシュ
何も一致しない検索はエラーではありません。forHandleとforUsernameではレスポンスに項目がないだけなので、items配列が空か存在しないかを確認します。channels.listのドキュメントにあるエラーは次のとおりです。
| エラーの理由 | HTTP | 発生する場面 | 対応 |
|---|---|---|---|
200のレスポンスにitemsがない | 200 | ハンドルやユーザー名が存在しない | nullを返し、見つからなかった結果を短時間キャッシュする |
channelNotFound | 404 | idパラメータのチャンネルが見つからない | 上のヘルパーと同じく、見つからなかったものとして扱う |
invalidCriteria | 400 | リクエストに複数のフィルタがある | forHandle、forUsername、idのうち1つだけを送る |
channelForbidden | 403 | チャンネルがそのリクエストに対応していないか、適切に認可されていない | ログに残してそのチャンネルを飛ばす |
quotaExceeded | 403 | プロジェクトが1日の割り当て量を使い切った | 止めて太平洋時間の午前0時以降に再試行するか、割り当て量の追加を申請する |
badRequest(APIキーが無効) | 400 | キーがない、または間違っている | 設定を直す。再試行しない |
割り当て量と待ち時間を節約するために結果をキャッシュしましょう。ただし、データの保存に関するYouTubeのルールは守ってください。YouTube API Services Developer Policies(III.E.4項)によれば、認可されていないAPIデータを保存できるのは最長30暦日で、その後は削除するか更新する必要があります。小文字のハンドルをキーにし、TTLを30日より十分短くしたキャッシュで、効果の大半は得られます。
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/の名前を変換できる唯一の方法になります。
// 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;
}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では対応できない場合だけ使いましょう。非公式で壊れやすい方法です。
- マークアップはドキュメント化されたインターフェースではなく、いつでも変わる可能性があります。
- YouTubeは自動化されたリクエスト、特にクラウドのIP範囲からのものに、同意ページ、レート制限、CAPTCHAを返すことがあります。私たちのテストでは、クラウドサーバーから
youtu.beのリンクをたどると、GoogleのCAPTCHAページにたどり着きました。 - サイトへの自動アクセスはYouTubeの利用規約に抵触するおそれがあります。規約を読み、公式APIを優先してください。
方法の比較
| 方法 | 入力 | コスト | メリット | デメリット |
|---|---|---|---|---|
forHandleを使ったchannels.list | ハンドル(@の有無は問わない) | ハンドル1件につき1ユニット | 公式、完全一致、どのハンドルにも使える | 1回の呼び出しで1ハンドル |
forUsernameを使ったchannels.list | 旧形式の/user/の名前 | 名前1件につき1ユニット | 公式、完全一致 | 旧形式のユーザー名専用 |
idを使ったchannels.list | 最大50件のチャンネルID | ID50件につき1ユニット | 公式、一括処理、タイトルとcustomUrlを返す | customUrlがないことがある |
search.list | 自由なテキスト | デフォルトで1日100回の別枠の上限 | 名前でチャンネルを探せる | 完全一致ではなく順位付きの推測。上限が小さい |
| チャンネルページのcanonicalリンク | /c/を含むあらゆるチャンネルURL | APIの割り当て量を使わない | キーなしで使える。/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週間以下、見つからなかった結果をそれより短くすれば、十分にその範囲内に収まります。