obsidian dataviewjs查找冗余文件

一、介绍

1.1 提出问题

起因我在整理网盘时,发现网盘的文件清理功能可以找出冗余的文件。然后扫描出我很多以前照片,pdf文档都是复制到了多个目录里。不过网盘上去删除这些重复的文件并没有本地的文件夹操作方便。

1.2 确定目标

于是我的目标就是:在本地用程序找出重复的文件。
因为obsidian已经成为了我的一个文件管理工具,而不仅仅只是markdown的笔记管理软件。我的一个更具体的目标就是能否在obsidian中实现冗余文件的查找。

二、调查方案

网上找了很多方案。对比如下:

方案1:专门的软件进行磁盘扫描。
优点:现成解决方案。
缺点:需要额外安装软件。不好集成到obsidian,因为我的文件管理是用obsidian所以还是希望找一个可以集成到obsidian。

方案2:用python程序自己实现。
优点:可以自己定制功能,自动化处理。
缺点:不好集成。

方案3:obsidian插件。
Local Images Plus插件,可以使用md5算法找出相同的照片。
awesome-image插件
unique-attachments:可以对照片一个hash值作为文件名。缺点:自动化命名文件导致文件名失去了可读性,为此我不得不又花了整天时间去恢复文件名,还是谨慎使用。
awesome-obsidian插件:不在插件市场里,需要自己手动安装。

目前遇到的插件的缺点:不能定制,不可控,照片名重命名后导致文档库需要重新整理。而且这些插件并不完全符合我清除冗余文件的目的。

方案4:obsidian用dataview的js语法实现。
优点:写程序逻辑,理论上可以实现很复杂的功能。
缺点:要编程技能,得益于AI时代可以帮助写代码。

三、实现方法

开发者文档: https://docs.obsidian.md/Home
经过调查所有的方案后我决定采用dataview的js程序实现。关于dataview的介绍我在另一个分享文档曾经介绍使用过,本文直接列举不同的解决方法。

3.1 方法1:找出所有重名md文件

效果:遍历dv.pages() 主要获取的是被视为页面(通常是 Markdown 文件)的数据。将文件名加入数组/hash表,遇到同名md文件就统计加1。最后展示数组/hash表中统计大于1的md文件。
js代码如下,这段代码来自obsidian论坛

//dataviewjs
// 假设您的对象数组为 data
const data = dv.pages();

let countMap = {}; // 用于存储计数的对象
let duplicates = []; // 用于存储重复元素的数组

// 遍历对象数组
data.forEach((element) => {
  let fileName = element.file.name;
  let filePath = element.file.path;

  // 计数
  if (countMap[fileName]) {
    countMap[fileName].count++;
    countMap[fileName].paths.push(filePath);
  } else {
    countMap[fileName] = { count: 1, paths: [filePath] };
  }
});

let dup=0//这是发现了几个dup的name
let flag =0//有重名文件的标志
for (const key in countMap) {
    const element = countMap[key];
    if (element.count > 1) {
      dup++;
      if(dup>0&&flag==0){dv.paragraph("==有重名文件==");flag=1}
      dv.span(`《${key}.md》出现了${element.count}次`);
      const pathstolink = element.paths.map(path => `[[${path}]]`);
      dv.list(pathstolink);

    }
}
if(dup==0){
dv.span("没有重名文件")
}

3.2 方法2:找出所有重名非md文件

3.1节方法存在只能找md文件,但往往大文件都是一些压缩包,pdf,照片文件。所以反而应该排除掉md文件,md文件很容易在自己写笔记的过程就能发现。要解决数据范围问题‌:对于一些非 Markdown 的文件,可能不在 dv.pages() 所涵盖的范围内。如果要获取所有文件(包括非 Markdown 文件),可以考虑使用 app.vault.getFiles() 等方式先获取所有文件,再进行过滤。

效果:过滤排除掉markdown文件的代码。

//dataviewjs
// 获取所有文件
const allFiles = app.vault.getFiles();
// 过滤出非md文件
const nonMdFiles = allFiles.filter(file => file.extension!== "md");
// 展示结果
dv.table(
    ["文件名", "文件扩展名"],
    nonMdFiles.map(file => [file.name, file.extension])
);

3.2.1 列表展示同名非md文件

js代码如下

// 假设您的对象数组为 data
//const data = dv.pages();


// 过滤非 Markdown 文件
const allFiles = app.vault.getFiles();
const nonMdFiles = allFiles.filter(file => !file.path.endsWith('.md'));
    
let countMap = {}; // 用于存储计数的对象
let duplicates = []; // 用于存储重复元素的数组

