בלוג / מדריך למפתחים
איך לקבל מזהה ערוץ YouTube משם משתמש (ולהפך)
יוצרים משתפים את ערוץ ה-YouTube שלהם כ-@handle, אבל כמעט הכול ב-YouTube Data API מבוסס על מזהה ערוץ. אם שומרים רק שמות משתמש, שינוי שם שובר את הרשומות; אם שומרים רק מזהים, אי אפשר להציג למשתמשים את השם שהם מכירים. המדריך הזה מראה איך להמיר שם משתמש למזהה ערוץ בקריאת API אחת, איך לחזור ממזהים לשמות משתמש 50 בכל פעם, מה לעשות עם כתובות ישנות של /user/ ו-/c/, ואיך להישאר בתוך מכסת ברירת המחדל.
שמות משתמש, מזהי ערוץ ושמות ישנים
אפשר להגיע לערוץ YouTube דרך עד ארבעה סוגי מזהים. רק אחד מהם מובטח כיציב.
| מזהה | דוגמה | צורת כתובת | הערות |
|---|---|---|---|
| מזהה ערוץ | UC_x5XG1OV2P6uZZ5FSM9Ttw | youtube.com/channel/UC... | המזהה ש-YouTube משתמשת בו כדי לזהות את הערוץ. רגיש לרישיות. השתמשו בו כמפתח הראשי. |
| שם משתמש (handle) | @GoogleDevelopers | youtube.com/@GoogleDevelopers | ייחודי, לא רגיש לרישיות, והבעלים יכול לשנות אותו ב-YouTube Studio. |
| שם משתמש ישן (legacy username) | GoogleDevelopers | youtube.com/user/GoogleDevelopers | ממערכת שמות המשתמש הישנה. אפשר להמיר אותו עם המסנן forUsername של ה-API. |
| כתובת מותאמת ישנה | GoogleDevelopers | youtube.com/c/GoogleDevelopers | כתובות מותאמות אישית ישנות, ש-YouTube אומרת שהן ממשיכות לעבוד. אין מסנן API שממיר אותן. |
שמרו את מזהה הערוץ כמפתח, ואת שם המשתמש כערך תצוגה שאתם מרעננים. שאר המדריך עוסק בדרך ממה שהמשתמש נתן לכם אל המזהה הזה.
הגדרת מפתח API והבנת המכסה
- Step 1צרו פרויקט ב-Google Cloud
ב-Google Cloud console, צרו פרויקט או בחרו פרויקט קיים.
- Step 2הפעילו את YouTube Data API v3
פתחו את APIs and services, מצאו את YouTube Data API v3 בספרייה והפעילו אותו בפרויקט.
- Step 3צרו מפתח API
תחת Credentials, צרו מפתח API. נתוני ערוץ ציבוריים כמו
channels.listלפי שם משתמש או מזהה צריכים רק מפתח, לא OAuth. הגבילו את המפתח ל-YouTube Data API. - Step 4השאירו את המפתח מחוץ לקוד
שימו אותו במשתנה סביבה, למשל
YOUTUBE_API_KEY, וקראו אותו בזמן ריצה. הדוגמאות שבהמשך עושות בדיוק את זה. - Step 5בדקו את המכסה
בקונסול, דף ה-Quotas של ה-API מציג את ההקצאה היומית ואת השימוש.
המספרים שחשובים, מתוך התיעוד של Google:
- קריאה ל-
channels.listעולה יחידה אחת, בלי קשר למסנן. - פרויקטים מקבלים הקצאת ברירת מחדל של 10,000 יחידות ליום לכל נקודות הקצה חוץ מ-
search.listו-videos.insert, שלכל אחת מהן יש עכשיו מגבלה משלה של 100 קריאות ליום. - כל בקשה עולה לפחות יחידה אחת, כולל בקשות לא תקינות.
- המכסות היומיות מתאפסות בחצות לפי שעון האוקיינוס השקט (Pacific Time).
כלומר מכסת ברירת המחדל מספיקה לכ-10,000 חיפושי שמות משתמש ביום, ועד פי 50 חיפושי מזהים, כי קריאה אחת יכולה להביא 50 ערוצים לפי מזהה באותה יחידה אחת.
משם משתמש למזהה ערוץ עם forHandle
channels.list מקבל מסנן אחד בדיוק. המסנן forHandle מקבל שם משתמש של YouTube, והתיעוד אומר שאפשר להוסיף לערך @ בהתחלה, כך שגם GoogleDevelopers וגם @GoogleDevelopers עובדים. בקשו part=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העבירו שמות משתמש כפי שהמשתמש כתב אותם, אחרי פענוח קידוד אחוזים מהכתובת. שמות משתמש יכולים לכלול אותיות מכתבים רבים, לא רק ASCII, וגם URLSearchParams וגם requests מקודדים את הפרמטר בשבילכם.
שמות משתמש ישנים וכתובות מותאמות /c/
קישורים ישנים בביו של יוצרים ובגיליונות משתמשים לעיתים קרובות באחת מהצורות שקדמו לשמות המשתמש.
- `youtube.com/user/<name>`: העבירו את השם ל-
forUsernameעםchannelIdFromUsernameשלמעלה. זה עולה יחידה אחת כמו כל קריאה אחרת ל-channels.list. - `youtube.com/c/<name>`: אין מסנן של
channels.listלכתובות מותאמות. ערוצים רבים בחרו שם משתמש שתואם לשם המותאם הישן שלהם, כך שניסיון שלforHandleעם אותה מחרוזת יכול לעבוד, אבל התאמה אינה הוכחה שזה אותו ערוץ. אשרו את זה בדרך אחרת לפני שאתם מסתמכים על זה, למשל על ידי בדיקת הכותרת או קריאת הקישור הקנוני של הדף (מתואר בהמשך). - `search.list` מחזיר תוצאות מדורגות לשאילתה, לא חיפוש מדויק, ומגבלת ברירת המחדל שלו היא 100 קריאות ליום. הימנעו ממנו להמרת מזהים.
ממזהה ערוץ לשם משתמש, 50 בכל פעם
הכיוון ההפוך משתמש במסנן id, שמקבל רשימה של מזהי ערוץ מופרדים בפסיקים. בקשו part=snippet וקראו את snippet.customUrl. הפונקציה channelsById שלמעלה מסירה כפילויות, שולחת את המזהים בחבילות של 50 ומחזירה מפה לפי מזהה. מזהים שלא מופיעים במפה לא נמצאו, נמחקו או לא גלויים ל-API.
- גודל אצווה. התיעוד מתאר את
idכרשימה מופרדת בפסיקים ומגביל אתmaxResultsל-50. הוא לא מציין מגבלה נפרדת למספר המזהים, ולכן הקוד שולח 50 בכל קריאה ומגדירmaxResults=50כדי שברירת המחדל של 5 לא תקטע אף אצווה. - סדר. אל תניחו שהתוצאות חוזרות בסדר ששלחתם. סדרו את התשובה לפי
id. - `customUrl` אופציונלי. התיעוד מתאר אותו רק ככתובת המותאמת של הערוץ, והערת גרסה מ-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 | הפרויקט ניצל את המכסה היומית | עצרו ונסו שוב אחרי חצות לפי Pacific Time, או בקשו מכסה נוספת |
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 שלהם. בקשה ל-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 לא יכול לעזור. זה לא רשמי ושביר:
- הסימון (markup) אינו ממשק מתועד ויכול להשתנות בכל רגע.
- YouTube יכולה להחזיר לבקשות אוטומטיות דפי הסכמה, הגבלת קצב או CAPTCHA, במיוחד מטווחי IP של ענן. בבדיקות שלנו, מעקב אחרי קישור
youtu.beמשרת ענן הסתיים בדף CAPTCHA של Google. - גישה אוטומטית לאתר עלולה להתנגש עם תנאי השימוש של YouTube. קראו אותם והעדיפו את ה-API הרשמי.
השוואה בין הגישות
| גישה | קלט | עלות | יתרונות | חסרונות |
|---|---|---|---|---|
channels.list עם forHandle | שם משתמש, עם או בלי @ | יחידה אחת לשם משתמש | רשמי, התאמה מדויקת, עובד לכל שם משתמש | שם משתמש אחד לקריאה |
channels.list עם forUsername | שם /user/ ישן | יחידה אחת לשם | רשמי, התאמה מדויקת | רק לשמות משתמש ישנים |
channels.list עם id | עד 50 מזהי ערוץ | יחידה אחת ל-50 מזהים | רשמי, באצוות, מחזיר כותרת ו-customUrl | customUrl יכול להיות חסר |
search.list | טקסט חופשי | מגבלה נפרדת של 100 קריאות ליום כברירת מחדל | מוצא ערוצים לפי שם | ניחושים מדורגים, לא מדויק; הקצאה קטנה |
| קישור קנוני בדף הערוץ | כל כתובת ערוץ, כולל /c/ | בלי מכסת API | עובד בלי מפתח; ממיר שמות /c/ | לא רשמי, שביר, עלול להיחסם, עלול להתנגש עם התנאים |
לרוב המוצרים ההגדרה הנכונה פשוטה: המירו שמות משתמש עם forHandle, שמרו את מזהה הערוץ, רעננו שמות משתמש באצוות של 50 עם המסנן id, והשאירו את החלופה של קריאת הדף מאחורי דגל למקרים הנדירים של קישור /c/. אם אתם צריכים גם את החשבונות של היוצר בפלטפורמות אחרות, sociallinks.dev הוא API שמקבל כתובת פרופיל אחת של Instagram, TikTok, YouTube או X ומחזיר את החשבונות של היוצר בפלטפורמות אחרות, עם ציון ביטחון והראיות לכל התאמה.
שאלות נפוצות
האם צריך את ה-@ כשמעבירים שם משתמש ל-forHandle?
לא. התיעוד של channels.list אומר שאפשר להוסיף @ לתחילת הערך של forHandle, ונותן גם את GoogleDevelopers וגם את @GoogleDevelopers כדוגמאות.
כמה מכסה צורך חיפוש של שם משתמש?
יחידה אחת. כל קריאה ל-channels.list עולה יחידה אחת, והקצאת ברירת המחדל היא 10,000 יחידות ליום, שמתאפסות בחצות לפי Pacific Time. גם בקשות לא תקינות עולות לפחות יחידה אחת.
אפשר לחפש כמה שמות משתמש בקריאה אחת?
לא. forHandle מקבל שם משתמש יחיד. רק המסנן id מקבל רשימה מופרדת בפסיקים, אז עבדו באצוות בכיוון ההפוך (ממזהים לשמות משתמש) ושמרו במטמון את הכיוון הרגיל.
האם מזהה ערוץ משתנה אי פעם?
מזהה הערוץ הוא המזהה ש-YouTube משתמשת בו כדי לזהות את הערוץ באופן ייחודי. שינוי שם המשתמש או הכותרת לא משנה אותו. בגלל זה הוא המפתח הראשי הנכון. שמות משתמש, כותרות וכתובות מותאמות יכולים כולם להשתנות.
למה snippet.customUrl חוזר ריק?
לא לכל ערוץ יש כתובת מותאמת, כמו שהיסטוריית הגרסאות של YouTube עצמה מציינת. התייחסו לשדה כאופציונלי, והציגו במקומו את כתובת ה-/channel/UC....
כמה זמן אפשר לשמור את התוצאות במטמון?
מדיניות המפתחים של YouTube מגבילה נתוני API שמורים ל-30 ימים קלנדריים לפני שצריך למחוק או לרענן אותם. TTL של שבוע או פחות לפגיעות, וקצר יותר להחטאות, משאיר אתכם הרבה בתוך הגבול.