المدونة / دليل المطوّرين
كيف تحصل على معرّف قناة 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. |
| اسم المستخدم القديم | 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راجع حصتك
في console، تُظهر صفحة Quotas الخاصة بالـ API حصتك اليومية واستخدامك.
الأرقام المهمة، من وثائق Google:
- استدعاء
channels.listيكلف وحدة واحدة، أيًا كان الفلتر الذي تستخدمه. - تحصل المشاريع على حصة افتراضية قدرها 10,000 وحدة يوميًا لكل نقاط النهاية ما عدا
search.listوvideos.insert، اللتين صار لكل منهما حد خاص قدره 100 استدعاء يوميًا. - كل طلب يكلف وحدة واحدة على الأقل، بما في ذلك الطلبات غير الصالحة.
- تُعاد الحصص اليومية عند منتصف الليل بتوقيت المحيط الهادئ.
إذن الحصة الافتراضية تغطي نحو 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مرّر أسماء المستخدمين كما كتبها المستخدم، بعد فك أي ترميز نسبة مئوية (percent-encoding) من الرابط. أسماء المستخدمين قد تستخدم أحرفًا من لغات كثيرة، لا ASCII فقط، وكل من URLSearchParams وrequests يرمّز المعامل نيابة عنك.
أسماء المستخدمين القديمة وروابط /c/ المخصصة
الروابط القديمة في سير صنّاع المحتوى وجداول البيانات تستخدم غالبًا إحدى الصيغ السابقة لأسماء المستخدمين الحالية.
- `youtube.com/user/<name>`: مرّر الاسم إلى
forUsernameعبرchannelIdFromUsernameأعلاه. يكلف وحدة واحدة مثل أي استدعاء آخر لـchannels.list. - `youtube.com/c/<name>`: لا يوجد فلتر في
channels.listللروابط المخصصة. كثير من القنوات اختارت اسم مستخدم يطابق اسمها المخصص القديم، لذلك قد تنجح تجربةforHandleبالنص نفسه، لكن التطابق ليس دليلًا على أنها القناة نفسها. أكّده بطريقة أخرى قبل أن تعتمد عليه، مثل التحقق من العنوان أو قراءة الرابط الموحّد (canonical) في الصفحة (موضح أدناه). - `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 | استهلك مشروعك حصته اليومية | توقف وأعد المحاولة بعد منتصف الليل بتوقيت المحيط الهادئ، أو اطلب حصة أكبر |
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، وامنح حالات عدم العثور مدة صلاحية أقصر من حالات العثور حتى يُلتقط اسم المستخدم المنشأ حديثًا بسرعة.
البديل دون مفتاح 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 المساعدة. فهو غير رسمي وهش:
- بنية الصفحة ليست واجهة موثقة ويمكن أن تتغير في أي وقت.
- قد يعرض 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 وحدة يوميًا، تُعاد عند منتصف الليل بتوقيت المحيط الهادئ. والطلبات غير الصالحة تكلف وحدة واحدة على الأقل أيضًا.
هل يمكنني البحث عن عدة أسماء مستخدمين في استدعاء واحد؟
لا. يأخذ forHandle اسم مستخدم واحدًا. فلتر id وحده يقبل قائمة مفصولة بفواصل، لذلك استخدم الدفعات في الاتجاه المعاكس (من المعرّفات إلى أسماء المستخدمين) وخزّن الاتجاه الأمامي مؤقتًا.
هل يتغير معرّف القناة أبدًا؟
معرّف القناة هو المعرّف الذي يستخدمه YouTube لتحديد القناة بشكل فريد. تغيير اسم المستخدم أو العنوان لا يغيّره. ولهذا هو المفتاح الأساسي الصحيح. أما أسماء المستخدمين والعناوين والروابط المخصصة فكلها قد تتغير.
لماذا يعود snippet.customUrl فارغًا؟
ليس لكل قناة رابط مخصص، كما يذكر سجل المراجعات الخاص بـ YouTube نفسه. تعامل مع الحقل كاختياري، واعرض رابط /channel/UC... بديلًا.
كم من الوقت يمكنني تخزين النتائج مؤقتًا؟
تحدّ Developer Policies الخاصة بـ YouTube حفظ بيانات API بـ 30 يومًا تقويميًا قبل أن تحذفها أو تحدّثها. مدة صلاحية أسبوع أو أقل لحالات العثور، وأقصر لحالات عدم العثور، تبقيك ضمن هذا الحد بفارق مريح.