//const data = dv.pages().where(p => !p.file.path.endsWith(".md"));
//    .sort(p => p.file.name);

// 遍历对象数组
for (let file of nonMdFiles){
  let fileName = file.name;
  let filePath = file.path;
  
  // 计数
  if (countMap[fileName]) {
    countMap[fileName].count++;
    countMap[fileName].paths.push(filePath);
  } else {
    countMap[fileName] = { count: 1, paths: [filePath] };
  }
}

let dup=0//这是发现了几个dup的name
let flag =0//有重名文件的标志
const MAX_DUPLICATE_GROUPS = 2;

for (const key in countMap) {
	if (dup >= MAX_DUPLICATE_GROUPS) {
		break;
	}

    const element = countMap[key];
    if (element.count > 1) {
      dup++;
      if(dup>0&&flag==0){dv.paragraph("==有重名文件==");flag=1}
      dv.span(`${key}出现了${element.count}次`);
      const pathstolink = element.paths.map(path => `[[${path}]]`);
      dv.list(pathstolink); //列表格式
	  //dv.table(pathstolink);
	  
	  dv.table(
            ['文件名', '文件类型', '文件大小', 'MD5值'],
            [
                key,
                element.paths,
                element.paths,
                element.paths,
            ]
        );
    }
}
if(dup==0){
dv.span("没有重名文件")
}

3.2.2 表格展示文件名+文件大小

不但要比较文件名,还要比较大小。

AI提问:obsidian dataview优化这段代码,表格展示文件路径名和文件大小
用file name作为key记录hash表,然后统计出现次数
应该以md5作为key

js代码如下

//dataviewjs
// 过滤非 Markdown 文件
const allFiles = app.vault.getFiles();
const nonMdFiles = allFiles.filter(file =>!file.path.endsWith('.md'));

// 用于存储计数的对象
const countMap = {};
// 用于存储重复元素的数组
const duplicates = [];

// 遍历对象数组,统计文件出现次数和路径
for (let file of nonMdFiles) {
    const fileName = file.name;
    if (!countMap[fileName]) {
        countMap[fileName] = {
            count: 1,
            paths: [file.path],
            size: file.stat.size // 记录文件大小
        };
    } else {
        countMap[fileName].count++;
        countMap[fileName].paths.push(file.path);
    }
}

let dupCount = 0; // 发现的重名文件数量
const MAX_DUPLICATE_GROUPS = 20;

// 遍历统计结果,处理重名文件
for (const key in countMap) {
    if (dupCount >= MAX_DUPLICATE_GROUPS) {
        break;
    }
    const element = countMap[key];
    if (element.count > 1) {
        dupCount++;
        if (dupCount === 1) {
            dv.paragraph("==有重名文件==");
        }
        dv.span(`${key}出现了${element.count}次`);
        const tableData = element.paths.map(path => {
            const file = app.vault.getAbstractFileByPath(path);
            return [`[[${path}]]`, file.stat.size + ' bytes']; // 准备表格数据,包含路径和大小
        });
        dv.table(['文件路径', '文件大小'], tableData); // 展示表格
    }
}

if (dupCount === 0) {
    dv.span("没有重名文件");
}

3.2.3 表格展示过滤出相等的文件名+文件大小

效果:查找性能高。原理:用文件名+文件大小作为key。
js代码如下

//dataviewjs
// 过滤非 Markdown 文件
const allFiles = app.vault.getFiles();
const nonMdFiles = allFiles.filter(file =>!file.path.endsWith('.md'));

// 用于存储计数的对象
const countMap = {};
// 用于存储重复元素的数组
const duplicates = [];

// 遍历对象数组,统计文件出现次数和路径
for (let file of nonMdFiles) {
    const fileName = file.name;
    const fileSize = file.stat.size;
    
    // 构建唯一键(文件名+文件大小,确保仅文件名和大小完全相同时匹配)
    const key = `${fileName}-${fileSize}`;
    
    if (!countMap[key]) {
        countMap[key] = {
            count: 1,
            paths: [file.path],
            size: file.stat.size // 记录文件大小
        };
    } else {
        countMap[key].count++;
        countMap[key].paths.push(file.path);
    }
}

let dupCount = 0; // 发现的重名文件数量
const MAX_DUPLICATE_GROUPS = 200;

// 遍历统计结果,处理重名文件
for (const key in countMap) {
    if (dupCount >= MAX_DUPLICATE_GROUPS) {
        break;
    }
    const element = countMap[key];
    if (element.count > 1) {
        dupCount++;
        if (dupCount === 1) {
            dv.paragraph("==有重名文件==");
        }
        dv.span(`${key}出现了${element.count}次`);
        const tableData = element.paths.map(path => {
            const file = app.vault.getAbstractFileByPath(path);
            return [`[[${path}]]`, file.stat.size + ' bytes']; // 准备表格数据,包含路径和大小
        });
        dv.table(['文件路径', '文件大小'], tableData); // 展示表格
    }
}

