关于 deduplication
用于去重多来源数据的处理技能。它会按语义相似度分组,并依据来源声誉和记录完整度等条件选择规范版本。
主要功能
- 01按 ID 处理重复项
- 02语义内容分组
- 03选择规范记录
- 04来源声誉评分
- 05去重缩减指标
适用场景
- 01清理多来源数据流中的重复项
- 02为同一新闻选择规范版本
- 03ID 冲突时保留信息更完整的记录
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ dadbodgeoff/ drift --skill deduplication安装后在 Claude Code 中输入/deduplication确认已加载。
详细介绍
事件去重
使用信誉评分和基于哈希的分组,从多个来源中选择规范版本。
何时使用此技能
- 汇总来自多个来源的数据(新闻、事件、产品)
- 同一内容出现在不同媒体或来源中
- 需要从重复内容中选出“最佳”版本
- 跟踪去重指标以进行优化
核心概念
简单的 URL 去重还不够。生产环境还需要:
- 按语义相似度分组(同一事件,不同媒体)
- 选择规范版本(选出“最佳”版本)
- 信誉评分(优先选择权威来源)
- 同时支持基于 ID 和基于内容的去重
两种模式:
- 基于 ID:来源具有唯一 ID 时,在 ID 冲突时保留“最佳”版本
- 基于内容:按语义相似度分组,并从每组中选出规范版本
实现
TypeScript
import { createHash } from 'crypto';
interface DeduplicationResult<T> {
items: T[];
originalCount: number;
dedupedCount: number;
reductionPercent: number;
duplicateGroups?: number;
}
// ============================================
// ID-Based Deduplication
// ============================================
function deduplicateById<T extends { id: string }>(
items: T[],
preferFn: (existing: T, candidate: T) => T
): DeduplicationResult<T> {
const seen = new Map<string, T>();
for (const item of items) {
const existing = seen.get(item.id);
if (existing) {
seen.set(item.id, preferFn(existing, item));
} else {
seen.set(item.id, item);
}
}
const dedupedItems = Array.from(seen.values());
const reductionPercent = items.length > 0
? Math.round((1 - dedupedItems.length / items.length) * 100)
: 0;
return {
items: dedupedItems,
originalCount: items.length,
dedupedCount: dedupedItems.length,
reductionPercent,
};
}
// ============================================
// Content-Based Deduplication
// ============================================
interface Article {
title: string;
url: string;
domain: string;
publishedAt: string;
tone?: number;
}
/**
* Generate deduplication key from content
* Groups by: normalized title + source country + date
*/
function generateDedupKey(article: Article): string {
const normalizedTitle = article.title
.toLowerCase()
.replace(/[^\w\s]/g, '')
.trim()
.slice(0, 50);
const dateStr = article.publishedAt?.slice(0, 10).replace(/-/g, '') || 'unknown';
return `${normalizedTitle}|${dateStr}`;
}
/**
* Generate unique ID from URL
*/
function generateEventId(url: string): string {
return createHash('md5').update(url).digest('hex').slice(0, 12);
}
/**
* Source reputation scoring
*/
function getReputationScore(domain: string): number {
// Tier 1: Wire services and major international
const tier1 = ['reuters.com', 'apnews.com', 'bbc.com', 'bbc.co.uk',
'aljazeera.com', 'france24.com', 'dw.com'];
if (tier1.some(r => domain.includes(r))) return 100;
// Tier 2: Major newspapers
const tier2 = ['nytimes.com', 'washingtonpost.com', 'theguardian.com',
'ft.com', 'economist.com', 'wsj.com'];
if (tier2.some(r => domain.includes(r))) return 75;
// Tier 3: Regional/national
const tier3 = ['cnn.com', 'foxnews.com', 'nbcnews.com', 'abcnews.go.com'];
if (tier3.some(r => domain.includes(r))) return 50;
return 10;
}
/**
* Select canonical article from duplicate group
*/
function selectCanonical<T extends Article>(
group: { item: T; source: string }[]
): { item: T; source: string } {
return group.reduce((best, current) => {
const bestScore = getReputationScore(best.item.domain) +
Math.abs(best.item.tone || 0);
const currentScore = getReputationScore(current.item.domain) +
Math.abs(current.item.tone || 0);
return currentScore > bestScore ? current : best;
});
}
/**
* Deduplicate articles from multiple sources
*/
function deduplicateArticles<T extends Article>(
sourceResults: { sourceName: string; articles: T[] }[]
): DeduplicationResult<T & { source: string }> {
const groups = new Map<string, { item: T; source: string }[]>();
let totalArticles = 0;
// Group articles by dedup key
for (const { sourceName, articles } of sourceResults) {
for (const article of articles) {
totalArticles++;
const key = generateDedupKey(article);
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key)!.push({ item: article, source: sourceName });
}
}
// Select canonical article from each group
const items: (T & { source: string })[] = [];
for (const group of groups.values()) {
const canonical = selectCanonical(group);
items.push({ ...canonical.item, source: canonical.source });
}
const reductionPercent = totalArticles > 0
? Math.round((1 - items.length / totalArticles) * 100)
: 0;
console.log(`[Dedup] ${totalArticles} → ${items.length} (${reductionPercent}% reduction)`);
return {
items,
originalCount: totalArticles,
dedupedCount: items.length,
reductionPercent,
duplicateGroups: groups.size,
};
}
使用示例
基于 ID 的去重
const events = await fetchEvents();
const result = deduplicateById(events, (existing, candidate) => {
// Prefer events with coordinates
if (!existing.lat && candidate.lat) return candidate;
// Prefer higher sentiment magnitude
if (Math.abs(candidate.sentiment) > Math.abs(existing.sentiment)) {
return candidate;
}
return existing;
});
console.log(`Reduced ${result.reductionPercent}% duplicates`);
多来源汇总
const results = await Promise.all([
fetchFromSourceA(),
fetchFromSourceB(),
fetchFromSourceC(),
]);
const { items, reductionPercent } = deduplicateArticles([
{ sourceName: 'source-a', articles: results[0] },
{ sourceName: 'source-b', articles: results[1] },
{ sourceName: 'source-c', articles: results[2] },
]);
// items now contains canonical articles with source attribution
最佳实践
- 语义分组 - 按规范化内容分组,而不只是按 URL
- 信誉评分 - 优先选择权威来源作为规范来源
- 选择最佳版本 - ID 冲突时,保留数据最完整的版本
- 跟踪缩减情况 - 记录去重带来的效果
- 标注来源 - 跟踪规范版本来自哪个来源
常见错误
- 仅按 URL 去重(无法识别来自不同媒体的同一事件)
- 随机选择重复项(丢失质量信号)
- 不进行规范化(大小写或标点差异会造成漏判)
- 不跟踪缩减指标(无法进行优化)
- 硬编码来源列表(应使其可配置)
相关模式
- batch-processing - 高效处理去重后的条目
- validation-quarantine - 去重前进行验证
- checkpoint-resume - 跟踪哪些文件已完成去重
评论
支持 Markdown