博客 / 开发者指南
如何解析并规范化社交媒体主页链接(Instagram、TikTok、YouTube、X)
创作者粘贴的主页链接千奇百怪:带不带 www 的,从手机应用里复制、带追踪参数的,短链接,旧的 twitter.com 网址,甚至是帖子链接而不是主页。如果你的产品照原样存储这些字符串,同一位创作者就会出现三次,数据关联也会悄悄漏掉。本指南教你把任何 Instagram、TikTok、YouTube 或 X 主页链接转换为平台、账号名或频道 ID,以及一个规范链接,并提供经过测试的 JavaScript 和 Python 代码。
为什么要规范化主页链接
主页链接是一种标识符,而标识符只有写法统一时才管用。https://www.instagram.com/natgeo/、instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA== 和 https://m.instagram.com/natgeo 都指向同一个账号,但作为字符串,它们是三个不同的值。
- 去重。当一位创作者注册了两次,或两位同事把同一位达人加进了同一个活动时,你希望只有一条记录,而不是两条。
- 数据关联。把你的 CRM 和另一个工具的导出数据对比匹配,只有在两边使用同一个键时才行得通。
- 尽早拒绝错误输入。指向 Reel、搜索页或登录页的链接不是主页。在录入时就拦住,比事后清理便宜。
- 调用 API。官方 API 需要的是账号名或 ID,而不是带着追踪参数的网址。
目标是一个小而严格的函数:接收用户实际粘贴的内容,对真正的主页链接返回规范化结果,对其他一切返回 null。
你会遇到的网址形式
下面的网址形式已于 2026 年 10 月对照线上网站核实。平台会在没有通知的情况下改变路由,所以把它当作一份快照,并保持你的测试用例是最新的。
- 主页位于
https://www.instagram.com/<handle>/。裸域名instagram.com和m.instagram.com会重定向到www.instagram.com。 - 从应用中分享的链接常带有
?igsh=参数。把整个查询字符串和片段都去掉。 - 帖子是
/p/<code>/,Reel 是/reel/<code>/,快拍是/stories/<handle>/<id>/。即使路径里出现了账号名,这些也都不是主页链接。 /explore/、/accounts/和/direct/这类应用路由和账号名共用路径的第一段,所以你需要一份保留词列表。下面代码中的列表并不完整。
TikTok
- 主页是
https://www.tiktok.com/@<handle>。裸域名tiktok.com会重定向到www。 - 视频是
/@<handle>/video/<id>。账号名是作者,但这个网址是视频链接,所以下面的解析器会拒绝它。 vm.tiktok.com、vt.tiktok.com和www.tiktok.com/t/<code>/上的短链接完全不含账号名。你必须跟随 HTTP 重定向看它指向哪里,再解析最终的网址。- 不带
@的路径(例如tiktok.com/khaby.lame)不是主页链接。
YouTube
- 账号名网址:
https://www.youtube.com/@<handle>。YouTube 帮助中心说明,账号名会自动成为频道的网址。 - 频道 ID 网址:
https://www.youtube.com/channel/UC...。这是稳定的形式。实际中的频道 ID 是以UC开头的 24 个字符。Google 没有公开说明这个格式,所以下面的检查只是合理性范围,而不是规则。 - 旧式形式:
/user/<username>和/c/<customname>。两者目前仍然可以访问,帮助中心也表示旧式个性化网址会继续有效。但不经过查询,两者都无法告诉你频道 ID。 - 裸路径
youtube.com/<name>对一些频道也仍然有效,但它与网站路由共用命名空间,所以下面的解析器不接受它。 /@handle/videos和/channel/UC.../featured这类标签页属于频道本身,可以放心接受。m.youtube.com会重定向到www.youtube.com。视频链接(/watch?v=、/shorts/、youtu.be/<id>)不是频道。
X(原 Twitter)
- 主页是
https://x.com/<handle>。twitter.com、www.twitter.com、www.x.com、mobile.twitter.com和mobile.x.com都会重定向到x.com或twitter.com上的主页,所以把它们当作同一个平台。 - 帖子是
/<handle>/status/<id>。和 TikTok 视频一样,它们包含作者名,但不是主页链接。 /home、/explore、/search、/settings、/notifications、/messages这类应用路由以及/i/下的任何路径都不是账号名。- 分享链接常常附加
?s=和?t=参数。去掉它们。
各平台的账号名字符规则
校验账号名可以拦住拼写错误和大多数非主页路径。平台公布了规则的,就用平台自己的规则;没有公布的,就把检查放宽一些,免得误拒真实账号。
| 平台 | 允许的字符 | 长度 | 大小写 | 来源 |
|---|---|---|---|---|
| 字母、数字、句点、下划线 | 普遍说法是 30;Instagram 帮助页面中未找到 | 当作不区分大小写;以小写存储 | Instagram 帮助中心提到了句点、数字和下划线 | |
| TikTok | 字母、数字、下划线、句点;不能以句点结尾 | 2 到 24 | 网址中不区分大小写(@TikTok 解析为 tiktok) | TikTok 支持中心,更改用户名 |
| YouTube | 受支持语言的字母或数字,外加下划线、连字符、句点和拉丁中间点(不能在首尾) | 大多数文字为 3 到 30;汉字、韩文和其他部分文字限制更短 | 不区分大小写 | YouTube 帮助,账号名准则 |
| X | 字母、数字和下划线 | 最多 15;新用户名必须多于 4 个字符,但存在较早的短账号名 | 不区分大小写(x.com/JACK 会规范为 x.com/jack) | X 帮助中心,用户名规则 |
规范化算法
- Step 1去空格并补上协议
去掉空白字符和多余的尖括号。如果输入没有协议,就在前面加上
https://,这样instagram.com/natgeo也能被解析。 - Step 2用真正的网址解析器解析
在 JavaScript 中用
new URL(),在 Python 中用urllib.parse.urlsplit,绝不要对整个字符串套正则表达式。解析器能处理端口、用户信息和编码字符,还能防止https://instagram.com@evil.com/natgeo冒充 Instagram 蒙混过关。 - Step 3规范化主机名
把它转成小写,去掉开头的一个
www.、m.或mobile.。然后在精确的主机白名单中查找。绝不要用includes("instagram.com"),它也会匹配instagram.com.evil.com。 - Step 4去掉查询字符串和片段
igsh、s、t和lang这类追踪参数永远不会改变网址指向的主页。 - Step 5拆分并解码路径
按
/拆分,丢掉空段(这样就去掉了结尾斜杠),并对每一段做百分号解码。编码格式错误的视为无效输入。 - Step 6按平台分流
套用每个平台的形式:Instagram 和 X 是一段,TikTok 是带
@的一段,YouTube 是@handle、channel/UC...、user/name或c/name。拒绝保留词。 - Step 7校验账号名并转成小写
按平台的字符规则检查账号名,然后转成小写。不要把 YouTube 频道 ID 转成小写:它们区分大小写。
- Step 8生成规范链接
在一个固定的主机名上用各部分重建网址,这样所有等价的输入都会得到完全相同的字符串。
- Step 9单独处理短链接
识别 TikTok 短链接,用 HTTP 请求跟随重定向,再把最终网址交回解析器。把这一步放在纯函数之外,让解析保持快速、可测试。
JavaScript 和 Python 代码
对于相同的输入,两个函数返回相同的对象:账号名网址返回 { platform, handle, canonicalUrl },YouTube 频道 ID 返回 { platform, channelId, canonicalUrl },两种旧式 YouTube 形式分别返回 username 或 customName,任何不是主页链接的输入都返回 null。两者都不需要任何依赖。
JavaScript(Node 18+)
const HOSTS = {
"instagram.com": "instagram",
"tiktok.com": "tiktok",
"youtube.com": "youtube",
"x.com": "x",
"twitter.com": "x",
};
const IG_RESERVED = new Set(["p", "reel", "reels", "tv", "stories", "explore", "accounts", "direct"]);
const X_RESERVED = new Set([
"home", "explore", "search", "i", "settings", "notifications", "messages",
"compose", "intent", "hashtag", "login", "signup", "logout", "tos", "privacy",
]);
const IG_HANDLE = /^[a-z0-9._]{1,30}$/;
const TIKTOK_HANDLE = /^[a-z0-9._]{2,24}$/;
const X_HANDLE = /^[a-z0-9_]{1,15}$/;
const YT_HANDLE = /^[\p{L}\p{M}\p{N}._\-·]{1,30}$/u;
const YT_CHANNEL_ID = /^UC[A-Za-z0-9_-]{22}$/;
const YT_LEGACY_NAME = /^[A-Za-z0-9._-]{1,100}$/;
export function parseProfileUrl(input) {
if (typeof input !== "string") return null;
let raw = input.trim().replace(/^<|>$/g, "");
if (!raw) return null;
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) raw = "https://" + raw;
let url;
try {
url = new URL(raw);
} catch {
return null;
}
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
const host = url.hostname.toLowerCase().replace(/^(www|m|mobile)\./, "");
const platform = HOSTS[host];
if (!platform) return null;
let segments;
try {
segments = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
} catch {
return null; // malformed percent-encoding
}
const [first = "", second] = segments;
if (platform === "instagram") {
const handle = first.toLowerCase();
if (segments.length !== 1 || IG_RESERVED.has(handle) || !IG_HANDLE.test(handle)) return null;
return { platform, handle, canonicalUrl: `https://www.instagram.com/${handle}/` };
}
if (platform === "tiktok") {
if (segments.length !== 1 || !first.startsWith("@")) return null;
const handle = first.slice(1).toLowerCase();
if (!TIKTOK_HANDLE.test(handle) || handle.endsWith(".")) return null;
return { platform, handle, canonicalUrl: `https://www.tiktok.com/@${handle}` };
}
if (platform === "x") {
const handle = first.toLowerCase();
if (segments.length !== 1 || X_RESERVED.has(handle) || !X_HANDLE.test(handle)) return null;
return { platform, handle, canonicalUrl: `https://x.com/${handle}` };
}
// YouTube: /@handle[/tab], /channel/UC...[/tab], /user/name, /c/name
if (first.startsWith("@")) {
const handle = first.slice(1).toLowerCase();
if (!YT_HANDLE.test(handle) || /^[._\-·]|[._\-·]$/.test(handle)) return null;
return { platform, handle, canonicalUrl: `https://www.youtube.com/@${encodeURIComponent(handle)}` };
}
if (first === "channel" && second && YT_CHANNEL_ID.test(second)) {
return { platform, channelId: second, canonicalUrl: `https://www.youtube.com/channel/${second}` };
}
if ((first === "user" || first === "c") && second && segments.length === 2 && YT_LEGACY_NAME.test(second)) {
const key = first === "user" ? "username" : "customName";
return { platform, [key]: second, canonicalUrl: `https://www.youtube.com/${first}/${second}` };
}
return null; // videos, shorts, playlists, search and anything else
}
const SHORT_HOSTS = new Set(["vm.tiktok.com", "vt.tiktok.com"]);
export function isShortLink(input) {
try {
const url = new URL(/^https?:\/\//i.test(input) ? input : "https://" + input);
const host = url.hostname.toLowerCase();
return SHORT_HOSTS.has(host) || (host.replace(/^www\./, "") === "tiktok.com" && url.pathname.startsWith("/t/"));
} catch {
return false;
}
}
export async function resolveShortLink(input) {
const res = await fetch(/^https?:\/\//i.test(input) ? input : "https://" + input, {
redirect: "follow",
signal: AbortSignal.timeout(10_000),
});
await res.body?.cancel();
return res.url; // the final URL after every redirect
}resolveShortLink 使用内置的 fetch,它会跟随重定向,并通过 res.url 给出最终地址。当 isShortLink(link) 为 true 时,调用 parseProfileUrl(await resolveShortLink(link))。短链接也可能指向视频而不是主页,这时解析器会正确地返回 null。
Python 3
import re
import unicodedata
import urllib.request
from urllib.parse import quote, unquote, urlsplit
HOSTS = {
"instagram.com": "instagram",
"tiktok.com": "tiktok",
"youtube.com": "youtube",
"x.com": "x",
"twitter.com": "x",
}
IG_RESERVED = {"p", "reel", "reels", "tv", "stories", "explore", "accounts", "direct"}
X_RESERVED = {
"home", "explore", "search", "i", "settings", "notifications", "messages",
"compose", "intent", "hashtag", "login", "signup", "logout", "tos", "privacy",
}
IG_HANDLE = re.compile(r"[a-z0-9._]{1,30}")
TIKTOK_HANDLE = re.compile(r"[a-z0-9._]{2,24}")
X_HANDLE = re.compile(r"[a-z0-9_]{1,15}")
YT_CHANNEL_ID = re.compile(r"UC[A-Za-z0-9_-]{22}")
YT_LEGACY_NAME = re.compile(r"[A-Za-z0-9._-]{1,100}")
YT_PUNCT = "._-·"
def _is_youtube_handle(handle):
if not 1 <= len(handle) <= 30 or handle[0] in YT_PUNCT or handle[-1] in YT_PUNCT:
return False
return all(
ch.isalnum() or ch in YT_PUNCT or unicodedata.category(ch).startswith("M")
for ch in handle
)
def parse_profile_url(value):
if not isinstance(value, str):
return None
raw = value.strip().strip("<>")
if not raw:
return None
if not re.match(r"^[a-z][a-z0-9+.-]*://", raw, re.I):
raw = "https://" + raw
try:
url = urlsplit(raw)
host = (url.hostname or "").lower()
except ValueError:
return None
if url.scheme.lower() not in ("http", "https"):
return None
host = re.sub(r"^(www|m|mobile)\.", "", host)
platform = HOSTS.get(host)
if not platform:
return None
try:
segments = [unquote(s, errors="strict") for s in url.path.split("/") if s]
except UnicodeDecodeError:
return None # malformed percent-encoding
first = segments[0] if segments else ""
second = segments[1] if len(segments) > 1 else None
if platform == "instagram":
handle = first.lower()
if len(segments) != 1 or handle in IG_RESERVED or not IG_HANDLE.fullmatch(handle):
return None
return {"platform": platform, "handle": handle,
"canonicalUrl": f"https://www.instagram.com/{handle}/"}
if platform == "tiktok":
if len(segments) != 1 or not first.startswith("@"):
return None
handle = first[1:].lower()
if not TIKTOK_HANDLE.fullmatch(handle) or handle.endswith("."):
return None
return {"platform": platform, "handle": handle,
"canonicalUrl": f"https://www.tiktok.com/@{handle}"}
if platform == "x":
handle = first.lower()
if len(segments) != 1 or handle in X_RESERVED or not X_HANDLE.fullmatch(handle):
return None
return {"platform": platform, "handle": handle, "canonicalUrl": f"https://x.com/{handle}"}
# YouTube: /@handle[/tab], /channel/UC...[/tab], /user/name, /c/name
if first.startswith("@"):
handle = first[1:].lower()
if not _is_youtube_handle(handle):
return None
return {"platform": platform, "handle": handle,
"canonicalUrl": f"https://www.youtube.com/@{quote(handle, safe='._-')}"}
if first == "channel" and second and YT_CHANNEL_ID.fullmatch(second):
return {"platform": platform, "channelId": second,
"canonicalUrl": f"https://www.youtube.com/channel/{second}"}
if first in ("user", "c") and second and len(segments) == 2 and YT_LEGACY_NAME.fullmatch(second):
key = "username" if first == "user" else "customName"
return {"platform": platform, key: second,
"canonicalUrl": f"https://www.youtube.com/{first}/{second}"}
return None # videos, shorts, playlists, search and anything else
SHORT_HOSTS = {"vm.tiktok.com", "vt.tiktok.com"}
def is_short_link(value):
url = urlsplit(value if re.match(r"^https?://", value, re.I) else "https://" + value)
host = (url.hostname or "").lower()
return host in SHORT_HOSTS or (
re.sub(r"^www\.", "", host) == "tiktok.com" and url.path.startswith("/t/")
)
def resolve_short_link(value, timeout=10):
if not re.match(r"^https?://", value, re.I):
value = "https://" + value
request = urllib.request.Request(value, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(request, timeout=timeout) as response:
return response.geturl() # the final URL after every redirectPython 的 \w 和 str.isalnum() 覆盖了 Unicode 字母和数字,但不包括天城文等文字使用的组合符号,所以 YouTube 的检查还接受 Unicode M 类别中的字符。
边界情况和测试
下表中的每一行都属于测试套件,两种实现都能通过。
| 输入 | 输出 | 原因 |
|---|---|---|
instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA== | instagram、natgeo、https://www.instagram.com/natgeo/ | 没有协议,大小写混合,带分享参数 |
https://m.instagram.com/nat.geo_/#bio | instagram、nat.geo_ | 移动版主机名和片段 |
https://www.instagram.com/p/C9xYzAbCdEf/ | null | 是帖子,不是主页 |
https://www.instagram.com/stories/natgeo/1234567890/ | null | 是快拍,尽管路径里有账号名 |
tiktok.com/@Khaby.Lame/ | tiktok、khaby.lame、https://www.tiktok.com/@khaby.lame | 结尾斜杠和大小写 |
https://www.tiktok.com/@khaby.lame/video/7106594312292453675 | null | 视频网址 |
https://vm.tiktok.com/ZMabc123/ | null(先跟随重定向) | 短链接里没有账号名 |
https://m.youtube.com/@GoogleDevelopers/videos?app=desktop | youtube、googledevelopers | 移动版主机名、频道标签页、查询参数 |
https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw/featured | youtube、channelId UC_x5XG1OV2P6uZZ5FSM9Ttw | 频道 ID 保留大小写 |
https://www.youtube.com/c/GoogleDevelopers | youtube、customName GoogleDevelopers | 旧式自定义网址;需要查询才能得到 ID |
https://www.youtube.com/@%E3%83%86%E3%82%B9%E3%83%88 | youtube、账号名 テスト | 百分号编码的 Unicode 账号名 |
https://youtu.be/dQw4w9WgXcQ | null | 是视频,不是频道 |
https://mobile.twitter.com/jack?s=21&t=abc | x、jack、https://x.com/jack | 旧域名和分享参数 |
https://x.com/i/flow/login | null | 应用路由 |
https://x.com/this_handle_is_too_long | null | 超过 15 个字符 |
https://instagram.com.evil.com/natgeo | null | 仿冒主机名 |
https://instagram.com@evil.com/natgeo | null | 用户信息伎俩;真正的主机是 evil.com |
@natgeo | null | 单独的账号名没有平台信息 |
把这些用例放进你的代码仓库,并在 CI 中运行。当某个平台新增路由或分享参数时,你只需加一行,修复方法一目了然。
import assert from "node:assert/strict";
import { parseProfileUrl } from "./parse.mjs";
const cases = [
["instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA==", { platform: "instagram", handle: "natgeo", canonicalUrl: "https://www.instagram.com/natgeo/" }],
["https://www.instagram.com/reel/C9xYzAbCdEf/", null],
["https://www.instagram.com/explore/", null],
["tiktok.com/@Khaby.Lame/", { platform: "tiktok", handle: "khaby.lame", canonicalUrl: "https://www.tiktok.com/@khaby.lame" }],
["https://www.tiktok.com/@khaby.lame/video/7106594312292453675", null],
["https://vm.tiktok.com/ZMabc123/", null],
["https://m.youtube.com/@GoogleDevelopers/videos", { platform: "youtube", handle: "googledevelopers", canonicalUrl: "https://www.youtube.com/@googledevelopers" }],
["https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw/featured", { platform: "youtube", channelId: "UC_x5XG1OV2P6uZZ5FSM9Ttw", canonicalUrl: "https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw" }],
["https://www.youtube.com/user/GoogleDevelopers", { platform: "youtube", username: "GoogleDevelopers", canonicalUrl: "https://www.youtube.com/user/GoogleDevelopers" }],
["https://youtu.be/dQw4w9WgXcQ", null],
["https://twitter.com/Jack/", { platform: "x", handle: "jack", canonicalUrl: "https://x.com/jack" }],
["https://mobile.twitter.com/jack?s=21", { platform: "x", handle: "jack", canonicalUrl: "https://x.com/jack" }],
["https://x.com/jack/status/20", null],
["https://x.com/i/flow/login", null],
["https://instagram.com.evil.com/natgeo", null],
];
for (const [input, expected] of cases) {
assert.deepEqual(parseProfileUrl(input), expected, input);
}
console.log(`${cases.length} cases passed`);from parse_profile import parse_profile_url
CASES = [
("instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA==", {"platform": "instagram", "handle": "natgeo", "canonicalUrl": "https://www.instagram.com/natgeo/"}),
("https://www.instagram.com/reel/C9xYzAbCdEf/", None),
("tiktok.com/@Khaby.Lame/", {"platform": "tiktok", "handle": "khaby.lame", "canonicalUrl": "https://www.tiktok.com/@khaby.lame"}),
("https://vm.tiktok.com/ZMabc123/", None),
("https://m.youtube.com/@GoogleDevelopers/videos", {"platform": "youtube", "handle": "googledevelopers", "canonicalUrl": "https://www.youtube.com/@googledevelopers"}),
("https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw/featured", {"platform": "youtube", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "canonicalUrl": "https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw"}),
("https://youtu.be/dQw4w9WgXcQ", None),
("https://twitter.com/Jack/", {"platform": "x", "handle": "jack", "canonicalUrl": "https://x.com/jack"}),
("https://x.com/i/flow/login", None),
("https://instagram.com.evil.com/natgeo", None),
]
for value, expected in CASES:
assert parse_profile_url(value) == expected, value
print(f"{len(CASES)} cases passed")同时存储原始输入和规范形式
规范化在设计上就会丢失信息,而且你的规则会随时间不断改进。两个值都要存:
- `raw_url`:用户或导入数据给你的原样内容。它让你以后能重新运行更好的解析器,用户编辑主页时也是把它显示回去。
- `platform` 以及 `handle` 或 `channel_id`:拆分出的各部分,并在
(platform, handle)上建唯一索引。这是你去重和关联的键。 - `canonical_url`:重建后的网址,用于显示和调用平台。
- `parsed_at` 和解析器版本:方便你找出用旧版规则规范化的记录。
账号名不是永久的。创作者在每个平台上都会改名,你去年存下的账号名现在可能已经属于别人。如果官方 API 能给你一个稳定的 ID(例如 YouTube 频道 ID),就把它存为长期键,把账号名当作需要定期刷新的显示值。配套指南从账号名获取 YouTube 频道 ID介绍了具体做法。
从一个主页到其他主页
主页链接规范化之后,下一个问题通常是:同一位创作者还有哪些其他账号?sociallinks.dev 是一个 API,接收一个 Instagram、TikTok、YouTube 或 X 主页链接,返回该创作者在其他平台上的账号,并为每个匹配给出置信度分数和证据。
常见问题
存储账号名之前应该转成小写吗?
作为查询键,应该。YouTube 和 X 都说明账号名不区分大小写,TikTok 也会把 @TikTok 解析为和 @tiktok 相同的账号。Instagram 没有公布大小写规则,但用户名以小写显示,所以转小写是稳妥的默认做法。如果想显示原始大小写,就保留在原始网址里。不要把 YouTube 频道 ID 转成小写,它们区分大小写。
能从帖子或视频网址中提取账号名吗?
对于 TikTok 视频(/@handle/video/id)和 X 帖子(/handle/status/id),路径中的账号名就是作者,如果你需要的就是作者,可以提取。Instagram 的帖子和 Reel 网址(/p/ 和 /reel/)不包含账号名。本指南中的函数对这些网址一律返回 null,因为它们不是主页链接。
vm.tiktok.com 链接怎么处理?
短链接只包含一个代码。发起 HTTP 请求,跟随重定向,再解析最终的网址。把这一步单独处理,并设置超时和缓存,因为它是一次网络调用,而且代码可能会过期。
twitter.com 还是有效的主页链接吗?
是的。twitter.com 的主页链接会重定向到 x.com,所以把两个域名都当作 X 平台,并以 x.com 形式作为规范链接存储。
为什么不用一个大正则表达式?
对整个字符串套一个正则表达式,既难读,又很容易被仿冒主机名、用户信息或编码字符骗过。先用标准库解析网址,再只对账号名使用小的正则表达式。
怎样把 YouTube 的 /c/ 或 /user/ 网址转换为频道 ID?
/user/<name> 网址对应 YouTube Data API 的 forUsername 筛选条件。/c/ 自定义名称没有对应的 API 筛选条件。两种情况都可以参考“从账号名获取 YouTube 频道 ID”那篇指南。