【More Than Coding】0代码基础纯小白 用SOLO搭建抖音视频文案自动提取工具

【More Than Coding】用浏览器扩展+油猴脚本打造抖音视频文案自动提取工具

1. 摘要

本文详细记录了如何借助AI工具,通过油猴脚本与浏览器扩展的混合方案,实现抖音热点视频文案自动提取的完整开发过程。方案经历了Python爬虫失败、FunASR本地部署效果不佳、API调用费用过高等多次迭代,最终确定采用油猴脚本负责抖音页面视频数据爬取,浏览器扩展接收链接后自动调用豆包AI进行文案提取,最终生成包含视频信息和AI文案的Excel报表。整个流程实现了从手动操作数小时到一键完成的效率飞跃。
(TRAE生成的摘要)

2. 背景

我是一名普通新媒体运营工作者,日常需要收集和提取大量抖音热点视频文案内容。工作中面临几个核心痛点:

  • 搜索效率低:需要手动一个个浏览过来收集视频
  • 人工操作繁琐:获取每个视频的文案需要复制链接、打开AI工具、粘贴、发送、等待、收集
  • 数据分散:视频信息、下载链接、AI生成的文案分散在不同地方,难以统一管理
  • 批量处理困难:一次分析几十个视频需要重复操作大量步骤

原始流程至少需要花我一天的时间,且容易出错。

我本身是一个普通用户,没有专业的技术背景。我需要一个简单易用的工具,希望通过自动化工具将这个过程缩短到十几分钟。

3. 开发过程简述(作者自述)

实际的实践过程中经历了多次方案的大改,对话次数超过400次。AI的记忆有限,前面失败的方案实践过程由我自己补充说明,成功方案部分由AI总结。

3.1 方案探索

实际的实践过程中经历了多次方案的大改,对话次数超过了400次,但AI的记忆没这么长,所以前面失败的几次方案实践过程由我自己来自述,最后成功的方案由AI来总结。

第一阶段:Python爬虫的尝试

我在工作区向AI提出需求后,AI首先推荐建立Python爬虫工具。然而尝试过程中发现:

  • 页面采用动态加载,无法直接获取真实DOM结构
  • 改用Selenium等模拟浏览器后,每次启动都需要重新登录
  • 多次测试后确认抖音网页具有较强的反爬虫机制

该方案最终废弃。

第二阶段:油猴脚本的引入

转换思路后,改为使用油猴脚本(Tampermonkey)实现。我向AI描述了核心需求:自由设定关键词、视频卡片数量、筛选条件和结果数量,通过页面滚动收集卡片数据,最终以表格展示。

经过多轮测试后,搜索功能成功实现。AI还帮助后续还增加了Excel导出、下载链接刷新、结果分类、搜索时间预计、AI关键词优化等人性化功能。



3.2 文案提取的艰难探索

FunASR本地部署方案

完成视频爬取后,需要将视频内容转为文案。AI推荐使用FunASR本地部署方案:本地启动FunASR服务,在油猴脚本中添加"文案提取"按钮,自动下载视频并调用FunASR进行语音识别处理。

虽然FunASR搭建成功,但实际体验不佳:服务启动时间长、视频下载耗时、识别准确率低、错字较多无法形成完整语句。该方案最终放弃。

千问ASR API方案

尝试接入千问ASR API,但因调用费用问题无法接受。(不可能让我付费上班的hhh)

豆包网页自动化方案

最终确定的方案是:利用油猴脚本在新建的豆包对话页面中,自动定位输入框并填充提示词(链接+“提取该视频中的文案文本。(只返回文案内容)”),然后触发发送按钮。

3.3 跨域问题的最终解决

测试中发现浏览器扩展的Content Script运行在隔离的JavaScript环境中,无法访问抖音页面的window对象(如window.player获取视频下载链接)。AI随后推荐采用浏览器扩展配合油猴脚本的混合方案。

最初尝试将油猴脚本完整迁移到扩展中,但由于Content Script与网页脚本的权限差异,导致无法获取视频下载链接。

最终采用的折中方案是:油猴脚本负责爬取链接,浏览器扩展接收链接后在豆包页面完成对话并收集结果,最终以表格形式输出。

4. 实践过程(以下是TRAE总结的实践过程)

4.1 任务拆解

我将这个需求拆解为两个独立工具,通过标准接口通信:

油猴脚本(Tampermonkey):负责抖音页面内的操作

  • 多关键词搜索
  • 自动滚动页面收集视频卡片
  • 提取视频标题、作者、链接、点赞数、视频下载链接等元数据
  • 结果展示与筛选
  • 数据导出Excel

浏览器扩展(Chrome Extension):负责跨站点的AI交互

  • 接收油猴脚本传递的视频链接
  • 自动打开豆包网页
  • 填充链接并发送请求
  • 监控AI输出完成
  • 收集文案并生成最终Excel

通信架构设计

┌─────────────────────────────────────────────────────────────┐
│                        油猴脚本                              │
│                                                              │
│  window.dispatchEvent(CustomEvent)  ────────────────────────┼──→ Content Script
│  localStorage.setItem()              ────────────────────────┼──→ (隔离世界)
└─────────────────────────────────────────────────────────────┘
                              ↓
                    ┌─────────────────────┐
                    │   chrome.storage    │
                    │   (数据中转站)       │
                    └─────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                      浏览器扩展                              │
│                                                              │
│  Content Script ────────────────────────────────────────────→ Popup/后台脚本
│  (接收数据)         (处理任务)                                │
└─────────────────────────────────────────────────────────────┘

4.2 核心技术挑战与解决方案

挑战一:油猴脚本与浏览器扩展的通信

浏览器扩展的Content Script运行在隔离的JavaScript环境中(Isolated World),与网页脚本处于不同的JavaScript上下文。这意味着:

  • Content Script无法直接访问网页的window对象
  • Content Script中的全局变量网页无法访问
  • 两者通过DOM进行间接通信

解决方案:采用双重通信机制

  1. DOM事件机制:网页可以触发DOM事件,Content Script可以监听
  2. localStorage同步:设置localStorage会触发storage事件
// 油猴脚本端:触发自定义事件(主要通道)
const videoData = {
    urls: ['https://v.douyin.com/xxx1/', 'https://v.douyin.com/xxx2/'],
    videos: [
        { title: '视频1', author: '作者1', videoLink: '...', likes: 1234 },
        { title: '视频2', author: '作者2', videoLink: '...', likes: 5678 }
    ],
    timestamp: Date.now()
};

// 创建并派发自定义事件
const event = new CustomEvent('douyin-video-urls-ready', {
    detail: videoData,
    bubbles: true  // 允许事件冒泡
});
window.dispatchEvent(event);

// localStorage备份(备用通道)
// 防止事件监听器未就绪时数据丢失
localStorage.setItem('douyin-pending-urls', JSON.stringify(videoData));
// 浏览器扩展Content Script端:监听事件
let lastProcessedTimestamp = 0;

function processVideoData(urls, timestamp, source, data) {
    // 时间戳去重:防止Event和storage同时触发导致重复处理
    if (timestamp <= lastProcessedTimestamp) {
        console.log('[扩展] 跳过重复数据,timestamp:', timestamp);
        return;
    }
    lastProcessedTimestamp = timestamp;

    // 存储到chrome.storage供popup使用
    chrome.storage.local.set({
        videoUrls: urls,
        videoData: data.videos || [],
        receivedAt: Date.now()
    });

    // 请求打开弹窗
    chrome.runtime.sendMessage({ action: 'openPopup' });
}

// 监听CustomEvent
window.addEventListener('douyin-video-urls-ready', function(event) {
    const data = event.detail;
    console.log('[扩展] 收到Event:', data.urls.length, '个链接');
    processVideoData(data.urls, data.timestamp, 'event', data);
});

// 监听storage变化(备用通道)
window.addEventListener('storage', function(event) {
    if (event.key === 'douyin-pending-urls' && event.newValue) {
        try {
            const data = JSON.parse(event.newValue);
            console.log('[扩展] 收到storage变化');
            processVideoData(data.urls, data.timestamp, 'storage', data);
            // 清理数据避免重复处理
            localStorage.removeItem('douyin-pending-urls');
        } catch (e) {
            console.error('[扩展] 解析失败:', e);
        }
    }
});

挑战二:Content Script无法访问抖音全局对象

浏览器扩展的Content Script无法直接访问抖音页面的window.player对象,这是获取视频下载链接的关键障碍。

问题分析

  • window.player是抖音页面播放器实例
  • player.config.awemeInfo包含视频元数据
  • player.config.awemeInfo.video.bitRateList包含下载链接

解决方案:使用脚本注入绕过隔离

通过创建<script>标签并注入代码,可以在网页的执行上下文中运行:

// 创建注入脚本
function injectScript() {
    const callbackName = '__douyin_callback_' + Date.now();

    // 在window上注册回调函数
    window[callbackName] = function(videoData) {
        console.log('[注入] 收到视频数据:', videoData);
        // 提取下载链接
        const urls = extractDownloadUrls(videoData);
        console.log('[注入] 提取到链接:', urls);

        // 清理
        delete window[callbackName];
        document.body.removeChild(script);
    };

    // 构建注入代码
    const code = `
        (function() {
            console.log('[注入] 开始查找player对象');
            const player = window.player;
            if (player && player.config && player.config.awemeInfo) {
                console.log('[注入] 找到player');
                window['${callbackName}'](player.config.awemeInfo);
            } else {
                console.log('[注入] 未找到player');
                window['${callbackName}'](null);
            }
        })();
    `;

    // 创建script标签并注入
    const script = document.createElement('script');
    script.src = 'data:application/javascript,' + encodeURIComponent(code);
    document.body.appendChild(script);
}

挑战三:豆包页面AI输出的监控

豆包的回复是流式输出的,回复内容分布在多个DOM元素中。每个段落都是独立的<div class="paragraph-element">容器。

问题分析

  • AI开始回复时,语音输入按钮变为"暂停"按钮
  • AI回复完成时,按钮恢复为"语音输入"
  • 需要正确收集所有段落而非只取最后一个

完整监控逻辑

function startMonitoring() {
    console.log('[豆包] 开始监控输出');
    let stableCount = 0;
    const stableThreshold = 3;  // 连续3次检测到完成才确认
    const checkInterval = 1000; // 每秒检测一次

    const monitorInterval = setInterval(() => {
        // 查找语音相关按钮
        const voiceButton = document.querySelector('button[aria-label="语音输入"], button[aria-label="暂停"]');

        if (!voiceButton) {
            console.log('[豆包] 未找到语音按钮');
            return;
        }

        // 检测按钮状态:aria-label为"暂停"表示正在输出
        const isSpeaking = voiceButton.getAttribute('aria-label') === '暂停';

        if (!isSpeaking) {
            stableCount++;
            console.log(`[豆包] 输出稳定 ${stableCount}/${stableThreshold}`);

            if (stableCount >= stableThreshold) {
                console.log('[豆包] 确认输出完成');
                clearInterval(monitorInterval);
                extractAllParagraphs();  // 提取所有段落
            }
        } else {
            stableCount = 0;
            console.log('[豆包] 正在输出中...');
        }
    }, checkInterval);
}

function extractAllParagraphs() {
    // 选择器匹配豆包的段落容器
    const paragraphs = document.querySelectorAll('.paragraph-element');

    console.log(`[豆包] 找到 ${paragraphs.length} 个段落`);

    if (paragraphs.length === 0) {
        console.log('[豆包] 未找到段落,内容可能未加载');
        return;
    }

    // 遍历所有段落,收集文本内容
    const contents = [];
    paragraphs.forEach((p, index) => {
        const text = p.textContent.trim();
        if (text) {
            contents.push(text);
            console.log(`[豆包] 段落${index + 1}: ${text.substring(0, 30)}...`);
        }
    });

    // 用双换行符连接各段落
    const fullContent = contents.join('\n\n');
    console.log(`[豆包] 提取完成,总长度: ${fullContent.length} 字符`);

    // 存储到chrome.storage
    chrome.storage.local.get(['extractedTexts', 'currentIndex'], function(result) {
        const texts = result.extractedTexts || [];
        const index = result.currentIndex || 0;
        texts[index] = fullContent;
        chrome.storage.local.set({ extractedTexts: texts });
    });
}

4.3 关键代码实现

油猴脚本:视频数据收集与发送

// 全局变量存储搜索结果
let currentResultsData = {};

function sendAllToDoubao() {
    // 1. 收集所有视频数据
    const allVideos = [];
    Object.keys(currentResultsData).forEach(keyword => {
        const result = currentResultsData[keyword];
        if (result && result.videos) {
            result.videos.forEach((video, index) => {
                allVideos.push({
                    id: `${keyword}-${index}`,
                    title: video.title || '',
                    author: video.author || '',
                    videoLink: video.videoLink || '',
                    downloadLink: video.downloadLink || '',
                    likes: video.likes || 0,
                    publishTime: video.publishTime || '',
                    keyword: keyword
                });
            });
        }
    });

    // 2. 根据复选框筛选用户选中的视频
    const selectedIndices = [];
    document.querySelectorAll('.dy-video-checkbox:checked').forEach(cb => {
        const index = parseInt(cb.getAttribute('data-index'));
        selectedIndices.push(index);
    });

    // 3. 构建发送数据
    const selectedVideos = selectedIndices
        .map(index => allVideos[index])
        .filter(Boolean);

    if (selectedVideos.length === 0) {
        alert('没有选中任何视频');
        return;
    }

    const videoData = {
        videos: selectedVideos,  // 完整视频数据
        urls: selectedVideos.map(v => v.videoLink),  // 链接数组
        timestamp: Date.now(),
        count: selectedVideos.length
    };

    // 4. 触发事件(主要通道)
    const event = new CustomEvent('douyin-video-urls-ready', {
        detail: videoData
    });
    window.dispatchEvent(event);

    // 5. localStorage备份(备用通道)
    localStorage.setItem('douyin-pending-urls', JSON.stringify(videoData));

    console.log(`[油猴] 已发送 ${selectedVideos.length} 个视频数据`);
}

浏览器扩展popup.js:任务调度与Excel生成

// 状态管理
let videoUrls = [];
let videoData = [];
let currentIndex = 0;
let extractedTexts = [];
let currentWindowId = null;

function startNextTask() {
    if (currentIndex >= videoUrls.length) {
        // 所有任务完成,生成Excel
        generateExcel(videoData, extractedTexts);
        updateStatus('🎉 所有任务完成!', 'success', 100);
        return;
    }

    const currentVideo = videoData[currentIndex];
    const currentUrl = videoUrls[currentIndex];

    updateStatus(`处理中 (${currentIndex + 1}/${videoUrls.length}): ${currentVideo.title}`);

    // 将当前链接存储,供Content Script读取
    chrome.storage.local.set({
        videoUrl: currentUrl,
        currentIndex: currentIndex,
        extractedTexts: extractedTexts
    }, function() {
        // 创建豆包窗口
        chrome.windows.create({
            url: 'https://www.doubao.com/chat',
            type: 'popup',
            width: 400,
            height: 300,
            left: window.screen.width - 450,
            top: window.screen.height - 350
        }, function(window) {
            currentWindowId = window.id;

            // 等待一段时间后处理下一个(给AI生成时间)
            setTimeout(() => {
                startNextTask();
            }, 8000);  // 8秒间隔,可根据实际情况调整
        });
    });
}

// Excel生成函数
function generateExcel(videos, texts) {
    // 构建HTML表格
    let html = '<!DOCTYPE html><html><head><meta charset="UTF-8">';
    html += '<style>';
    html += 'table{border-collapse:collapse;width:100%;}';
    html += 'th,td{border:1px solid #ddd;padding:8px;text-align:left;}';
    html += 'th{background:#f2f2f2;font-weight:bold;}';
    html += 'tr:nth-child(even){background:#fafafa;}';
    html += '</style></head><body>';
    html += '<table>';
    html += '<tr><th>排名</th><th>来源关键词</th><th>作者</th><th>视频名称</th><th>发布时间</th><th>点赞数量</th><th>视频链接</th><th>下载链接</th><th>提取到的文案</th></tr>';

    videos.forEach((video, index) => {
        html += '<tr>';
        html += `<td>${index + 1}</td>`;
        html += `<td>${escapeHtml(video.keyword || '')}</td>`;
        html += `<td>${escapeHtml(video.author || '')}</td>`;
        html += `<td>${escapeHtml(video.title || '')}</td>`;
        html += `<td>${escapeHtml(video.publishTime || '')}</td>`;
        html += `<td>${video.likes || 0}</td>`;
        html += `<td><a href="${escapeHtml(video.videoLink || '')}" target="_blank">点击查看</a></td>`;
        html += `<td><a href="${escapeHtml(video.downloadLink || '')}" target="_blank">点击下载</a></td>`;
        html += `<td>${escapeHtml(texts[index] || '提取失败')}</td>`;
        html += '</tr>';
    });

    html += '</table></body></html>';

    // 生成并下载文件
    const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `视频文案提取结果_${Date.now()}.xls`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
}