if (dupCount === 0) {
    dv.span("扫描完成,没有重名文件!");
}

3.3 方法3:文件唯一性MD5值

文件的唯一确定其实是用文件的MD5值标识。只要两个文件的内容完全一样那MD5值也一样。文件名可以重命名成不同命,但MD5不会骗人。

3.3.1 表格展示所有文件的MD5值

AI提问: obsidian dataview展示每个文件(不仅仅是markdown文件)的md5值

// 统计所有类型文件
const files = app.vault.getAllLoadedFiles();

js代码如下

//dataviewjs
// 引入crypto模块用于计算MD5
const crypto = require('crypto');

// 获取所有文件,并过滤掉Markdown文件
const files = app.vault.getFiles().filter(file => file.extension !== "md");

// 创建表格数据
const tableData = await Promise.all(files.map(async (file) => {
    try {
        // 读取文件内容
        const content = await app.vault.read(file);
        
        // 计算MD5哈希值
        const hash = crypto.createHash('md5');
        hash.update(content);
        const md5Hash = hash.digest('hex');
        
        return {
            文件: file.link,
            文件名: file.name,
            文件夹: file.path,
            MD5值: md5Hash,
            文件大小: file.stat.size,
            修改时间: file.stat.mtime
        };
    } catch (error) {
        console.error(`计算文件 ${file.name} 的MD5时出错:`, error);
        return null;
    }
}));

// 过滤掉计算失败的文件
const validData = tableData.filter(item => item !== null);

// 生成表格
dv.table(["文件", "文件名", "MD5值", "文件大小", "修改时间"], 
    validData.map(item => [
        item.文件,
        item.文件名,
        item.MD5值,
        `${(item.文件大小 / 1024).toFixed(2)} KB`,
    new Date(item.修改时间).toLocaleString()
]));

3.3.2 用MD5作为hash key

效果:通过使用MD5值作为hash表的key存储文件信息。我的仓库有5000个文件,使用后查找时间过长,我分析是因为计算MD5值消耗时间太多。实际时我只保留前几个查找结果。

js代码如下

//dataviewjs
// 过滤非 Markdown 文件
const allFiles = app.vault.getFiles();
const nonMdFiles = allFiles.filter(file =>!file.path.endsWith('.md'));

// 引入CryptoJS库用于计算MD5
const crypto = require('crypto');

// 用于存储计数的对象
const countMap = {};
// 用于存储重复元素的数组
const duplicates = [];

// 遍历对象数组,统计文件出现次数和路径
for (let file of nonMdFiles) {
    const fileName = file.name;
    
    // 读取文件内容
    const fileContent = await app.vault.read(file);

    // 计算MD5值
	const hash = crypto.createHash('md5');
	hash.update(fileContent);
	const md5 = hash.digest('hex');

    if (!countMap[md5]) {
        countMap[md5] = {
            count: 1,
            paths: [file.path],
            size: file.stat.size // 记录文件大小
        };
    } else {
        countMap[md5].count++;
        countMap[md5].paths.push(file.path);
    }
}

let dupCount = 0; // 发现的重名文件数量
const MAX_DUPLICATE_GROUPS = 2;

// 遍历统计结果,处理重名文件
for (const key in countMap) {
    if (dupCount >= MAX_DUPLICATE_GROUPS) {
        break;
    }
    const element = countMap[key];
    if (element.count > 1) {
        dupCount++;
        if (dupCount === 1) {
            dv.paragraph("==有重名文件==");
        }
        dv.span(`${key}出现了${element.count}次`);
        const tableData = element.paths.map(path => {
            const file = app.vault.getAbstractFileByPath(path);
            return [path, file.stat.size + ' bytes']; // 准备表格数据,包含路径和大小
        });
        dv.table(['文件路径', '文件大小'], tableData); // 展示表格
    }
}

if (dupCount === 0) {
    dv.span("没有重名文件");
}

3.3.3 表格显示前两组MD5相同文件

AI提问: 代码是dataview中统计同名文件并展示。请将这段代码改造为: obsidian dataview中展示每个文件(不仅仅是markdown文件)的md5值。需要过滤并展示出具有相同md5的文件,以列表格式展示,列表应该包含文件完整路径名,md5值。

js代码如下

//dataviewjs
// 引入CryptoJS库用于计算MD5
const crypto = require('crypto');

// 获取所有非markdown文件
const files = app.vault.getFiles().filter(file => file.extension !== "md");


