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);

浙公网安备 33010602011771号