sociallinks.dev

Блог / Руководство для разработчиков

Как получить ID канала YouTube по никнейму (и наоборот)

Авторы делятся каналом на YouTube в виде @handle, но почти всё в YouTube Data API привязано к ID канала. Если хранить только никнеймы, переименование сломает записи; если хранить только ID, вы не сможете показать пользователям привычное имя. В этом руководстве мы показываем, как получить ID канала по никнейму одним вызовом API, как получить никнеймы по ID пачками по 50, что делать со старыми URL /user/ и /c/ и как уложиться в квоту по умолчанию.

Никнеймы, ID каналов и старые имена

Канал на YouTube можно открыть по четырём видам идентификаторов. Гарантированно постоянен только один из них.

ИдентификаторПримерВид URLПримечания
ID каналаUC_x5XG1OV2P6uZZ5FSM9Ttwyoutube.com/channel/UC...ID, по которому YouTube идентифицирует канал. Чувствителен к регистру. Используйте его как первичный ключ.
Никнейм (handle)@GoogleDevelopersyoutube.com/@GoogleDevelopersУникален, не зависит от регистра, владелец может изменить его в YouTube Studio.
Старое имя пользователяGoogleDevelopersyoutube.com/user/GoogleDevelopersИз старой системы имён пользователей. Определяется фильтром API forUsername.
Старый персональный URLGoogleDevelopersyoutube.com/c/GoogleDevelopersСтарые персональные URL, которые, по словам YouTube, продолжают работать. Ни один фильтр API их не определяет.

Храните ID канала как ключ, а никнейм как отображаемое значение, которое вы обновляете. Остальная часть руководства о том, как прийти к этому ID от того, что дал пользователь.

Настройте ключ API и разберитесь с квотой

  1. Step 1Создайте проект Google Cloud

    В консоли Google Cloud создайте проект или выберите существующий.

  2. Step 2Включите YouTube Data API v3

    Откройте раздел APIs and services, найдите YouTube Data API v3 в библиотеке и включите его для проекта.

  3. Step 3Создайте ключ API

    В разделе Credentials создайте ключ API. Для публичных данных каналов, таких как channels.list по никнейму или ID, нужен только ключ, без OAuth. Ограничьте ключ YouTube Data API.

  4. Step 4Не храните ключ в коде

    Положите его в переменную окружения, например YOUTUBE_API_KEY, и читайте во время выполнения. Примеры ниже делают именно так.

  5. Step 5Проверьте квоту

    В консоли на странице Quotas для этого API видны ваш дневной лимит и расход.

Важные числа из документации Google:

Значит, квоты по умолчанию хватает примерно на 10 000 запросов по никнейму в день и в 50 раз больше запросов по ID, потому что один вызов может получить 50 каналов по ID за ту же единицу.

Из никнейма в ID канала через forHandle

channels.list принимает ровно один фильтр. Фильтр forHandle принимает никнейм YouTube, и в справочнике сказано, что перед значением можно поставить символ @, поэтому работают и GoogleDevelopers, и @GoogleDevelopers. Запрашивайте part=id, если нужен только ID: ответ меньше, а стоимость та же.

cURL

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

Пример на Node ниже оборачивает запрос во вспомогательную функцию 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

Передавайте никнеймы так, как их написал пользователь, предварительно раскодировав percent-encoding из URL. В никнеймах могут быть буквы из многих алфавитов, не только ASCII, и URLSearchParams, и requests сами закодируют параметр.

Старые имена пользователей и персональные URL /c/

В старых ссылках в описаниях профилей и таблицах часто встречается один из форматов, существовавших до никнеймов.

Из ID канала в никнейм, по 50 за раз

В обратную сторону используется фильтр id, который принимает список ID каналов через запятую. Запросите part=snippet и прочитайте snippet.customUrl. Функция channelsById выше убирает дубли ID, отправляет их пачками по 50 и возвращает словарь с ключами по ID. ID, которых нет в словаре, не найдены, удалены или недоступны для API.

Не найдено, ошибки и кеширование

Поиск, который ничего не нашёл, это не ошибка. Для forHandle и forUsername в ответе просто нет элементов, поэтому проверяйте, пуст ли массив items или его нет. Документированные ошибки channels.list:

Причина ошибкиHTTPКогда возникаетЧто делать
Нет items в ответе 200200Никнейма или имени пользователя не существуетВернуть null; ненадолго закешировать промах
channelNotFound404Канал из параметра id не найденСчитать ненайденным, как делают функции выше
invalidCriteria400В запросе больше одного фильтраОтправлять ровно один из forHandle, forUsername или id
channelForbidden403Канал не поддерживает запрос или запрос не авторизован должным образомЗаписать в лог и пропустить канал
quotaExceeded403Проект израсходовал дневную квотуОстановиться и повторить после полуночи по тихоокеанскому времени или запросить больше квоты
badRequest (API key not valid)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: чтение страницы канала

Публичные страницы каналов содержат ID канала в HTML. Запрос к 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 не помогает. Способ неофициальный и ненадёжный:

Сравнение подходов

ПодходВходСтоимостьПлюсыМинусы
channels.list с forHandleНикнейм, с @ или без1 единица за никнеймОфициально, точное совпадение, работает для любого никнеймаОдин никнейм за вызов
channels.list с forUsernameСтарое имя /user/1 единица за имяОфициально, точное совпадениеТолько для старых имён пользователей
channels.list с idДо 50 ID каналов1 единица за 50 IDОфициально, пакетно, возвращает название и customUrlcustomUrl может отсутствовать
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 не больше недели для попаданий и короче для промахов оставляет хороший запас.

Найти аккаунты автора