Блог / Руководство для разработчиков
Как получить ID канала YouTube по никнейму (и наоборот)
Авторы делятся каналом на YouTube в виде @handle, но почти всё в YouTube Data API привязано к ID канала. Если хранить только никнеймы, переименование сломает записи; если хранить только ID, вы не сможете показать пользователям привычное имя. В этом руководстве мы показываем, как получить ID канала по никнейму одним вызовом API, как получить никнеймы по ID пачками по 50, что делать со старыми URL /user/ и /c/ и как уложиться в квоту по умолчанию.
Никнеймы, ID каналов и старые имена
Канал на YouTube можно открыть по четырём видам идентификаторов. Гарантированно постоянен только один из них.
| Идентификатор | Пример | Вид URL | Примечания |
|---|---|---|---|
| ID канала | UC_x5XG1OV2P6uZZ5FSM9Ttw | youtube.com/channel/UC... | ID, по которому YouTube идентифицирует канал. Чувствителен к регистру. Используйте его как первичный ключ. |
| Никнейм (handle) | @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 1Создайте проект Google Cloud
В консоли Google Cloud создайте проект или выберите существующий.
- Step 2Включите YouTube Data API v3
Откройте раздел APIs and services, найдите YouTube Data API v3 в библиотеке и включите его для проекта.
- Step 3Создайте ключ API
В разделе Credentials создайте ключ API. Для публичных данных каналов, таких как
channels.listпо никнейму или ID, нужен только ключ, без OAuth. Ограничьте ключ YouTube Data API. - Step 4Не храните ключ в коде
Положите его в переменную окружения, например
YOUTUBE_API_KEY, и читайте во время выполнения. Примеры ниже делают именно так. - Step 5Проверьте квоту
В консоли на странице Quotas для этого API видны ваш дневной лимит и расход.
Важные числа из документации Google:
- Вызов
channels.listстоит 1 единицу квоты, какой бы фильтр вы ни использовали. - Проекты по умолчанию получают 10 000 единиц в день на все методы, кроме
search.listиvideos.insert, у которых теперь свои лимиты: по 100 вызовов в день. - Каждый запрос стоит минимум 1 единицу, включая некорректные запросы.
- Дневные квоты сбрасываются в полночь по тихоокеанскому времени.
Значит, квоты по умолчанию хватает примерно на 10 000 запросов по никнейму в день и в 50 раз больше запросов по ID, потому что один вызов может получить 50 каналов по ID за ту же единицу.
Из никнейма в ID канала через forHandle
channels.list принимает ровно один фильтр. Фильтр forHandle принимает никнейм YouTube, и в справочнике сказано, что перед значением можно поставить символ @, поэтому работают и GoogleDevelopers, и @GoogleDevelopers. Запрашивайте part=id, если нужен только ID: ответ меньше, а стоимость та же.
curl -s "https://www.googleapis.com/youtube/v3/channels?part=id&forHandle=@GoogleDevelopers&key=$YOUTUBE_API_KEY"Пример на Node ниже оборачивает запрос во вспомогательную функцию 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Передавайте никнеймы так, как их написал пользователь, предварительно раскодировав percent-encoding из URL. В никнеймах могут быть буквы из многих алфавитов, не только ASCII, и URLSearchParams, и requests сами закодируют параметр.
Старые имена пользователей и персональные URL /c/
В старых ссылках в описаниях профилей и таблицах часто встречается один из форматов, существовавших до никнеймов.
- `youtube.com/user/<name>`: передайте имя в
forUsernameс помощьюchannelIdFromUsernameвыше. Это стоит 1 единицу, как любой другой вызовchannels.list. - `youtube.com/c/<name>`: у
channels.listнет фильтра для персональных URL. Многие каналы выбрали никнейм, совпадающий со старым персональным именем, поэтому попыткаforHandleс той же строкой может сработать, но совпадение не доказывает, что это тот же канал. Прежде чем на него полагаться, подтвердите его другим способом, например проверив название или прочитав канонический URL страницы (описано ниже). - `search.list` возвращает ранжированные результаты по запросу, а не точное совпадение, и по умолчанию его лимит 100 вызовов в день. Не используйте его для определения идентификаторов.
Из ID канала в никнейм, по 50 за раз
В обратную сторону используется фильтр id, который принимает список ID каналов через запятую. Запросите part=snippet и прочитайте snippet.customUrl. Функция channelsById выше убирает дубли ID, отправляет их пачками по 50 и возвращает словарь с ключами по ID. ID, которых нет в словаре, не найдены, удалены или недоступны для API.
- Размер пачки. Справочник описывает
idкак список через запятую и ограничиваетmaxResultsзначением 50. Отдельного лимита на число ID там нет, поэтому код отправляет 50 за вызов и задаётmaxResults=50, чтобы значение по умолчанию 5 никогда не обрезало пачку. - Порядок. Не рассчитывайте, что результаты придут в том же порядке, в каком вы их отправили. Индексируйте ответ по
id. - `customUrl` необязателен. Справочник описывает его только как персональный URL канала, а в заметке об изменениях 2016 года сказано, что он есть не у всех каналов. Считайте, что он может быть пустым, убирайте
@в начале, если он есть, и проверьте формат на нескольких известных вам каналах, прежде чем на него опираться. - Названия это не никнеймы.
snippet.titleэто отображаемое имя. Оно не уникально и может содержать пробелы и эмодзи.
Не найдено, ошибки и кеширование
Поиск, который ничего не нашёл, это не ошибка. Для forHandle и forUsername в ответе просто нет элементов, поэтому проверяйте, пуст ли массив items или его нет. Документированные ошибки channels.list:
| Причина ошибки | HTTP | Когда возникает | Что делать |
|---|---|---|---|
Нет items в ответе 200 | 200 | Никнейма или имени пользователя не существует | Вернуть null; ненадолго закешировать промах |
channelNotFound | 404 | Канал из параметра id не найден | Считать ненайденным, как делают функции выше |
invalidCriteria | 400 | В запросе больше одного фильтра | Отправлять ровно один из forHandle, forUsername или id |
channelForbidden | 403 | Канал не поддерживает запрос или запрос не авторизован должным образом | Записать в лог и пропустить канал |
quotaExceeded | 403 | Проект израсходовал дневную квоту | Остановиться и повторить после полуночи по тихоокеанскому времени или запросить больше квоты |
badRequest (API key not valid) | 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: чтение страницы канала
Публичные страницы каналов содержат ID канала в HTML. Запрос к 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 может отдавать автоматическим запросам страницы согласия, ограничения частоты или CAPTCHA, особенно с IP-адресов облачных провайдеров. В наших тестах переход по ссылке
youtu.beс облачного сервера закончился страницей Google с CAPTCHA. - Автоматический доступ к сайту может противоречить Условиям использования YouTube. Прочитайте их и отдавайте предпочтение официальному API.
Сравнение подходов
| Подход | Вход | Стоимость | Плюсы | Минусы |
|---|---|---|---|---|
channels.list с forHandle | Никнейм, с @ или без | 1 единица за никнейм | Официально, точное совпадение, работает для любого никнейма | Один никнейм за вызов |
channels.list с forUsername | Старое имя /user/ | 1 единица за имя | Официально, точное совпадение | Только для старых имён пользователей |
channels.list с id | До 50 ID каналов | 1 единица за 50 ID | Официально, пакетно, возвращает название и customUrl | customUrl может отсутствовать |
search.list | Произвольный текст | Отдельный лимит, по умолчанию 100 вызовов в день | Находит каналы по названию | Ранжированные догадки, а не точное совпадение; маленький лимит |
| Канонический URL на странице канала | Любой URL канала, включая /c/ | Без квоты API | Работает без ключа; определяет имена /c/ | Неофициально, ненадёжно, может блокироваться, может противоречить условиям |
Для большинства продуктов правильная схема проста: определяйте никнеймы через forHandle, храните ID канала, обновляйте никнеймы пачками по 50 через фильтр id, а запасной вариант со страницей держите за флагом для редких ссылок /c/. Если вам нужны и аккаунты автора на других платформах, sociallinks.dev это API, которое принимает один URL профиля в Instagram, TikTok, YouTube или X и возвращает аккаунты автора на других платформах с оценкой уверенности и доказательствами для каждого совпадения.
Частые вопросы
Нужен ли @, когда я передаю никнейм в forHandle?
Нет. В справочнике channels.list сказано, что перед значением forHandle можно поставить символ @, и в качестве примеров приведены и GoogleDevelopers, и @GoogleDevelopers.
Сколько квоты тратит поиск по никнейму?
Одну единицу. Каждый вызов channels.list стоит 1 единицу, а лимит по умолчанию 10 000 единиц в день со сбросом в полночь по тихоокеанскому времени. Некорректные запросы тоже стоят минимум 1 единицу.
Можно ли найти несколько никнеймов за один вызов?
Нет. forHandle принимает один никнейм. Список через запятую принимает только фильтр id, поэтому пакетно выполняйте обратное направление (из ID в никнеймы), а прямое кешируйте.
Меняется ли когда-нибудь ID канала?
ID канала это идентификатор, по которому YouTube однозначно определяет канал. Смена никнейма или названия его не меняет. Поэтому он и подходит как первичный ключ. Никнеймы, названия и персональные URL могут меняться.
Почему snippet.customUrl приходит пустым?
Персональный URL есть не у каждого канала, как отмечено в собственной истории изменений YouTube. Считайте поле необязательным и в его отсутствие показывайте URL /channel/UC....
Как долго можно кешировать результаты?
Developer Policies YouTube ограничивают хранение данных API 30 календарными днями, после чего их нужно удалить или обновить. TTL не больше недели для попаданий и короче для промахов оставляет хороший запас.