// 存储MD5值和对应文件信息的对象
const md5Map = {};
const fileInfoMap = {};
let duplicateGroups = 0;
const MAX_DUPLICATE_GROUPS = 2;


// 异步计算每个文件的MD5值
for (const file of files) {
	if (duplicateGroups >= MAX_DUPLICATE_GROUPS) {
		break;
	}

    try {
        // 读取文件内容
        const fileContent = await app.vault.read(file);
        
        // 计算MD5值
        const hash = crypto.createHash('md5');
        hash.update(fileContent);
        const md5 = hash.digest('hex');
        
        // 存储文件信息
        const fileInfo = {
            name: file.name,
            path: file.path,
            extension: file.extension,
            size: file.stat.size,
            mtime: file.stat.mtime
        };
        
        // 存储到映射中
        if (md5Map[md5]) {
            md5Map[md5].push(fileInfo);
            duplicateGroups++;
        } else {
            md5Map[md5] = [fileInfo];
        }
        
        fileInfoMap[file.path] = {
            md5: md5,
            ...fileInfo
        };
        
    } catch (error) {
        console.log(`无法读取文件 ${file.path}: ${error}`);
        // 对于无法读取的文件,使用文件路径和大小计算MD5
        const fallbackContent = file.path + file.stat.size;
        //const fallbackMD5 = crypto.MD5(fallbackContent).toString();
        const fallbackhash = crypto.createHash('md5');
        fallbackhash.update(fallbackContent);
        const fallbackMD5 = hash.digest('hex');
        
        const fileInfo = {
            name: file.name,
            path: file.path,
            extension: file.extension,
            size: file.stat.size,
            mtime: file.stat.mtime,
            error: true
        };
        
        if (md5Map[fallbackMD5]) {
            md5Map[fallbackMD5].push(fileInfo);
        } else {
            md5Map[fallbackMD5] = [fileInfo];
        }
        
        fileInfoMap[file.path] = {
            md5: fallbackMD5,
            ...fileInfo
        };
    }
}

// 过滤出有重复MD5的文件组
const duplicates = {};
let hasDuplicates = false;

for (const [md5, fileInfos] of Object.entries(md5Map)) {
    if (fileInfos.length > 1) {
        duplicates[md5] = fileInfos;
        hasDuplicates = true;
    }
}

// 展示重复文件表格
if (hasDuplicates) {
    dv.paragraph("##  MD5重复文件检测结果");
    dv.paragraph(`共发现 **${Object.keys(duplicates).length}** 组重复文件`);
    
    // 为每个重复组创建表格
    let groupCount = 1;
    for (const [md5, fileInfos] of Object.entries(duplicates)) {
        dv.paragraph(`### 重复文件组 ${groupCount} (MD5: \`${md5.substring(0, 16)}...\`)`);
        
        // 创建表格头部
        const headers = ["文件名", "文件路径", "类型", "大小", "修改时间", "操作"];
        
        // 创建表格数据
        const tableData = fileInfos.map(info => [
            info.name,
            info.path,
            info.extension,
            `${(info.size / 1024).toFixed(2)} KB`,
            new Date(info.mtime).toLocaleString(),
            `[[${info.path}]]`
        ]);
        
        // 渲染表格
        dv.table(headers, tableData);
        groupCount++;
    }
} else {
    dv.paragraph("## Y 未发现MD5重复文件");
    dv.paragraph("所有文件的MD5值都是唯一的。");
}

// 显示统计信息
dv.paragraph("---");
dv.paragraph("##  文件MD5统计信息");

const statsData = [
    ["总文件数", files.length],
    ["唯一MD5值数", Object.keys(md5Map).length],
    ["重复文件组数", Object.keys(duplicates).length],
    ["重复文件总数", Object.values(duplicates).reduce((sum, group) => sum + group.length, 0)]
];

dv.table(["统计项", "数量"], statsData);

// 可选:显示所有文件的MD5值(前20个作为示例)
dv.paragraph("---");
dv.paragraph("##  文件MD5值示例(前20个)");

const sampleFiles = files.slice(0, 20);
const sampleTableData = sampleFiles.map(file => {
    const info = fileInfoMap[file.path];
    return [
        file.name,
        file.extension,
        `${(file.stat.size / 1024).toFixed(2)} KB`,
        info ? `\`${info.md5.substring(0, 16)}...\`` : "计算失败",
        info?.error ? "N" : "Y"
    ];
});

dv.table(["文件名", "类型", "大小", "MD5(前16位)", "状态"], sampleTableData);
posted @ 2025-12-06 22:59  liqinglucky  阅读(17)  评论(0)    收藏  举报