【问题标题】:Powershell Speed: How to speed up ForEach-Object MD5/hash checkPowershell Speed:如何加快 ForEach-Object MD5/哈希检查
【发布时间】:2020-05-11 20:36:38
【问题描述】:

我正在对 5 亿个文件运行以下 MD5 检查以检查重复项。脚本需要永远运行,我想知道如何加快它。我怎样才能加快速度?当哈希已经存在时,我可以使用 try catch 循环而不是 contains 来引发错误吗?大家会推荐什么?

$folder = Read-Host -Prompt 'Enter a folder path'

$hash = @{}
$lineCheck = 0

Get-ChildItem $folder -Recurse | where {! $_.PSIsContainer} | ForEach-Object {
    $lineCheck++
    Write-Host $lineCheck
    $tempMD5 = (Get-FileHash -LiteralPath $_.FullName -Algorithm MD5).Hash;

    if(! $hash.Contains($tempMD5)){
        $hash.Add($tempMD5,$_.FullName)
    }
    else{
        Remove-Item -literalPath $_.fullname;
    }
} 

【问题讨论】:

  • "5 亿个文件"
  • 仍然会感谢任何有关如何加快速度的建议。
  • 散列的大小无关紧要 - 您的计算机仍然需要读取您正在处理的每个文件的每个字节 - 这是缓慢的部分。由于操作系统管理文件 IO 和缓存的方式,读取大量小文件也比读取少量大文件慢得多 - 即使在 SSD 上也是如此。
  • PowerShell 不是一种编写并行代码的好语言——如果你的程序是 IO 密集型(而不是 CPU 密集型),并行性根本不会帮助你——我强烈怀疑你的程序是非常非常受 IO 限制。
  • 做你目前正在做的事情,但首先要考虑文件的长度。对于您找到的每个匹配项,创建一个新表 $LengthMatch[$File1] = $File2(应该只有几个)然后只对 $LengthMatch 列表中的文件进行(缓慢)深入的 MD5 检查。

标签: powershell file hash parallel-processing md5


【解决方案1】:

按照 cmets 中的建议,您可以考虑在与首先找到的文件长度匹配的情况下开始散列文件。这意味着您不会为任何唯一的文件长度调用昂贵的哈希方法。

*注意:Write-Host 命令本身非常昂贵,因此我会显示每次迭代 (Write-Host $lineCheck) 但例如仅在找到匹配项时。

$Folder = Read-Host -Prompt 'Enter a folder path'

$FilesBySize = @{}
$FilesByHash = @{}

Function MatchHash([String]$FullName) {
    $Hash = (Get-FileHash -LiteralPath $FullName -Algorithm MD5).Hash
    $Found = $FilesByHash.Contains($Hash)
    If ($Found) {$Null = $FilesByHash[$Hash].Add($FullName)}
    Else {$FilesByHash[$Hash] = [System.Collections.ArrayList]@($FullName)}
    $Found
}

Get-ChildItem $Folder -Recurse | Where-Object -Not PSIsContainer | ForEach-Object {
    $Files = $FilesBySize[$_.Length]
    If ($Files) {
        If ($Files.Count -eq 1) {$Null = MatchHash $Files[0]}
        If ($Files.Count -ge 1) {If (MatchHash $_) {Write-Host 'Found match:' $_.FullName}}
        $Null = $FilesBySize[$_.Length].Add($_.FullName)
    } Else {
        $FilesBySize[$_.Length] = [System.Collections.ArrayList]@($_.FullName)
    }
}

显示找到的重复项:

ForEach($Hash in $FilesByHash.GetEnumerator()) {
    If ($Hash.Value.Count -gt 1) {
        Write-Host 'Hash:' $Hash.Name
        ForEach ($File in $Hash.Value) {
            Write-Host 'File:' $File
        }
    }
}

【讨论】:

  • 我喜欢独特的解决方案!这可以轻松加快散列所需的时间。
【解决方案2】:

我猜您的代码中最慢的部分是 Get-FileHash 调用,因为其他所有内容要么不是计算密集型的,要么受您的硬件(磁盘 IOPS)限制。

您可以尝试调用具有更优化 MD5 实现的本机工具来替换它,看看是否有帮助。

当哈希已经存在时,我可以使用 try catch 循环而不是 contains 来引发错误吗?

异常很慢,不建议将它们用于流量控制:

虽然使用异常处理程序来捕获错误和其他中断程序执行的事件是一种很好的做法,但将异常处理程序用作常规程序执行逻辑的一部分可能代价高昂,应该避免

实施它们的人 Chris Brumme 对此给出了明确的答案。他写了一篇关于这个主题的优秀博客文章(警告 - 很长)(警告2 - 写得很好,如果你是技术人员,你会读到最后,然后必须在下班后弥补你的时间:) )

执行摘要:它们很慢。它们是作为 Win32 SEH 异常实现的,因此有些甚至会通过 ring 0 CPU 边界!

【讨论】:

  • 看起来是您的权利。获取每个文件的哈希是迄今为止最慢的部分。根据这篇文章,PowerShell 仅在单核 (experts-exchange.com/questions/29148077/…) 上运行。这使得并行化变得困难。看来我需要切换到 Python 或 C#。
【解决方案3】:

我知道这是一个 PowerShell 问题,但您可以在 C# 中充分利用并行化。您还在其中一个 cmets 中提到了使用 C# 作为替代方案,所以我认为发布可能的实现方式不会有什么坏处。

您可以先创建一个方法来计算文件的 MD5 校验和:

private static string CalculateMD5(string filename)
{
    using var md5 = MD5.Create();
    using var stream = File.OpenRead(filename);
    var hash = md5.ComputeHash(stream);
    return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
}

然后您可以使用ParallelEnumerable.AsParallel() 查询所有文件哈希的并行方法:

private static IEnumerable<FileHash> FindFileHashes(string directoryPath)
{
    var allFiles = Directory
        .EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories);

    var hashedFiles = allFiles
        .AsParallel()
        .Select(filename => new FileHash { 
            FileName = filename, 
            Hash = CalculateMD5(filename) 
        });

    return hashedFiles;
}

那么就可以简单的使用上面的方法删除重复文件了:

private static void DeleteDuplicateFiles(string directoryPath)
{
    var fileHashes = new HashSet<string>();

    foreach (var fileHash in FindFileHashes(directoryPath))
    {
        if (!fileHashes.Contains(fileHash.Hash))
        {
            Console.WriteLine($"Found - File : {fileHash.FileName} Hash : {fileHash.Hash}");
            fileHashes.Add(fileHash.Hash);
            continue;
        }

        Console.WriteLine($"Deleting - File : {fileHash.FileName} Hash : {fileHash.Hash}");
        File.Delete(fileHash.FileName);
    }
}

完整计划:

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Security.Cryptography;

namespace Test
{
    internal class FileHash
    {
        public string FileName { get; set; }
        public string Hash { get; set; }
    }

    public class Program
    {
        public static void Main()
        { 
            var path = "C:\\Path\To\Files";
            if (File.Exists(path))
            {
                Console.WriteLine($"Deleting duplicate files at {path}");
                DeleteDuplicateFiles(path);
            }
        }

        private static void DeleteDuplicateFiles(string directoryPath)
        {
            var fileHashes = new HashSet<string>();

            foreach (var fileHash in FindFileHashes(directoryPath))
            {
                if (!fileHashes.Contains(fileHash.Hash))
                {
                    Console.WriteLine($"Found - File : {fileHash.FileName} Hash : {fileHash.Hash}");
                    fileHashes.Add(fileHash.Hash);
                    continue;
                }

                Console.WriteLine($"Deleting - File : {fileHash.FileName} Hash : {fileHash.Hash}");
                File.Delete(fileHash.FileName);
            }
        }

        private static IEnumerable<FileHash> FindFileHashes(string directoryPath)
        {
            var allFiles = Directory
                .EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories);

            var hashedFiles = allFiles
                .AsParallel()
                .Select(filename => new FileHash { 
                    FileName = filename, 
                    Hash = CalculateMD5(filename) 
                });

            return hashedFiles;
        }

        private static string CalculateMD5(string filename)
        {
            using var md5 = MD5.Create();
            using var stream = File.OpenRead(filename);
            var hash = md5.ComputeHash(stream);
            return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
        }
    }
}

【讨论】:

    【解决方案4】:

    如果您要查找重复项,最快的方法是使用 jdupes 或 fdupes 之类的东西。它们的性能令人难以置信,并且是用 C 语言编写的。

    【讨论】:

      猜你喜欢
      • 2016-01-03
      • 2021-09-03
      • 1970-01-01
      • 2012-08-17
      • 2013-04-02
      • 1970-01-01
      • 2016-10-29
      • 2013-01-30
      • 1970-01-01
      相关资源
      最近更新 更多