【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进行间接通信
解决方案:采用双重通信机制
- DOM事件机制:网页可以触发DOM事件,Content Script可以监听
- 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工作方式的思考
-
任务拆解的重要性:将大需求拆解为独立的子任务(油猴脚本+浏览器扩展),每个工具做自己擅长的事,通过标准接口通信。
-
渐进式开发:不是一开始就想好所有细节,而是在开发过程中根据实际遇到的问题不断调整方案。
-
用户体验优先:最初的设计可能很理想,但实际使用时发现不便,需要勇于推翻重来,选择用户最容易接受的方案。
-
工具协作优于全能工具:让专业工具做专业的事。油猴脚本擅长页面内操作,浏览器扩展擅长跨站点交互,两者通过事件机制解耦,既保持了灵活性,又便于单独维护和升级。