// HTML特殊字符转义
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

4.4 技术难点详解

难点一:时间戳去重机制

Event和localStorage可能同时触发,导致重复处理。使用时间戳比较解决:

let lastProcessedTimestamp = 0;

function processData(timestamp) {
    // 只有更新的数据才处理
    if (timestamp <= lastProcessedTimestamp) {
        console.log('跳过重复数据');
        return;
    }
    lastProcessedTimestamp = timestamp;
    // 处理数据...
}

难点二:豆包页面DOM结构适配

豆包的DOM结构可能随版本更新变化,需要设计健壮的选择器:

function findInputElement() {
    // 优先级1:placeholder包含"输入"或"提问"的textarea
    const textareas = document.querySelectorAll('textarea');
    for (const el of textareas) {
        const placeholder = el.placeholder || '';
        if (placeholder.includes('输入') || placeholder.includes('提问')) {
            return el;
        }
    }

    // 优先级2:role="textbox"的contenteditable元素
    const editors = document.querySelectorAll('[contenteditable="true"]');
    for (const el of editors) {
        if (el.getAttribute('role') === 'textbox') {
            return el;
        }
    }

    // 优先级3:任意textarea(兜底)
    return textareas.length > 0 ? textareas[0] : null;
}

function findSendButton() {
    const buttons = document.querySelectorAll('button');

    for (const btn of buttons) {
        const text = btn.textContent.trim();
        const ariaLabel = btn.getAttribute('aria-label');
        const className = btn.className;

        // 多种匹配方式
        if (
            text.includes('发送') ||
            ariaLabel === '发送' ||
            className.includes('send') ||
            className.includes('submit')
        ) {
            return btn;
        }
    }
    return null;
}

难点三:浏览器扩展权限配置

Manifest V3版本的权限配置需要仔细规划:

{
    "manifest_version": 3,
    "name": "豆包视频文案提取",
    "version": "1.0.0",
    "permissions": [
        "activeTab",
        "storage",
        "tabs",
        "notifications"
    ],
    "host_permissions": [
        "https://*.douyin.com/*",
        "https://www.doubao.com/*"
    ],
    "content_scripts": [{
        "matches": [
            "https://*.douyin.com/*",
            "https://www.doubao.com/chat*"
        ],
        "js": ["content.js"],
        "run_at": "document_end"
    }]
}

关键点说明:

  • storage:用于popup、content script、background script之间共享数据
  • tabs:允许创建新标签页/窗口
  • notifications:显示系统通知
  • host_permissions:声明需要访问的域名(豆包需要主动授权)

5. 成果展示

最终实现的工具包含两个组件:

油猴脚本功能

  • 多关键词批量搜索
  • 自动滚动收集视频信息
  • 视频筛选(点赞数、发布时间)
  • 结果表格展示,支持复选框选择
  • 发送到豆包扩展
  • 导出Excel

浏览器扩展功能

  • 自动弹出任务窗口
  • 批量处理视频链接
  • 豆包网页自动填充与发送
  • AI输出监控与文案提取
  • 最终Excel生成(包含视频元数据+AI文案)

产出示例

排名 来源关键词 作者 视频名称 发布时间 点赞数量 视频链接 提取到的文案
1 高考志愿 某UP主 三步报好志愿的方法 2024-01-01 50000 点击查看 今天教大家一个方法,三步报出好志愿…

6. 效果与总结

提效数据

  • 原始流程:3-4小时(人工操作)
  • 自动化后:15-20分钟(自动运行,中间可做其他事)
  • 效率提升:约90%

AI工作方式的思考

  1. 任务拆解的重要性:将大需求拆解为独立的子任务(油猴脚本+浏览器扩展),每个工具做自己擅长的事,通过标准接口通信。

  2. 渐进式开发:不是一开始就想好所有细节,而是在开发过程中根据实际遇到的问题不断调整方案。

  3. 用户体验优先:最初的设计可能很理想,但实际使用时发现不便,需要勇于推翻重来,选择用户最容易接受的方案。

  4. 工具协作优于全能工具:让专业工具做专业的事。油猴脚本擅长页面内操作,浏览器扩展擅长跨站点交互,两者通过事件机制解耦,既保持了灵活性,又便于单独维护和升级。