Back to skills
extension
Category: Data & AnalyticsNo API key required

Hellotik Video Parser

通过真实浏览器操作 HelloTik.app 网站,解析并下载抖音、小红书、快手平台的去水印视频/图片

personAuthor: user_b22a52bahubcommunity

HelloTik 视频解析下载技能

通过真实浏览器操作 HelloTik.app (https://www.hellotik.app) 实现短视频去水印解析与下载。

设计思路

HelloTik 使用了 ticket 加密 + 反自动化检测机制,nodriver/Playwright 等自动化浏览器会被识别并触发 IP 限制。因此本 Skill 采用 「用户手动操作 + Agent 自动提取结果」 的混合模式:

  1. Agent:打开 HelloTik 页面 → 注入 JS 拦截器 → 弹出浏览器给用户
  2. 用户:手动粘贴链接 → 点击解析(不会被检测为自动化)
  3. 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

注意事项

  1. 必须弹出浏览器:解析步骤由用户在真实浏览器中手动完成,Agent 仅负责页面准备和结果提取。
  2. IP 限制:即使手动操作,同一 IP 短时间内大量请求仍可能触发限制,通常 15 分钟后恢复。
  3. 链接有效期:部分视频链接可能已过期或被删除,解析会失败。
  4. 版权声明:解析下载的视频版权归原平台及作者所有,请尊重版权。
  5. 无声音问题:解析下载的视频通常包含原声,少数情况可能无声。

支持的链接格式

| 平台 | 链接格式示例 | |---|---| | 抖音 | https://www.douyin.com/video/xxxxxxxxxhttps://v.douyin.com/xxxxx | | 小红书 | https://www.xiaohongshu.com/explore/xxxxxxxxhttps://xhslink.com/xxxxx | | 快手 | https://www.kuaishou.com/short-video/xxxxxxxxhttps://v.kuaishou.com/xxxxx |