다중 플랫폼 비디오 링크 다운로드 가이드
이 문서는 Douyin, TikTok, Bilibili, Xiaohongshu, Weibo 등 플랫폼에서 비디오 링크를 가져온 후, 비디오 파일을 올바르게 다운로드하는 방법을 자세히 소개합니다. 핵심은 크로스 도메인, 핫링크 방지, 요청 헤더 검증 등 흔한 문제를 해결하는 것입니다.
목차
공통 문제 개요
각 플랫폼에서 비디오 링크를 가져온 후, 직접 다운로드하면 보통 다음과 같은 문제를 만나게 됩니다:
| 문제 유형 | 설명 | 해결 방안 |
|---|---|---|
| 핫링크 방지 (Referer) | 서버가 요청 출처를 검사하여 불법 출처를 거부 | 올바른 Referer 헤더 설정 |
| 크로스 도메인 (CORS) | 브라우저 보안 정책이 크로스 도메인 요청을 차단 | 백엔드 프록시 / 서버 측 다운로드 |
| User-Agent 검증 | 서버가 클라이언트 식별자를 검사 | 브라우저 UA 모방 |
| Cookie 검증 | 일부 링크는 로그인 상태 필요 | 유효한 Cookie 포함 |
| 링크 유효 시간 | 비디오 링크에는 만료 시간이 있음 | 즉시 사용, 만료 시 재획득 |
| IP 제한 | 일부 링크는 지역 접근 제한 | 해당 지역 프록시 사용 |
Douyin
비디오 링크 특징
Douyin 비디오 링크는 보통 다음과 같은 형태입니다:
1# 播放地址 (无水印)
2https://www.douyin.com/aweme/v1/play/?video_id=xxx&line=0&file_id=xxx
3
4# CDN 地址
5https://v26-web.douyinvod.com/xxx/xxx.mp4
6
7# 带水印下载地址
8https://aweme.snssdk.com/aweme/v1/playwm/?video_id=xxx다운로드 요구사항
| 요청 헤더 | 필수 여부 | 값 |
|---|---|---|
Referer | 필수 | https://www.douyin.com/ |
User-Agent | 권장 | 브라우저 UA |
Cookie | 선택 | 일부 고화질 링크에 필요 |
서버 측 다운로드 예시
1import httpx
2
3async def download_douyin_video(video_url: str, save_path: str) -> bool:
4 """
5 下载抖音视频
6
7 Args:
8 video_url: 视频播放地址
9 save_path: 保存路径
10
11 Returns:
12 是否下载成功
13 """
14 headers = {
15 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
16 "Referer": "https://www.douyin.com/", # 必须
17 "Accept": "*/*",
18 "Accept-Encoding": "identity", # 避免压缩,方便流式下载
19 "Range": "bytes=0-", # 支持断点续传
20 }
21
22 async with httpx.AsyncClient(follow_redirects=True, timeout=60) as client:
23 async with client.stream("GET", video_url, headers=headers) as response:
24 if response.status_code in [200, 206]:
25 with open(save_path, "wb") as f:
26 async for chunk in response.aiter_bytes(chunk_size=8192):
27 f.write(chunk)
28 return True
29 return False프런트엔드 크로스 도메인 문제
Douyin 비디오 링크는 CORS를 지원하지 않으므로, 프런트엔드에서 직접 요청하면 브라우저에 의해 차단됩니다:
1// ❌ 错误方式 - 会被 CORS 拦截
2fetch('https://v26-web.douyinvod.com/xxx.mp4')
3 .then(res => res.blob()) // CORS error!
4
5// ✅ 正确方式 - 使用后端代理
6fetch('/api/proxy/video?url=' + encodeURIComponent(videoUrl))
7 .then(res => res.blob())
8 .then(blob => {
9 const url = URL.createObjectURL(blob);
10 const a = document.createElement('a');
11 a.href = url;
12 a.download = 'video.mp4';
13 a.click();
14 });주의사항
- 링크 유효 시간: Douyin 비디오 링크는 보통 24시간 내에 유효합니다
- 워터마크 없는 링크:
play_addr를 사용하고download_addr는 사용하지 마세요 - 고화질 링크: 일부 4K/원본 화질 비디오는 용량이 크므로, 한 번에 다운로드 실패를 피하기 위해 분할 다운로드를 권장합니다
TikTok
비디오 링크 특징
1# 播放地址
2https://v16-webapp-prime.tiktok.com/video/tos/xxx.mp4
3
4# 无水印地址
5https://www.tiktok.com/aweme/v1/play/?video_id=xxx
6
7# 带水印地址
8https://v16-webapp.tiktok.com/video/tos/xxx.mp4다운로드 요구사항
Web端 비디오 링크
| 요청 헤더 | 필수 여부 | 값 |
|---|---|---|
Referer | 필수 | https://www.tiktok.com/ |
User-Agent | 필수 | 브라우저 UA |
Cookie | 필수 | tt_chain_token 필드 필요 |
중요: TikTok Web端에서 반환된 비디오 링크는 다운로드하려면 반드시
tt_chain_tokenCookie를 포함해야 하며, 그렇지 않으면 403 오류가 반환됩니다.
App端 비디오 링크
| 요청 헤더 | 필수 여부 | 값 |
|---|---|---|
User-Agent | 권장 | 임의의 UA |
Cookie | 불필요 | App端 링크에는 이 제한이 없음 |
권장: App端 링크를 얻을 수 있다면 우선적으로 App端 링크를 사용하세요. Cookie 문제를 처리할 필요가 없습니다.
서버 측 다운로드 예시
Web端 다운로드 (tt_chain_token 필요)
1import httpx
2
3async def download_tiktok_video_web(
4 video_url: str,
5 save_path: str,
6 tt_chain_token: str # 必须提供
7) -> bool:
8 """
9 下载 TikTok Web 端视频
10
11 注意:
12 - Web 端链接必须携带 tt_chain_token Cookie
13 - tt_chain_token 可从浏览器或 API 响应中获取
14 """
15 headers = {
16 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
17 "Referer": "https://www.tiktok.com/",
18 "Accept": "video/webm,video/ogg,video/*;q=0.9,*/*;q=0.8",
19 "Accept-Language": "en-US,en;q=0.5",
20 "Cookie": f"tt_chain_token={tt_chain_token}", # 必须
21 "Sec-Fetch-Dest": "video",
22 "Sec-Fetch-Mode": "no-cors",
23 "Sec-Fetch-Site": "cross-site",
24 }
25
26 async with httpx.AsyncClient(follow_redirects=True, timeout=60) as client:
27 async with client.stream("GET", video_url, headers=headers) as response:
28 if response.status_code in [200, 206]:
29 with open(save_path, "wb") as f:
30 async for chunk in response.aiter_bytes(chunk_size=8192):
31 f.write(chunk)
32 return True
33 elif response.status_code == 403:
34 print("下载失败: tt_chain_token 无效或缺失")
35 return FalseApp端 다운로드 (Cookie 제한 없음)
1async def download_tiktok_video_app(video_url: str, save_path: str) -> bool:
2 """
3 下载 TikTok App 端视频
4
5 App 端链接无需 Cookie,直接下载即可
6 """
7 headers = {
8 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
9 "Accept": "*/*",
10 }
11
12 async with httpx.AsyncClient(follow_redirects=True, timeout=60) as client:
13 async with client.stream("GET", video_url, headers=headers) as response:
14 if response.status_code in [200, 206]:
15 with open(save_path, "wb") as f:
16 async for chunk in response.aiter_bytes(chunk_size=8192):
17 f.write(chunk)
18 return True
19 return Falsett_chain_token 획득 방법
tt_chain_token은 TikTok이 비디오 다운로드 인증에 사용하는 Cookie로, 획득 방법은 다음과 같습니다:
-
브라우저에서 획득: TikTok 웹사이트를 열고, F12 개발자 도구 -> Application -> Cookies ->
tt_chain_token찾기 -
API 응답에서 획득: 일부 TikTok API 응답의
Set-Cookie헤더에 이 값이 포함될 수 있음 -
TikHub API 사용: API 반환 데이터에 사용 가능한 token이 이미 포함되어 있을 수 있음
1# 示例:从响应头提取 tt_chain_token
2def extract_tt_chain_token(response_headers: dict) -> str:
3 """从响应头中提取 tt_chain_token"""
4 cookies = response_headers.get("set-cookie", "")
5 for cookie in cookies.split(";"):
6 if "tt_chain_token=" in cookie:
7 return cookie.split("tt_chain_token=")[1].split(";")[0]
8 return ""지역 제한
TikTok 비디오는 지역 제한이 있을 수 있으며, 일부 비디오는 특정 국가에서만 접근 가능합니다:
1# 使用代理下载特定地区视频
2proxies = {
3 "http://": "http://us-proxy:8080",
4 "https://": "http://us-proxy:8080",
5}
6
7async with httpx.AsyncClient(proxies=proxies) as client:
8 # ...주의사항
- 지역 제한: 일부 비디오는 미국/유럽 IP가 있어야 접근 가능
- 링크 유효 시간: 링크 유효 기간은 약 24시간
- 워터마크 없음:
downloadAddr필드를 우선 사용
Bilibili
비디오 링크 특징
B站 비디오 링크는 비교적 특수하며, 오디오와 비디오가 분리되어 있습니다:
1# 视频流 (DASH)
2https://upos-sz-mirrorali.bilivideo.com/xxx/xxx.m4s
3
4# 音频流 (DASH)
5https://upos-sz-mirrorali.bilivideo.com/xxx/xxx.m4s
6
7# FLV 格式 (旧版)
8https://upos-sz-mirrorali.bilivideo.com/xxx/xxx.flv다운로드 요구사항
| 요청 헤더 | 필수 여부 | 값 |
|---|---|---|
Referer | 필수 | https://www.bilibili.com/ |
User-Agent | 권장 | 브라우저 UA |
Cookie | 고화질, 4K 필수 | SESSDATA 등 |
Range | 권장 | 분할 다운로드 |
서버 측 다운로드 예시
1import httpx
2import subprocess
3from pathlib import Path
4
5async def download_bilibili_video(
6 video_url: str,
7 audio_url: str,
8 save_path: str,
9 cookie: str = None
10) -> bool:
11 """
12 下载 B站 视频 (需要合并音视频)
13
14 Args:
15 video_url: 视频流地址
16 audio_url: 音频流地址
17 save_path: 最终保存路径
18 cookie: 可选,高清视频需要 SESSDATA
19 """
20 headers = {
21 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
22 "Referer": "https://www.bilibili.com/", # 必须,否则 403
23 "Accept": "*/*",
24 "Accept-Encoding": "identity",
25 "Origin": "https://www.bilibili.com",
26 }
27
28 if cookie:
29 headers["Cookie"] = cookie
30
31 # 临时文件路径
32 video_temp = save_path + ".video.m4s"
33 audio_temp = save_path + ".audio.m4s"
34
35 async with httpx.AsyncClient(follow_redirects=True, timeout=120) as client:
36 # 下载视频流
37 async with client.stream("GET", video_url, headers=headers) as response:
38 if response.status_code not in [200, 206]:
39 return False
40 with open(video_temp, "wb") as f:
41 async for chunk in response.aiter_bytes(chunk_size=8192):
42 f.write(chunk)
43
44 # 下载音频流
45 async with client.stream("GET", audio_url, headers=headers) as response:
46 if response.status_code not in [200, 206]:
47 return False
48 with open(audio_temp, "wb") as f:
49 async for chunk in response.aiter_bytes(chunk_size=8192):
50 f.write(chunk)
51
52 # 使用 FFmpeg 合并音视频
53 try:
54 subprocess.run([
55 "ffmpeg", "-y",
56 "-i", video_temp,
57 "-i", audio_temp,
58 "-c:v", "copy",
59 "-c:a", "copy",
60 save_path
61 ], check=True, capture_output=True)
62
63 # 删除临时文件
64 Path(video_temp).unlink()
65 Path(audio_temp).unlink()
66 return True
67 except subprocess.CalledProcessError:
68 return False화질과 Cookie의 관계
| 화질 | 로그인 필요 여부 | Cookie 요구사항 |
|---|---|---|
| 360P/480P | 아니오 | 없음 |
| 720P | 예 | SESSDATA |
| 1080P | 예 | SESSDATA + 대회원 |
| 4K/HDR | 예 | SESSDATA + 대회원 |
주의사항
- 반드시 병합: DASH 형식은 오디오와 비디오가 분리되어 있으므로 FFmpeg로 병합해야 함
- Referer 필수: 올바른 Referer가 없으면 403 반환
- 링크 유효 시간: 약 2시간 유효
- 분할 다운로드: 대용량 파일은 Range 헤더를 사용한 분할 다운로드 권장
Xiaohongshu
비디오 링크 특징
1# 视频地址
2https://sns-video-bd.xhscdn.com/xxx.mp4
3https://sns-video-hw.xhscdn.com/xxx.mp4
4
5# 图片地址
6https://sns-img-bd.xhscdn.com/xxx.jpg다운로드 요구사항
| 요청 헤더 | 필수 여부 | 값 |
|---|---|---|
Referer | 필수 | https://www.xiaohongshu.com/ |
User-Agent | 권장 | 브라우저 UA |
Cookie | 선택 | 일부 콘텐츠에 필요 |
서버 측 다운로드 예시
1import httpx
2
3async def download_xiaohongshu_video(video_url: str, save_path: str) -> bool:
4 """
5 下载小红书视频
6
7 小红书对 Referer 检查较严格
8 """
9 headers = {
10 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
11 "Referer": "https://www.xiaohongshu.com/", # 必须
12 "Accept": "*/*",
13 "Origin": "https://www.xiaohongshu.com",
14 "Sec-Fetch-Dest": "video",
15 "Sec-Fetch-Mode": "cors",
16 "Sec-Fetch-Site": "cross-site",
17 }
18
19 async with httpx.AsyncClient(follow_redirects=True, timeout=60) as client:
20 async with client.stream("GET", video_url, headers=headers) as response:
21 if response.status_code in [200, 206]:
22 with open(save_path, "wb") as f:
23 async for chunk in response.aiter_bytes(chunk_size=8192):
24 f.write(chunk)
25 return True
26 return False이미지 다운로드
Xiaohongshu 이미지도 핫링크 방지가 있습니다:
1async def download_xiaohongshu_image(image_url: str, save_path: str) -> bool:
2 """下载小红书图片"""
3 headers = {
4 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
5 "Referer": "https://www.xiaohongshu.com/",
6 "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
7 }
8
9 async with httpx.AsyncClient(follow_redirects=True) as client:
10 response = await client.get(image_url, headers=headers)
11 if response.status_code == 200:
12 with open(save_path, "wb") as f:
13 f.write(response.content)
14 return True
15 return False주의사항
- Referer 엄격: 반드시
xiaohongshu.com의 Referer를 포함해야 함 - CDN 노드:
bd(Baidu Cloud),hw(Huawei Cloud) 등 서로 다른 노드 - 워터마크 없음: API가 반환하는 링크는 보통 워터마크가 없음
비디오 링크 특징
Weibo 비디오 링크 형식은 여러 가지입니다:
1# 标准视频
2https://f.video.weibocdn.com/xxx.mp4
3
4# 直播回放
5https://live.video.weibocdn.com/xxx.flv
6
7# 高清视频
8https://video.weibo.com/media/play?fid=xxx다운로드 요구사항
| 요청 헤더 | 필수 여부 | 값 |
|---|---|---|
Referer | 필수 | https://weibo.com/ 또는 https://m.weibo.cn/ |
User-Agent | 권장 | 브라우저 UA |
Cookie | 일부 필요 | 비공개 비디오에 필요 |
서버 측 다운로드 예시
1import httpx
2
3async def download_weibo_video(video_url: str, save_path: str) -> bool:
4 """
5 下载微博视频
6
7 微博支持多个 Referer 域名
8 """
9 headers = {
10 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
11 "Referer": "https://weibo.com/", # 或 https://m.weibo.cn/
12 "Accept": "*/*",
13 "Accept-Encoding": "identity",
14 }
15
16 async with httpx.AsyncClient(follow_redirects=True, timeout=60) as client:
17 async with client.stream("GET", video_url, headers=headers) as response:
18 if response.status_code in [200, 206]:
19 with open(save_path, "wb") as f:
20 async for chunk in response.aiter_bytes(chunk_size=8192):
21 f.write(chunk)
22 return True
23 return False다중 화질 선택
Weibo 비디오는 보통 여러 화질을 제공합니다:
1def select_best_quality(media_info: dict) -> str:
2 """选择最高清晰度的视频链接"""
3 # 清晰度优先级
4 quality_priority = ["mp4_720p_mp4", "mp4_hd_mp4", "mp4_ld_mp4"]
5
6 playback_list = media_info.get("playback_list", [])
7
8 for quality in quality_priority:
9 for item in playback_list:
10 if item.get("quality_label") == quality:
11 return item.get("play_info", {}).get("url")
12
13 # 降级使用 stream_url
14 return media_info.get("stream_url")주의사항
- Referer 도메인:
weibo.com및m.weibo.cn지원 - 다중 화질: 반환 데이터에 여러 화질 선택지가 있음
- 링크 유효성: 일부 링크는 유효 기간 제한이 있음
YouTube
비디오 링크 특징
YouTube 비디오 링크는 보통 다음과 같은 형태입니다:
1# 标准视频流地址
2https://rr1---sn-xxx.googlevideo.com/videoplayback?expire=xxx&ei=xxx&ip=xxx&id=xxx&itag=xxx&source=youtube&...
3
4# 自适应流地址 (DASH)
5https://manifest.googlevideo.com/api/manifest/dash/...
6
7# 直播流地址 (HLS)
8https://manifest.googlevideo.com/api/manifest/hls_variant/...다운로드 요구사항
엄격한 IP 제한: YouTube 비디오 스트림 주소는 IP 지리 위치에 대해 매우 엄격한 검증을 하며, 미국 캘리포니아주 로스앤젤레스 지역 IP를 사용해야만 성공적으로 다운로드할 수 있습니다. 그렇지 않으면 403 Forbidden 오류가 반환됩니다.
| 요청 헤더 | 필수 여부 | 값 |
|---|---|---|
User-Agent | 필수 | 브라우저 UA |
Referer | 권장 | https://www.youtube.com/ |
Origin | 권장 | https://www.youtube.com |
| 프록시 IP | 필수 | 미국 캘리포니아주 로스앤젤레스 IP |
IP 지역 제한 설명
YouTube의 비디오 스트림 주소는 생성 시 요청자의 IP 주소에 바인딩되며, 다운로드 시 반드시 동일한 지역의 IP를 사용해야 합니다:
1# 视频流 URL 中包含 IP 绑定参数
2https://rr1---sn-xxx.googlevideo.com/videoplayback?
3 expire=1234567890 # 过期时间
4 &ei=xxx # 加密标识
5 &ip=1.2.3.4 # 绑定的 IP 地址 (关键!)
6 &id=xxx
7 &itag=137 # 视频质量标识
8 &source=youtube
9 &...왜 반드시 로스앤젤레스 IP여야 하나요?
- YouTube의 대부분 CDN 노드는 미국 서부 해안에 위치함
- 비디오 스트림 URL 생성 시 요청 IP에 따라 가장 가까운 CDN이 선택됨
- 다운로드 시 IP 지역이 일치하지 않으면 403 오류가 발생함
- 로스앤젤레스는 YouTube의 주요 데이터센터 위치로, 호환성이 가장 좋음
서버 측 다운로드 예시
1import httpx
2
3async def download_youtube_video(
4 video_url: str,
5 save_path: str,
6 proxy: str = None # 必须是美国加州洛杉矶代理
7) -> bool:
8 """
9 下载 YouTube 视频
10
11 重要:
12 - 必须使用美国加利福尼亚州洛杉矶地区的代理 IP
13 - 其他地区 IP 会返回 403 Forbidden
14 - 视频流 URL 有时效性,通常 6 小时内有效
15 """
16 headers = {
17 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
18 "Referer": "https://www.youtube.com/",
19 "Origin": "https://www.youtube.com",
20 "Accept": "*/*",
21 "Accept-Language": "en-US,en;q=0.9",
22 "Accept-Encoding": "identity", # 避免压缩
23 "Range": "bytes=0-", # 支持断点续传
24 "Sec-Fetch-Dest": "video",
25 "Sec-Fetch-Mode": "no-cors",
26 "Sec-Fetch-Site": "cross-site",
27 }
28
29 # 代理配置 - 必须是洛杉矶 IP
30 proxies = None
31 if proxy:
32 proxies = {
33 "http://": proxy,
34 "https://": proxy,
35 }
36
37 async with httpx.AsyncClient(
38 follow_redirects=True,
39 timeout=120,
40 proxies=proxies
41 ) as client:
42 async with client.stream("GET", video_url, headers=headers) as response:
43 if response.status_code in [200, 206]:
44 with open(save_path, "wb") as f:
45 async for chunk in response.aiter_bytes(chunk_size=8192):
46 f.write(chunk)
47 return True
48 elif response.status_code == 403:
49 print("下载失败: IP 地区不符合要求,请使用美国加州洛杉矶的代理")
50 elif response.status_code == 410:
51 print("下载失败: 视频链接已过期,请重新获取")
52 return False오디오/비디오 분리 처리
YouTube 고화질 비디오(1080p+)는 DASH 형식을 사용하며, 오디오와 비디오가 분리되어 있습니다:
1import subprocess
2from pathlib import Path
3
4async def download_youtube_video_with_audio(
5 video_url: str,
6 audio_url: str,
7 save_path: str,
8 proxy: str = None
9) -> bool:
10 """
11 下载 YouTube 视频并合并音频
12
13 YouTube 1080p 及以上清晰度的视频,音频和视频是分离的
14 需要分别下载后使用 FFmpeg 合并
15 """
16 video_temp = save_path + ".video.mp4"
17 audio_temp = save_path + ".audio.m4a"
18
19 # 下载视频流
20 success = await download_youtube_video(video_url, video_temp, proxy)
21 if not success:
22 return False
23
24 # 下载音频流
25 success = await download_youtube_video(audio_url, audio_temp, proxy)
26 if not success:
27 return False
28
29 # 使用 FFmpeg 合并
30 try:
31 subprocess.run([
32 "ffmpeg", "-y",
33 "-i", video_temp,
34 "-i", audio_temp,
35 "-c:v", "copy",
36 "-c:a", "aac",
37 save_path
38 ], check=True, capture_output=True)
39
40 # 删除临时文件
41 Path(video_temp).unlink()
42 Path(audio_temp).unlink()
43 return True
44 except subprocess.CalledProcessError as e:
45 print(f"FFmpeg 合并失败: {e}")
46 return False프록시 설정 예시
1# 推荐的代理配置 - 必须是洛杉矶地区
2LA_PROXIES = [
3 "http://user:pass@la-proxy1.example.com:8080", # 洛杉矶代理 1
4 "http://user:pass@la-proxy2.example.com:8080", # 洛杉矶代理 2
5 "socks5://user:pass@la-socks.example.com:1080", # SOCKS5 代理
6]
7
8# 验证代理 IP 是否在洛杉矶
9async def verify_proxy_location(proxy: str) -> bool:
10 """验证代理 IP 是否在洛杉矶地区"""
11 async with httpx.AsyncClient(proxies={"https://": proxy}) as client:
12 response = await client.get("https://ipapi.co/json/")
13 data = response.json()
14
15 city = data.get("city", "")
16 region = data.get("region", "")
17 country = data.get("country_code", "")
18
19 # 检查是否在加州洛杉矶
20 is_la = (
21 country == "US" and
22 region == "California" and
23 "Los Angeles" in city
24 )
25
26 if is_la:
27 print(f"✓ 代理 IP 位于: {city}, {region}, {country}")
28 else:
29 print(f"✗ 代理 IP 位于: {city}, {region}, {country} (不符合要求)")
30
31 return is_la화질 식별자(itag) 대응표
| itag | 해상도 | 형식 | 오디오 포함 여부 |
|---|---|---|---|
| 18 | 360p | MP4 | 예 |
| 22 | 720p | MP4 | 예 |
| 137 | 1080p | MP4 | 아니오 (병합 필요) |
| 248 | 1080p | WebM | 아니오 (병합 필요) |
| 271 | 1440p | WebM | 아니오 (병합 필요) |
| 313 | 2160p | WebM | 아니오 (병합 필요) |
| 140 | - | M4A 오디오 | 오디오만 |
| 251 | - | WebM 오디오 | 오디오만 |
주의사항
- IP 제한이 매우 엄격함: 반드시 미국 캘리포니아주 로스앤젤레스 IP를 사용해야 하며, 다른 지역(미국의 다른 주 포함)은 실패할 수 있음
- 링크 유효 시간: 비디오 스트림 URL은 보통 6시간 내에 유효
- 오디오/비디오 분리: 720p 이상의 화질은 오디오와 비디오를 각각 다운로드한 후 병합해야 함
- 속도 제한: YouTube는 다운로드 속도에 제한이 있으므로, 대용량 파일은 분할 다운로드 권장
- 저작권 보호: 일부 비디오는 DRM 보호가 있어 직접 다운로드할 수 없음
프런트엔드 다운로드 방식
CORS 제한으로 인해 프런트엔드에서는 대부분 플랫폼의 비디오를 직접 다운로드할 수 없습니다. 가능한 방식은 다음과 같습니다:
방식 1: 백엔드 프록시(권장)
1// 前端请求后端代理接口
2async function downloadVideo(videoUrl, platform, filename) {
3 const proxyUrl = `/api/download/video?url=${encodeURIComponent(videoUrl)}&platform=${platform}`;
4
5 const response = await fetch(proxyUrl);
6 const blob = await response.blob();
7
8 // 创建下载链接
9 const url = URL.createObjectURL(blob);
10 const a = document.createElement('a');
11 a.href = url;
12 a.download = filename || 'video.mp4';
13 document.body.appendChild(a);
14 a.click();
15 document.body.removeChild(a);
16 URL.revokeObjectURL(url);
17}방식 2: 새 창에서 열기(일부 플랫폼에서 사용 가능)
1// 部分平台链接可以直接在新窗口打开下载
2function openVideoInNewTab(videoUrl) {
3 window.open(videoUrl, '_blank');
4}방식 3: download 속성 사용(동일 출처 시나리오)
1<!-- 仅适用于同源或允许 CORS 的资源 -->
2<a href="video.mp4" download="video.mp4">下载视频</a>백엔드 프록시 다운로드 방식
FastAPI 프록시 예시
1from fastapi import FastAPI, Query, Response
2from fastapi.responses import StreamingResponse
3import httpx
4
5app = FastAPI()
6
7# 各平台的请求头配置
8PLATFORM_HEADERS = {
9 "douyin": {
10 "Referer": "https://www.douyin.com/",
11 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
12 },
13 "tiktok": {
14 "Referer": "https://www.tiktok.com/",
15 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
16 },
17 "bilibili": {
18 "Referer": "https://www.bilibili.com/",
19 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
20 },
21 "xiaohongshu": {
22 "Referer": "https://www.xiaohongshu.com/",
23 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
24 },
25 "weibo": {
26 "Referer": "https://weibo.com/",
27 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
28 },
29}
30
31
32@app.get("/api/download/video")
33async def proxy_video(
34 url: str = Query(..., description="视频 URL"),
35 platform: str = Query(..., description="平台标识"),
36):
37 """
38 视频代理下载接口
39
40 解决前端跨域和防盗链问题
41 """
42 headers = PLATFORM_HEADERS.get(platform, {})
43
44 async def video_stream():
45 async with httpx.AsyncClient(follow_redirects=True, timeout=120) as client:
46 async with client.stream("GET", url, headers=headers) as response:
47 async for chunk in response.aiter_bytes(chunk_size=8192):
48 yield chunk
49
50 return StreamingResponse(
51 video_stream(),
52 media_type="video/mp4",
53 headers={
54 "Content-Disposition": "attachment; filename=video.mp4",
55 "Access-Control-Allow-Origin": "*", # 允许跨域
56 }
57 )
58
59
60@app.get("/api/download/image")
61async def proxy_image(
62 url: str = Query(..., description="图片 URL"),
63 platform: str = Query(..., description="平台标识"),
64):
65 """图片代理下载接口"""
66 headers = PLATFORM_HEADERS.get(platform, {})
67
68 async with httpx.AsyncClient(follow_redirects=True) as client:
69 response = await client.get(url, headers=headers)
70
71 return Response(
72 content=response.content,
73 media_type=response.headers.get("content-type", "image/jpeg"),
74 headers={
75 "Access-Control-Allow-Origin": "*",
76 }
77 )진행률이 있는 다운로드 예시
1import httpx
2from tqdm import tqdm
3
4async def download_with_progress(url: str, save_path: str, headers: dict) -> bool:
5 """带进度条的下载"""
6 async with httpx.AsyncClient(follow_redirects=True, timeout=120) as client:
7 async with client.stream("GET", url, headers=headers) as response:
8 total = int(response.headers.get("content-length", 0))
9
10 with open(save_path, "wb") as f:
11 with tqdm(total=total, unit="B", unit_scale=True, desc="下载中") as pbar:
12 async for chunk in response.aiter_bytes(chunk_size=8192):
13 f.write(chunk)
14 pbar.update(len(chunk))
15
16 return True
17 return False요약
각 플랫폼 다운로드 핵심 포인트 빠른 참고표
| 플랫폼 | Referer | Cookie 요구사항 | 특수 처리 | 링크 유효 시간 |
|---|---|---|---|---|
| Douyin | https://www.douyin.com/ | 선택 | 없음 | 24시간 |
| TikTok Web | https://www.tiktok.com/ | 필수 tt_chain_token | 지역 프록시 필요 | 1-6시간 |
| TikTok App | 불필요 | 불필요 | 사용 권장 | 김 |
| B站 | https://www.bilibili.com/ | 고화질은 SESSDATA 필수 | 오디오/비디오 분리 시 병합 필요 | 2시간 |
| Xiaohongshu | https://www.xiaohongshu.com/ | 선택 | 없음 | 김 |
https://weibo.com/ | 일부 필요 | 다중 화질 | 김 | |
| YouTube | https://www.youtube.com/ | 불필요 | 반드시 로스앤젤레스 IP + 오디오/비디오 분리 | 6시간 |
흔한 오류 점검
| 오류 코드 | 가능한 원인 | 해결 방안 |
|---|---|---|
| 403 Forbidden | Referer 오류 또는 누락 | Referer 헤더 확인 |
| 403 Forbidden | TikTok의 tt_chain_token 누락 | Cookie 추가 또는 App端 링크 사용 |
| 403 Forbidden | YouTube IP 지역 불일치 | 미국 캘리포니아주 로스앤젤레스 프록시 사용 |
| 404 Not Found | 링크 만료 | 링크 재획득 |
| 410 Gone | YouTube 링크 만료 | 비디오 스트림 URL 재획득 |
| CORS Error | 브라우저 크로스 도메인 제한 | 백엔드 프록시 사용 |
| 시간 초과 | 네트워크 문제 또는 파일이 너무 큼 | 타임아웃 증가, 분할 다운로드 |
모범 사례
- 항상 Referer 설정: 대부분 플랫폼의 필수 조건입니다
- 백엔드 프록시 사용: 프런트엔드 CORS 문제를 해결하는 최선의 방법입니다
- 링크 유효 시간 처리: 링크를 즉시 사용하고, 만료되면 재획득하세요
- 이어받기 지원: 대용량 파일은 Range 헤더를 사용해 분할 다운로드하세요
- 오류 재시도: 네트워크가 불안정할 때 재시도 메커니즘을 구현하세요
문의하기
이 문서의 안내에 따라 작업했음에도 비디오를 성공적으로 다운로드할 수 없다면, 고객센터에 문의하여 도움을 받으세요:
- 공식 문서: https://docs.tikhub.io/
- Discord 커뮤니티: https://discord.gg/Pu2uKkFu6u
- 온라인 고객센터(Chaport): 공식 웹사이트 오른쪽 아래의 온라인 상담 창을 이용
고객센터에 문의할 때는 더 빠른 도움을 위해 아래 정보를 제공해 주세요:
- 플랫폼 이름: Douyin/TikTok/B站/Xiaohongshu/Weibo/YouTube
- 비디오 링크: 원본 비디오 페이지 URL
- 오류 정보: 전체 오류 코드와 오류 설명
- 요청 헤더 설정: 사용한 Referer, Cookie 등의 정보
- 프록시 정보: 프록시를 사용한 경우 프록시 지역을 제공해 주세요