提高我的PowerShell脚本效率

4
下面的代码从list.txt文件中搜索400多个数字,查看它们是否存在于指定文件夹路径中的任何文件中。
该脚本非常缓慢,运行了25分钟仍未完成。我们要搜索的文件夹大小为507MB(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

2
你是在尝试从文件内容中查找$sw吗?问题听起来像是这样,但脚本只查找文件名。 - vonPryz
1
你要完整地阅读所有1,100个文件,寻找其中的400个单词!这种疯狂的语言能否一次搜索10个单词中的任意一个?那么你只需要对1,100个文件进行40次遍历,速度会快10倍。如果你找到一个数字,是否必须继续搜索文档,或者可以在第一次匹配时退出?这种疯狂的语言是否允许并行化?你能否使用Linux代替这个东西? - Mark Setchell
3
请看Select-String,它可以使用正则表达式进行更高效的匹配。另外,您可能更有效地先获取所有文件名,然后在内存中检查它们,而不是多次调用 Get-ChildItem。最后,尝试使用 PsCustomObject 方法,而不是 New-Object / Add-Member,因为管道可能会减慢速度。 - boxdog
2
@MarkSetchell 当然。select-string是PowerShell中grep的类比,它可以搜索多个模式以及正则表达式。 - phuclv
2
如果您有一个来自项目的可工作代码,并且正在寻找以下方面的开放反馈:最佳实践和设计模式使用、安全问题、性能、在意外情况下的正确性 - 那么Code Review SE是提问的正确场所。请问有人可以将这个问题移动吗?我无法移动。 - Nikhil Vartak
+= 会杀死小狗。 - js2010
3个回答

8
以下内容应该会大大加快您的任务:
如果真正的意图是在文件名中查找搜索词:
$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个(任意给定模式)。

  • Select-Object在管道中直接为你构建对象,使用计算属性,而不是在脚本块中使用New-ObjectAdd-Member构建自定义对象。

  • 使用单一管道而不是使用+=迭代生成一个中间数组 - 它在后台每次都会重新创建数组 - 直接将结果对象传输到Export-Csv中。


3
好的!我总是忘记-PipelineVariable! - Matt McNabb
1
谢谢,@MattMcNabb。这是一个方便的功能,但需要它的情况并不经常出现,所以很难记住。 - mklement0
感谢@MattMcNabb的精彩解释。不幸的是,我看到了以下错误。 `Select-String:无法将参数“Pattern”绑定到参数,因为它是一个空字符串。 在C:\ Users \ david.craven \ Downloads \ test.ps1:5字符39处
  • Select-String-SimpleMatch-Pattern $ searchWords | +〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
  • CategoryInfo:InvalidData:(:) [Select-String],ParameterBindin gValidationException
  • FullyQualifiedErrorId:ParameterArgumentValidationErrorEmptyStringNotAll lowed,Microsoft.PowerShell.Commands.SelectStringCommand`
- dcraven
2
@dcraven:这意味着$searchWords为空,而不是包含您的搜索词。 - mklement0
1
@LotPings,你提到的多重匹配很有道理。为了简单起见,我决定将“-List”添加到“Select-String”中,这将限制匹配最多出现1次。 - mklement0
显示剩余3条评论

1
所以,您发布的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
            }
        }
    }
}

一个更简单的方法是在文件数组上使用Where-Object:

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

尝试两种方法并测试性能。

0

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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接