HelloTik 视频解析下载技能
通过真实浏览器操作 HelloTik.app (https://www.hellotik.app) 实现短视频去水印解析与下载。
设计思路
HelloTik 使用了 ticket 加密 + 反自动化检测机制,nodriver/Playwright 等自动化浏览器会被识别并触发 IP 限制。因此本 Skill 采用 「用户手动操作 + Agent 自动提取结果」 的混合模式:
- Agent:打开 HelloTik 页面 → 注入 JS 拦截器 → 弹出浏览器给用户
- 用户:手动粘贴链接 → 点击解析(不会被检测为自动化)
- Agent:用户确认完成后 → 自动从页面提取下载链接 → 下载到本地
支持平台与内容类型
| 平台 | URL 路径 | 支持内容 |
|---|---|---|
| 抖音 | /zh/douyin | 短视频(去水印高清) |
| 小红书/Rednote | /zh/rednote | 视频、图片、实况Live、笔记 |
| 快手 | /zh/kuaishou | 视频、图片、直播回放、合集 |
核心特性
- 完全免费,不限次数
- 绕过反自动化检测(用户手动操作解析步骤)
- 去水印高清下载
- 支持批量解析(多条链接一次性解析)
依赖
本 Skill 依赖 browser 技能(nodriver 浏览器自动化)和 requests。
完整执行流程
Step 0: 加载依赖
import sys, os
sys.path.insert(0, os.path.join(os.getenv("SKILL_PATH"), "browser", "scripts"))
import browser
import time
import json
import requests
Step 1: 自动识别平台
def detect_platform(url: str) -> dict:
"""
根据视频链接自动识别平台。
返回 {path: str, name: str, placeholder: str}
"""
url_lower = url.lower()
if any(kw in url_lower for kw in ['douyin.com', '抖音', 'iesdouyin']):
return {'path': '/zh/douyin', 'name': '抖音', 'placeholder': '请粘贴视频链接进行解析'}
elif any(kw in url_lower for kw in ['xiaohongshu.com', 'xhslink.com', '小红书', 'rednote']):
return {'path': '/zh/rednote', 'name': '小红书', 'placeholder': '请粘贴小红书视频或笔记链接'}
elif any(kw in url_lower for kw in ['kuaishou.com', '快手', 'gifshow.com', 'chenzhongtech.com']):
return {'path': '/zh/kuaishou', 'name': '快手', 'placeholder': '请粘贴快手视频链接'}
else:
return {'path': '/zh/douyin', 'name': '抖音', 'placeholder': '请粘贴视频链接进行解析'}
Step 2: 导航 + 注入拦截器
def prepare_page(video_url: str):
"""
打开 HelloTik 对应平台页面,注入 JS 拦截器捕获解析结果。
"""
platform = detect_platform(video_url)
base_url = "https://www.hellotik.app"
# 导航到平台页面
browser.navigate(url=f"{base_url}{platform['path']}")
time.sleep(2)
# 注入 fetch 拦截器(在用户手动操作前注入)
browser.execute_script("""
window._hellotikParseResults = [];
window._hellotikDone = false;
const origFetch = window.fetch;
window.fetch = function(...args) {
const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '');
return origFetch.apply(this, args).then(async response => {
try {
const clone = response.clone();
if (url.includes('/api/parse')) {
const data = await clone.json();
window._hellotikParseResults.push({
url: url,
status: response.status,
data: data
});
window._hellotikDone = true;
}
} catch(e) {}
return response;
});
};
return 'ok';
""")
return platform
Step 3: 弹出浏览器让用户操作(关键步骤)
# 弹出浏览器窗口,用户手动粘贴链接并点击解析
browser.request_manual()
然后必须调用 ask_user_question 询问用户是否完成操作:
请用户在弹出的浏览器中完成以下操作:
1. 粘贴视频链接到输入框
2. 点击"解析视频"按钮
3. 等待解析结果显示在页面上
用户确认完成后再继续 Step 4。
Step 4: 自动提取解析结果
def extract_results() -> list:
"""
从页面中提取解析结果(视频/图片下载链接)。
优先使用 JS 拦截到的 API 响应,备选从 DOM 提取。
"""
# 方法1:从拦截的 API 响应获取
api_results = browser.execute_script("""
if (window._hellotikParseResults && window._hellotikParseResults.length > 0) {
return JSON.stringify(window._hellotikParseResults);
}
return null;
""")
if api_results and api_results != 'null':
try:
results = json.loads(api_results)
return parse_api_response(results)
except Exception as e:
print(f"API 结果解析失败: {e}")
# 方法2:从 DOM 提取(备选)
return extract_from_dom()
def parse_api_response(api_results: list) -> list:
"""
解析 HelloTik /api/parse 的响应数据。
响应结构示例:
{
"data": {
"videos": [
{
"title": "视频标题",
"cover": "封面URL",
"video_fullinfo": [
{"type": "default", "url": "无水印视频URL", "size": 1234567},
{"type": "HD", "url": "高清版URL", "size": 2345678}
],
"author": {"nickname": "作者名", "avatar": "头像URL"}
}
],
"pics": [
{
"url": "图片URL",
"cover": "缩略图URL"
}
]
}
}
"""
extracted = []
for result in api_results:
data = result.get('data', {})
# 视频列表
videos = data.get('videos', [])
for idx, v in enumerate(videos):
title = v.get('title', f'视频_{idx+1}')
cover = v.get('cover', '')
# video_fullinfo 包含多个分辨率
video_fullinfo = v.get('video_fullinfo', [])
video_urls = []
for vi in video_fullinfo:
video_urls.append({
'type': vi.get('type', 'default'),
'url': vi.get('url', ''),
'size': vi.get('size', 0)
})
author = v.get('author', {})
extracted.append({
'type': 'video',
'title': title,
'cover': cover,
'video_urls': video_urls,
'author': author.get('nickname', ''),
'avatar': author.get('avatar', '')
})
# 图片列表
pics = data.get('pics', [])
for idx, p in enumerate(pics):
extracted.append({
'type': 'image',
'url': p.get('url', ''),
'cover': p.get('cover', ''),
'title': f'图片_{idx+1}'
})
return extracted
def extract_from_dom() -> list:
"""
从页面 DOM 提取下载链接(当 API 拦截失败时使用)。
"""
result = browser.execute_script("""
const items = [];
// 查找所有视频/图片卡片元素
// HelloTik 的解析结果通常包含视频封面、标题、下载按钮
const cards = document.querySelectorAll('[class*="card"], [class*="item"], [class*="result"], [class*="video"]');
cards.forEach((card, idx) => {
// 提取标题
const titleEl = card.querySelector('[class*="title"], h2, h3, p');
const title = titleEl ? titleEl.textContent.trim() : '';
// 提取封面图
const imgEl = card.querySelector('img[src]');
const cover = imgEl ? imgEl.src : '';
// 提取下载链接(视频直链)
const links = card.querySelectorAll('a[href]');
const downloadUrls = [];
links.forEach(a => {
const href = a.href;
const text = a.textContent.trim();
if (href && !href.includes('javascript') && !href.includes('#')) {
downloadUrls.push({url: href, label: text});
}
});
// 提取视频源
const videoEl = card.querySelector('video[src]');
const videoSrc = videoEl ? videoEl.src : '';
const sourceEl = card.querySelector('source[src]');
const sourceSrc = sourceEl ? sourceEl.src : '';
if (title || cover || downloadUrls.length > 0 || videoSrc || sourceSrc) {
items.push({
title: title,
cover: cover,
download_urls: downloadUrls,
video_src: videoSrc || sourceSrc
});
}
});
// 如果上面没找到,尝试更宽泛的搜索
if (items.length === 0) {
const allLinks = document.querySelectorAll('a');
allLinks.forEach(a => {
const href = a.href;
if (href && (href.includes('.mp4') || href.includes('/video/') ||
href.includes('download'))) {
items.push({
title: a.textContent.trim(),
url: href
});
}
});
}
return JSON.stringify(items);
""")
try:
return json.loads(result)
except:
return []
Step 5: 下载文件到本地
def download_file(url: str, save_dir: str, filename: str) -> str:
"""
下载视频/图片到本地。
Args:
url: 文件直链
save_dir: 保存目录
filename: 文件名
Returns:
str: 本地文件路径
"""
os.makedirs(save_dir, exist_ok=True)
filepath = os.path.join(save_dir, filename)
headers = {
'Referer': 'https://www.hellotik.app/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
resp = requests.get(url, timeout=120, stream=True, headers=headers)
resp.raise_for_status()
total = int(resp.headers.get('content-length', 0))
downloaded = 0
with open(filepath, 'wb') as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
downloaded += len(chunk)
size_mb = downloaded / (1024 * 1024)
print(f"下载完成: {filename} ({size_mb:.1f} MB)")
return filepath
完整调用示例
def parse_and_download(video_url: str, save_dir: str = None):
"""
完整流程:解析视频链接 → 用户手动操作 → 提取结果 → 下载
Args:
video_url: 视频分享链接
save_dir: 保存目录(默认工作区/hellotik_downloads)
"""
if not save_dir:
save_dir = os.path.join(os.getenv("WORKSPACE_DIR"), "hellotik_downloads")
# Step 1-2: 准备页面
platform = prepare_page(video_url)
print(f"已打开 HelloTik {platform['name']} 解析页面")
# Step 3: 弹出浏览器让用户操作
browser.request_manual()
# ⚠️ 此处必须调用 ask_user_question 询问用户是否完成
# Step 4: 提取结果
results = extract_results()
if not results:
return {"error": "未找到解析结果,请确认页面已显示视频信息"}
# Step 5: 下载
downloaded_files = []
for item in results:
if item.get('type') == 'video':
urls = item.get('video_urls', [])
if urls:
# 优先下载最高分辨率
best = max(urls, key=lambda x: x.get('size', 0))
filename = sanitize_filename(item.get('title', 'video')) + '.mp4'
filepath = download_file(best['url'], save_dir, filename)
downloaded_files.append(filepath)
elif item.get('type') == 'image':
filename = sanitize_filename(item.get('title', 'image')) + '.jpg'
filepath = download_file(item.get('url', ''), save_dir, filename)
downloaded_files.append(filepath)
return {
"platform": platform['name'],
"total": len(results),
"downloaded": downloaded_files
}
def sanitize_filename(name: str) -> str:
"""清理文件名中的非法字符"""
import re
name = re.sub(r'[\\/:*?"<>|]', '_', name)
return name[:100] if len(name) > 100 else name
注意事项
- 必须弹出浏览器:解析步骤由用户在真实浏览器中手动完成,Agent 仅负责页面准备和结果提取。
- IP 限制:即使手动操作,同一 IP 短时间内大量请求仍可能触发限制,通常 15 分钟后恢复。
- 链接有效期:部分视频链接可能已过期或被删除,解析会失败。
- 版权声明:解析下载的视频版权归原平台及作者所有,请尊重版权。
- 无声音问题:解析下载的视频通常包含原声,少数情况可能无声。
支持的链接格式
| 平台 | 链接格式示例 |
|---|---|
| 抖音 | https://www.douyin.com/video/xxxxxxxxx、https://v.douyin.com/xxxxx |
| 小红书 | https://www.xiaohongshu.com/explore/xxxxxxxx、https://xhslink.com/xxxxx |
| 快手 | https://www.kuaishou.com/short-video/xxxxxxxx、https://v.kuaishou.com/xxxxx |
Scan to join WeChat group