使用 Node.js 查找重复文件

1,169 阅读6分钟

本文将介绍如何使用 Node.js 实现一个重复文件查找的功能。通过利用哈希算法,我们可以高效地计算文件的唯一标识,从而快速识别出重复的文件。

像素比较法

一种常见的判断两张图片是否相同的方法是通过比较它们的像素数据。对于每张图片,我们可以遍历每个像素点,比较它们的色值是否一致。以下是实现的基本代码:

function comparePhotos() {
  var ctx1 = canvas1.getContext('2d');
  var ctx2 = canvas2.getContext('2d');

  // 省略部分代码

  var imageData1 = ctx1.getImageData(0, 0, width, height);
  var imageData2 = ctx2.getImageData(0, 0, width, height);

  for (var i = 0; i < imageData1.data.length; i += 4) {
    if (
      imageData1.data[i] !== imageData2.data[i] ||
      imageData1.data[i + 1] !== imageData2.data[i + 1] ||
      imageData1.data[i + 2] !== imageData2.data[i + 2] ||
      imageData1.data[i + 3] !== imageData2.data[i + 3]
    ) {
      return false
    }
  }

  return true
}

缺点

  • 性能较差,尤其是对于大尺寸的图片,逐像素比较的开销非常大。
  • 内存占用较高,getImageData 会将图片的所有像素数据加载到内存中。

因此,在处理大量图片时,像素比较法可能导致明显的性能问题。

哈希值比较法

为了解决性能问题,我们可以通过计算文件的哈希值来判断文件内容是否相同。哈希算法可以将文件内容映射为一个固定长度的哈希值。只要文件内容相同,它们的哈希值就一定相同。这种方法避免了逐像素比较,可以大大提高效率,尤其适合用于处理大量图片。

以下是使用哈希值来查找重复图片的实现:

const crypto = require('node:crypto')
const fs = require('node:fs')
const path = require('node:path')
const kolorist = require('kolorist')

// 判断文件是否为图片文件
function isImageFile(file) {
  const exts = ['.jpg', '.jpeg', '.png']
  const extname = path.extname(file).toLowerCase()
  return exts.includes(extname)
}

// 计算文件的哈希值
function calcHash(filePath) {
  const hash = crypto.createHash('sha1')
  const data = fs.readFileSync(filePath)

  hash.update(data)
  return hash.digest('hex')
}

// 查找重复照骗
function findDuplicateImages(folderPath) {
  const files = fs.readdirSync(folderPath)
  const duplicatesMap = new Map()

  for (const file of files) {
    const fullPath = path.join(folderPath, file)

    if (isImageFile(fullPath)) {
      const hash = calcHash(fullPath)

      if (duplicatesMap.has(hash)) {
        duplicatesMap.get(hash).push(file)
      } else {
        duplicatesMap.set(hash, [file])
      }
    }
  }

  const duplicates = [...duplicatesMap.values()].filter(files => files.length > 1)
  return duplicates
}

const folderPath = path.resolve(__dirname, './assets')
const duplicateImages = findDuplicateImages(folderPath)

console.log(kolorist.green(`一共发现 ${duplicateImages.length} 组重复照片`))
console.log(kolorist.blue(JSON.stringify(duplicateImages, null, 2)))

代码分析

  1. 判断图片文件:使用 path.extname 方法判断文件扩展名,确保只处理图片文件。
  2. 计算文件哈希值:使用 crypto 模块的 createHash 方法计算每个文件的哈希值。我们采用 SHA1 算法来生成哈希值。
  3. 查找重复图片:遍历指定文件夹,计算每个文件的哈希值,并将相同哈希值的文件归为一组。最终,筛选出重复的文件,并输出它们。

优点

  • 性能高效:相比像素比较法,哈希值比较只需计算一次文件内容的哈希值,避免了逐像素比较,大大提高了性能。
  • 适用于大文件:哈希值计算不依赖于图片的尺寸,对于大文件和大批量图片,性能表现更好。
  • 简单易用:使用 Node.js 内建的 crypto 和 fs 模块,代码简洁,易于理解和扩展。

测试一下效果,很完美。

1.png

优化与扩展

对于大文件的处理,我们可以将大文件分割成固定大小的块,并逐块计算哈希值,最后将所有块的哈希值再继续计算得到最终的哈希值,而不是一次性读取整个文件。这样可以减少内存占用并加快哈希计算的速度。

在查阅资料时发现七牛云存储公开了一个计算文件 hash 值的算法 qetag 来解决上传‘消重’问题。算法大体如下:

  • 如果你能够确认文件 <= 4M,那么 hash = UrlsafeBase64([0x16, sha1(FileContent)])。也就是,文件的内容的 sha1 值(20 个字节),前面加一个 byte(值为 0x16),构成 21 字节的二进制数据,然后对这 21 字节的数据做 urlsafe 的 base64 编码。

  • 如果文件 > 4M,则 hash = UrlsafeBase64([0x96, sha1([sha1(Block1), sha1(Block2), ...])]),其中 Block 是把文件内容切分为 4M 为单位的一个个块,也就是  BlockI = FileContent[I*4M:(I+1)*4M]

这很符合我的需求,替换上面的 calcHash 方法后一个简单的查找程序就完成了。接下来继续了解一下最核心的哈希算法。

哈希(Hash)算法

哈希算法,又称为散列算法,是一种常见的数学函数。它将任意长度的输入数据映射成固定长度的输出,这个输出值通常被称为哈希值(也称散列值或摘要值)。

可以简单描述为:digest = Hash(data)

哈希算法具有以下特点:

  1. 输入敏感:输入数据只要有微小的变化,都会导致产生完全不同的摘要。
  2. 不可逆性:哈希算法是单向的,即在知道输出的情况下,无法逆向推出输入数据。
  3. 正向快速:计算哈希值的过程非常迅速,无论输入数据的大小如何,输出的哈希值长度是固定的。
  4. 低碰撞率:对于不同的输入数据,产生相同摘要的概率应该极低。

Node.js 内置的加密模块提供了多种常见的哈希算法:

  • MD5:输出 128 位的哈希值,虽然性能较高,但由于已知的安全漏洞,通常不推荐用于安全相关的应用。
  • SHA-1:输出 160 位的哈希值,广泛应用于数字签名,但也存在一定的碰撞漏洞,逐渐被淘汰。
  • SHA-256:输出 256 位的哈希值,属于较为安全的哈希算法,广泛用于现代应用。
  • SHA-512:输出 512 位的哈希值,比 SHA-256 更安全,但计算时间较长。

哈希算法的输出长度越长,产生碰撞的概率越低,因此通常选用更长输出的算法会更安全。然而,较长的输出值也会导致计算时间的增加,所以在选择哈希算法时需要权衡性能和安全性。

哈希算法在许多领域都有广泛应用,常用于检验数据的一致性、加密存储、数字签名和去重等。

总结

本文介绍了如何利用哈希算法和 Node.js 加密模块实现重复照片查找功能。通过计算文件的哈希值,可以快速有效地判断图片是否相同。不过,哈希算法也有其局限性,比如无法处理图片内容的相似度检测。未来可以通过引入更加复杂的图像比较技术(如感知哈希)进一步优化该功能。

往期文章推荐: