Files
2026-06-19 18:45:55 +08:00

249 lines
6.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 抖音内容解析器 - 优化版
* 特点:
* 1. 不需要打开浏览器
* 2. 支持抖音分享链接格式解析
* 3. 智能提取视频文案、链接、话题标签
*/
interface DouyinContent {
videoUrl: string; // 提取的视频链接
description: string; // 视频文案
hashtags: string[]; // 话题标签
rawText: string; // 原始文本
}
interface ParseResult {
success: boolean;
content?: DouyinContent;
error?: string;
}
class DouyinContentParser {
private static readonly DIRECT_VIDEO_PATTERN = /https?:\/\/[^\s"'<>]+?\.(mp4|mov|m4v|webm|avi|mkv)(\?[^\s"'<>]*)?/i;
/**
* 从混杂文本中提取抖音内容
* 支持格式:
* 1. 纯链接:https://v.douyin.com/HjUnkVdzqU/
* 2. 分享格式:文案 + 链接 + 说明文字
* 3. 富文本:包含话题标签、emoji等
*/
static parseContent(input: string): ParseResult {
try {
const raw = input.trim();
if (!raw) {
return {
success: false,
error: '输入内容为空'
};
}
// 1. 提取视频URL
const videoUrl = this.extractVideoUrl(raw);
if (!videoUrl) {
return {
success: false,
error: '未找到有效的抖音视频链接'
};
}
// 2. 提取视频文案(从文本中去掉URL和说明文字后的内容)
const description = this.extractDescription(raw, videoUrl);
// 3. 提取话题标签
const hashtags = this.extractHashtags(raw);
return {
success: true,
content: {
videoUrl,
description,
hashtags,
rawText: raw
}
};
} catch (error) {
return {
success: false,
error: `解析失败: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* 提取视频URL
* 支持多种格式:
* - https://v.douyin.com/HjUnkVdzqU/
* - https://www.douyin.com/video/7123456789
* - https://iesdouyin.com/...
*/
private static extractVideoUrl(text: string): string | null {
const directVideoMatch = text.match(this.DIRECT_VIDEO_PATTERN);
if (directVideoMatch?.[0]) {
return directVideoMatch[0];
}
// 多种URL模式
const patterns = [
// 短链接格式:https://v.douyin.com/HjUnkVdzqU/
/https:\/\/v\.douyin\.com\/[\w_\-@]+\/?/i,
// www格式:https://www.douyin.com/video/7123456789
/https:\/\/(?:www\.)?douyin\.com\/(?:video|modal)\/\d+/i,
// iesdouyin格式
/https:\/\/iesdouyin\.com\/(?:share\/video|video)\/\d+/i,
// 简化版本(不带https
/v\.douyin\.com\/[\w_\-@]+/i,
/douyin\.com\/(?:video|modal)\/\d+/i
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match) {
let url = match[0];
// 补充 https:// 前缀(如果缺失)
if (!url.startsWith('http')) {
url = 'https://' + url;
}
// 确保以斜杠结尾(短链接)或不重复(标准链接)
return url;
}
}
return null;
}
static isDirectVideoUrl(url: string): boolean {
return this.DIRECT_VIDEO_PATTERN.test(url.trim());
}
/**
* 提取视频文案
* 规则:
* 1. 从第一个非whitespace字符开始
* 2. 到URL出现前的所有文本
* 3. 去掉尾部的"复制此链接"等说明文字
*/
private static extractDescription(text: string, videoUrl: string): string {
// 找到URL在文本中的位置
const urlIndex = text.indexOf(videoUrl);
let descriptionText = '';
if (urlIndex > 0) {
// URL前面的文本可能是文案
descriptionText = text.substring(0, urlIndex).trim();
} else {
// 如果找不到完整URL,尝试提取URL后的内容
// 这种情况很少,但作为后备方案
const descriptionMatch = text.match(/^([^h]*?)(?:https?:\/\/|$)/);
if (descriptionMatch && descriptionMatch[1]) {
descriptionText = descriptionMatch[1].trim();
}
}
// 清理文案中可能存在的噪音
descriptionText = this.cleanDescription(descriptionText);
return descriptionText;
}
/**
* 清理文案中的噪音
* 1. 去掉行号标记(2.05 03/10 等)
* 2. 去掉乱码和特殊前缀
* 3. 保留正常内容
*/
private static cleanDescription(text: string): string {
if (!text) return '';
// 去掉开头的时间戳或版本号(如 "2.05 03/10"
let cleaned = text.replace(/^[\d\s\.\/]+/, '').trim();
// 去掉开头的乱码或特殊字符(如 "i@p.Qx rRX:/" 这样的)
// 如果开头包含太多特殊字符和数字的混合,说明是乱码
const startMatch = cleaned.match(/^[^\u4e00-\u9fff\w\s]*(.*)$/);
if (startMatch && startMatch[1]) {
// 如果清理后还有内容,就用清理后的
const potential = startMatch[1].trim();
if (potential.length > 0) {
cleaned = potential;
}
}
// 如果清理后发现全是特殊字符或数字,返回空
if (/^[\W_]+$/.test(cleaned)) {
return '';
}
return cleaned;
}
/**
* 提取话题标签
* 支持:#话题、#话题名、@用户 等格式
*/
private static extractHashtags(text: string): string[] {
const hashtags: string[] = [];
// 匹配 #话题 格式(包括中英文)
const hashtagPattern = /#[\w\u4e00-\u9fff_]+/g;
const matches = text.match(hashtagPattern);
if (matches) {
// 去重并返回
hashtags.push(...Array.from(new Set(matches)));
}
return hashtags;
}
/**
* 提取视频ID(用于后续处理)
*/
static extractVideoId(videoUrl: string): string | null {
// 短链接格式:v.douyin.com/HjUnkVdzqU/ -> HjUnkVdzqU
const shortMatch = videoUrl.match(/v\.douyin\.com\/([\w_\-@]+)/i);
if (shortMatch) {
return shortMatch[1];
}
// 标准链接格式:/video/7123456789 -> 7123456789
const standardMatch = videoUrl.match(/\/(?:video|modal)\/(\d+)/i);
if (standardMatch) {
return standardMatch[1];
}
return null;
}
/**
* 为文案生成一个合理的标题
* 规则:从文案中截取第一句话,或前N个字符
*/
static generateTitleFromDescription(description: string, maxLength: number = 30): string {
if (!description) {
return '分享精彩内容';
}
// 尝试找到第一个句号、问号或感叹号
const sentenceEnd = description.match(/[。!?\n]/);
let title = '';
if (sentenceEnd) {
title = description.substring(0, sentenceEnd.index).trim();
} else {
title = description;
}
// 限制长度
if (title.length > maxLength) {
title = title.substring(0, maxLength) + '...';
}
return title || '精彩分享';
}
}
export { DouyinContentParser, DouyinContent, ParseResult };