블로그 / 개발자 가이드
소셜 미디어 프로필 URL을 파싱하고 정규화하는 방법(Instagram, TikTok, YouTube, X)
크리에이터는 상상할 수 있는 모든 형태로 프로필 링크를 붙여 넣습니다. www가 있거나 없고, 모바일 앱에서 추적 파라미터가 붙어 오고, 단축 링크이거나, 예전 twitter.com URL이거나, 프로필이 아니라 게시물 링크이기도 합니다. 제품이 이런 문자열을 들어온 그대로 저장하면 같은 크리에이터가 세 번 나타나고 조인은 조용히 빗나갑니다. 이 가이드는 Instagram, TikTok, YouTube, X 프로필 URL을 플랫폼, 핸들 또는 채널 ID, 그리고 하나의 정규 URL로 바꾸는 방법을 검증된 JavaScript와 Python 코드로 보여 드립니다.
프로필 URL 정규화가 중요한 이유
프로필 URL은 식별자이고, 식별자는 한 가지 방식으로 적혀 있을 때만 제 역할을 합니다. https://www.instagram.com/natgeo/, instagram.com/NatGeo?igsh=MWQ1ZGUxMzBkMA==, https://m.instagram.com/natgeo는 모두 같은 계정을 가리키지만 문자열로는 서로 다른 세 값입니다.
- 중복 제거. 크리에이터가 두 번 가입하거나 팀원 두 명이 같은 인플루언서를 캠페인에 추가하면, 레코드는 둘이 아니라 하나여야 합니다.
- 조인. CRM과 다른 도구의 내보내기 파일을 대조하는 것은 양쪽이 같은 키를 쓸 때만 가능합니다.
- 잘못된 입력을 일찍 거부. 릴스, 검색 페이지, 로그인 화면으로 가는 링크는 프로필이 아닙니다. 입력 시점에 걸러 내는 것이 나중에 정리하는 것보다 저렴합니다.
- API 호출. 공식 API는 추적 파라미터가 붙은 URL이 아니라 핸들이나 ID를 원합니다.
목표는 작고 엄격한 함수입니다. 사용자가 실제로 붙여 넣는 것을 받아들이고, 실제 프로필 URL에는 정규화된 결과를 반환하고, 나머지는 모두 null을 반환합니다.
마주치게 될 URL 형태
아래 형태는 2026년 10월에 실제 사이트에서 확인했습니다. 플랫폼은 예고 없이 라우팅을 바꾸므로 이 내용은 한 시점의 기록으로 보고 테스트 케이스를 최신으로 유지하세요.
- 프로필은
https://www.instagram.com/<handle>/에 있습니다. 서브도메인 없는instagram.com과m.instagram.com호스트는www.instagram.com으로 리디렉션됩니다. - 앱에서 공유한 링크에는
?igsh=파라미터가 붙는 경우가 많습니다. 쿼리 문자열 전체와 프래그먼트를 버리세요. - 게시물은
/p/<code>/, 릴스는/reel/<code>/, 스토리는/stories/<handle>/<id>/입니다. 경로에 핸들이 들어 있더라도 이 중 어느 것도 프로필 URL이 아닙니다. /explore/,/accounts/,/direct/같은 앱 경로는 첫 번째 경로 세그먼트를 핸들과 공유하므로 예약어 목록이 필요합니다. 아래 코드의 목록이 전부는 아닙니다.
TikTok
- 프로필은
https://www.tiktok.com/@<handle>입니다. 서브도메인 없는tiktok.com호스트는www로 리디렉션됩니다. - 영상은
/@<handle>/video/<id>입니다. 핸들은 작성자이지만 URL은 영상 링크이므로 아래 파서는 이를 거부합니다. vm.tiktok.com,vt.tiktok.com,www.tiktok.com/t/<code>/의 단축 링크에는 핸들이 전혀 없습니다. HTTP 리디렉션을 따라가 어디로 가는지 확인한 뒤 최종 URL을 파싱해야 합니다.@가 없는 경로(예:tiktok.com/khaby.lame)는 프로필 URL이 아닙니다.
YouTube
- 핸들 URL:
https://www.youtube.com/@<handle>. YouTube 고객센터는 핸들이 자동으로 채널 URL이 된다고 설명합니다. - 채널 ID URL:
https://www.youtube.com/channel/UC.... 이것이 안정적인 형식입니다. 실제 채널 ID는UC로 시작하는 24자입니다. Google이 형식을 문서화하지 않았으므로 아래의 검사는 규칙이 아니라 상식적인 범위 확인입니다. - 예전 형식:
/user/<username>과/c/<customname>. 둘 다 지금도 연결되며, 고객센터는 예전 맞춤 URL이 계속 작동한다고 밝힙니다. 둘 다 조회 없이는 채널 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 영상처럼 작성자 이름이 들어 있지만 프로필 URL은 아닙니다. /home,/explore,/search,/settings,/notifications,/messages같은 앱 경로와/i/아래의 모든 경로는 핸들이 아닙니다.- 공유 링크에는
?s=와?t=파라미터가 자주 붙습니다. 버리세요.
플랫폼별 핸들 문자 규칙
핸들을 검증하면 오타와 대부분의 프로필이 아닌 경로를 걸러 낼 수 있습니다. 플랫폼이 규칙을 공개한 곳에서는 그 규칙을 쓰고, 공개하지 않은 곳에서는 실제 계정을 거부하는 일이 없도록 검사를 느슨하게 두세요.
| 플랫폼 | 허용 문자 | 길이 | 대소문자 | 출처 |
|---|---|---|---|---|
| 문자, 숫자, 마침표, 밑줄 | 30자가 널리 언급되지만 Instagram 고객센터에서는 찾지 못함 | 대소문자 구분 없음으로 취급하고 소문자로 저장 | Instagram 고객센터가 마침표, 숫자, 밑줄을 언급 | |
| TikTok | 문자, 숫자, 밑줄, 마침표. 마침표로 끝날 수 없음 | 2~24자 | URL에서 대소문자 구분 없음(@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실제 URL 파서로 파싱
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같은 추적 파라미터는 URL이 가리키는 프로필을 바꾸지 않습니다. - Step 5경로 분리와 디코딩
/로 나누고, 빈 세그먼트를 버리고(끝 슬래시가 제거됩니다), 각 세그먼트를 퍼센트 디코딩하세요. 잘못된 인코딩은 유효하지 않은 입력으로 처리하세요. - Step 6플랫폼별 분기
각 플랫폼의 형태를 적용하세요. Instagram과 X는 세그먼트 하나, TikTok은
@세그먼트, YouTube는@handle,channel/UC...,user/name,c/name입니다. 예약어는 거부하세요. - Step 7핸들 검증과 소문자 변환
핸들을 플랫폼의 문자 규칙으로 확인한 뒤 소문자로 바꾸세요. YouTube 채널 ID는 대소문자를 구분하므로 소문자로 바꾸지 마세요.
- Step 8정규 URL 만들기
고정된 호스트 하나에서 각 부분으로 URL을 다시 만들어, 동등한 입력이 모두 정확히 같은 문자열을 내도록 하세요.
- Step 9단축 링크는 따로 처리
TikTok 단축 링크를 감지하고, HTTP 요청으로 리디렉션을 따라간 뒤, 최종 URL을 다시 파서에 넣으세요. 파싱이 빠르고 테스트하기 쉽도록 이 처리는 순수 함수 밖에 두세요.
JavaScript와 Python 코드
두 함수는 같은 입력에 같은 객체를 반환합니다. 핸들 URL에는 { platform, handle, canonicalUrl }, YouTube 채널 ID에는 { platform, channelId, canonicalUrl }, 예전 YouTube 형식 두 가지에는 username 또는 customName, 프로필 URL이 아닌 것에는 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()은 유니코드 문자와 숫자는 포함하지만 데바나가리 같은 문자 체계에서 쓰는 결합 기호는 포함하지 않습니다. 그래서 YouTube 검사는 유니코드 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 | 영상 URL |
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 | 예전 맞춤 URL. ID를 얻으려면 조회 필요 |
https://www.youtube.com/@%E3%83%86%E3%82%B9%E3%83%88 | youtube, handle テスト | 퍼센트 인코딩된 유니코드 핸들 |
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`: 표시와 플랫폼 호출에 쓰는 다시 만든 URL.
- `parsed_at`과 파서 버전: 예전 버전 규칙으로 정규화된 행을 찾을 수 있도록.
핸들은 영구적이지 않습니다. 크리에이터는 모든 플랫폼에서 계정 이름을 바꾸며, 작년에 저장한 핸들이 지금은 다른 사람의 것일 수 있습니다. YouTube 채널 ID처럼 공식 API가 안정적인 ID를 준다면 그것을 장기 키로 저장하고, 핸들은 주기적으로 갱신하는 표시용 값으로 다루세요. 관련 가이드인 핸들로 YouTube 채널 ID 얻기에서 방법을 보여 드립니다.
프로필 하나에서 다른 프로필로
프로필 URL을 정규화하고 나면, 다음 질문은 보통 같은 크리에이터의 다른 계정이 무엇이냐는 것입니다. sociallinks.dev는 Instagram, TikTok, YouTube, X 프로필 URL 하나를 받아 다른 플랫폼의 크리에이터 계정을 일치마다 신뢰도 점수와 근거와 함께 반환하는 API입니다.
자주 묻는 질문
핸들을 저장하기 전에 소문자로 바꿔야 하나요?
조회 키로는 그렇습니다. YouTube와 X는 핸들이 대소문자를 구분하지 않는다고 밝히고, TikTok은 @TikTok을 @tiktok과 같은 계정으로 연결합니다. Instagram은 대소문자 규칙을 문서화하지 않았지만 사용자 이름이 소문자로 표시되므로 소문자 변환이 안전한 기본값입니다. 원래 대소문자로 보여 주고 싶다면 원본 URL에 남겨 두세요. 대소문자를 구분하는 YouTube 채널 ID는 소문자로 바꾸지 마세요.
게시물이나 영상 URL에서 핸들을 추출할 수 있나요?
TikTok 영상(/@handle/video/id)과 X 게시물(/handle/status/id)은 경로의 핸들이 작성자이므로 원한다면 추출할 수 있습니다. Instagram 게시물과 릴스 URL(/p/와 /reel/)에는 핸들이 없습니다. 이 가이드의 함수는 프로필 링크가 아니므로 이 모두에 대해 null을 반환합니다.
vm.tiktok.com 링크는 어떻게 처리하나요?
단축 링크에는 코드만 들어 있습니다. HTTP 요청을 보내 리디렉션을 따라가고 최종 URL을 파싱하세요. 네트워크 호출이고 코드가 만료될 수 있으므로 시간 제한과 캐시를 둔 별도 단계에서 처리하세요.
twitter.com도 여전히 유효한 프로필 URL인가요?
그렇습니다. twitter.com 프로필 URL은 x.com으로 리디렉션되므로 두 도메인을 모두 X 플랫폼으로 취급하고 x.com 형식을 정규 형식으로 저장하세요.
큰 정규식 하나를 쓰면 안 되나요?
문자열 전체에 쓰는 정규식 하나는 읽기 어렵고, 위장 호스트, 사용자 정보, 인코딩된 문자에 속기 쉽습니다. 먼저 표준 라이브러리로 URL을 파싱한 뒤, 작은 정규식은 핸들에만 쓰세요.
YouTube /c/ 또는 /user/ URL을 채널 ID로 바꾸려면 어떻게 하나요?
/user/<name> URL은 YouTube Data API의 forUsername 필터에 대응합니다. /c/ 맞춤 이름을 위한 API 필터는 없습니다. 두 경우 모두 핸들로 YouTube 채널 ID를 얻는 방법 가이드를 참고하세요.