【问题标题】:Improve the efficiency of my PowerShell script提高我的 PowerShell 脚本的效率
【发布时间】:2019-04-06 05:23:48
【问题描述】:

以下代码从 list.txt 文件中搜索 400 多个数字,以查看它是否存在于指定文件夹路径内的任何文件中。

脚本很慢,运行 25 分钟后仍未完成,因此尚未完成。我们正在搜索的文件夹是 507 MB(532,369,408 字节),其中包含 1,119 个文件480 个文件夹。非常感谢任何有助于提高搜索速度和效率的帮助。

$searchWords = (gc 'C:\temp\list.txt') -split ','
$results = @()
Foreach ($sw in $searchWords)
{
    $files = gci -path 'C:\Users\david.craven\Dropbox\Asset Tagging\_SJC Warehouse_\_Project Completed_\2018\A*' -filter "*$sw*" -recurse

    foreach ($file in $files)
    {
        $object = New-Object System.Object
        $object | Add-Member -Type NoteProperty –Name SearchWord –Value $sw
        $object | Add-Member -Type NoteProperty –Name FoundFile –Value $file.FullName
        $results += $object
    }

}

$results | Export-Csv C:\temp\output.csv -NoTypeInformation

【问题讨论】:

  • 您是否要从文件内容中查找$sw?这个问题听起来像你,但脚本只显示文件名。
  • 您完整阅读了所有 1,100 个文件,寻找 400 个单词中的每一个!这种疯狂的语言可以搜索任何一个,一次说 10 个单词吗?那么你只需要 40 次通过 1,100 个文件,它会快 10 倍。如果您找到一个数字,您是否必须继续搜索文档,或者您可以在第一次匹配时退出?这种疯狂的语言是否允许并行化?你能用 Linux 代替这个东西吗?
  • 看看Select-String,可以使用正则表达式进行更高效的匹配。此外,首先获取所有文件名然后在内存中检查它们而不是多次调用Get-ChildItem 可能更有效。最后,尝试使用PsCustomObject 方法而不是New-Object/Add-Member,因为管道可能会减慢速度。
  • @MarkSetchell 当然。 select-string是powershell中grep的类似物,它可以搜索多个模式以及正则表达式
  • 如果您的项目中有一段工作代码,并且正在寻找以下领域的开放式反馈:最佳实践和设计模式使用、安全问题、*性能*、正确性意料之外的情况 - 那么Code Review SE 是提问的正确地方。有人可以移动这个问题吗?我做不到。

标签: performance powershell coding-efficiency


【解决方案1】:

以下内容应该可以大大加快您的任务:

如果真正的意图是在文件名中查找搜索词:

$searchWords = (Get-Content 'C:\temp\list.txt') -split ','
$path = 'C:\Users\david.craven\Dropbox\Facebook Asset Tagging\_SJC Warehouse_\_Project Completed_\2018\A*'

Get-ChildItem -File -Path $path -Recurse -PipelineVariable file |
  Select-Object -ExpandProperty Name |
    Select-String -SimpleMatch -Pattern $searchWords |
      Select-Object @{n='SearchWord'; e='Pattern'},
                    @{n='FoundFile'; e={$file.FullName}} |
        Export-Csv C:\temp\output.csv -NoTypeInformation

如果打算在文件的内容中查找搜索词:

$searchWords = (Get-Content 'C:\temp\list.txt') -split ','
$path = 'C:\Users\david.craven\Dropbox\Facebook Asset Tagging\_SJC Warehouse_\_Project Completed_\2018\A*'

Get-ChildItem -File -Path $path -Recurse |
  Select-String -List -SimpleMatch -Pattern $searchWords |
    Select-Object @{n='SearchWord'; e='Pattern'},
                  @{n='FoundFile'; e='Path'} |
      Export-Csv C:\temp\output.csv -NoTypeInformation

性能提升的关键:

  • 使用单个命令执行搜索,将所有搜索词传递给Select-String。注意:-List 将匹配限制为 1 个匹配(通过任何给定模式)。

  • 不要使用New-ObjectAdd-Member 在脚本块中构造自定义对象,而是让Select-Object 使用calculated properties 直接在管道中为您构造对象。

  • 不是使用+= 迭代地构建一个中间数组 - 它在幕后重新创建 数组每次 - 使用单个管道将结果对象直接通过管道传输到 Export-Csv

【讨论】:

  • 不错!我总是忘记 -PipelineVariable!
  • 谢谢,@MattMcNabb。这是一个方便的功能,但它的需求并不经常出现,因此很难记住。
  • 感谢@MattMcNabb 的精彩解释。不幸的是,我看到了以下错误。 Select-String : Cannot bind argument to parameter 'Pattern' because it is an empty string. At C:\Users\david.craven\Downloads\test.ps1:5 char:39 + Select-String -SimpleMatch -Pattern $searchWords | + ~~~~~~~~~~~~ + CategoryInfo : InvalidData: (:) [Select-String], ParameterBindin gValidationException + FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAll owed,Microsoft.PowerShell.Commands.SelectStringCommand
  • @dcraven:这表明$searchWords 是空的,而不是包含您的搜索词。
  • 关于多场比赛的好点,@LotPings。为简单起见,我决定将 -List 添加到 Select-String,这将匹配限制为最多 1 次。
【解决方案2】:

因此,您发布的 PowerShell 代码中肯定有一些基本的东西可以改进,但它可能仍然不是超级快。根据您给我们的示例,我假设您正在寻找将文件名与单词列表进行匹配。您正在遍历单词列表(400 次迭代),并且在每个循环中您正在遍历所有 1,119 个文件。总共有 447,600 次迭代!

假设您无法减少循环中的迭代次数,让我们从加快每次迭代开始。 Add-Member cmdlet 会非常慢,因此通过将哈希表转换为 [PSCustomObject] 类型加速器来切换该方法:

[PSCustomObject]@{
    SearchWord = $Word
    File       = $File.FullName
}

此外,没有理由预先创建一个数组对象,然后将每个文件添加到其中。您可以简单地在变量中捕获 foreach 循环的输出:

$Results = Foreach ($Word in $Words)
{
...

所以更快的循环可能如下所示:

$Words = Get-Content -Path $WordList
$Files = Get-ChildItem -Path $Path -Recurse -File

$Results = Foreach ($Word in $Words)
{    
    foreach ($File in $Files)
    {
        if ($File.BaseName -match $Word)
        {
            [PSCustomObject]@{
                SearchWord = $Word
                File       = $File.FullName
            }
        }
    }
}

更简单的方法可能是在 files 数组上使用 Where-Object:

$Results = Foreach ($Word in $Words)
{
    $Files | Where-Object BaseName -match $Word
}

尝试两者并测试性能。

【讨论】:

    【解决方案3】:

    因此,如果加速循环不能满足您的需求,请尝试完全移除循环。您可以使用正则表达式并将所有单词连接在一起:

    $Words = Get-Content -Path $WordList
    $Files = Get-ChildItem -Path $Path -Recurse -File
    $WordRegex = $Words -join '|'
    $Files | Where basename -match $WordRegex
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多