【问题标题】:How to speed up my SUPER SLOW search script如何加快我的 SUPER SLOW 搜索脚本
【发布时间】:2022-06-22 03:12:42
【问题描述】:

更新(22 年 6 月 21 日):请参阅下面的更新脚本,它利用了一些答案。

我正在构建一个脚本来通过大量 CSV 文件搜索$name。这些文件可以大到 67,000 KB。这是我用来搜索文件的脚本:

Powershell 脚本

基本上,我使用Import-Csv。但是,我根据文件名更改了一些内容。例如,某些文件没有标题,或者它们可能使用不同的分隔符。然后我将所有匹配项存储在$results 中,然后返回该变量。这一切都放在一个名为CSVSearch 的函数中,以便于运行。

#create function called CSV Search
function CSVSearch{
    #prompt
    $name = Read-Host -Prompt 'Input name'

    #set path to root folder
    $path = 'Path\to\root\folder\'

    #get the file path for each CSV file in root folder
    $files = Get-ChildItem $path -Filter *.csv | Select-Object -ExpandProperty FullName

    #count files in $files
    $filesCount = $files.Count

    #create empty array, $results
    $results= @()

    #count for write-progress
    $i = 0

      foreach($file in $files){
       
        Write-Progress -Activity "Searching files: $i out of $filesCount searched. $resultsCount match(es) found" -PercentComplete (($i/$files.Count)*100)
        #import method changes depending on CSV file name found in $file (headers, delimiters). 
        if($file -match 'File1*'){$results += Import-Csv $file -Header A, Name, C, D -Delimiter '|' | Select-Object *,@{Name='FileName';Expression={$file}} | Where-Object { $_.'Name' -match $name}}
        if($file -match 'File2*'){$results += Import-Csv $file -Header A, B, Name  -Delimiter '|' | Select-Object *,@{Name='FileName';Expression={$file}} | Where-Object { $_.'Name' -match $name}}
        if($file -match 'File3*'){$results += Import-Csv $file | Select-Object *,@{Name='FileName';Expression={$file}} | Where-Object { $_.'Name' -match $name}}
        if($file -match 'File4*'){$results += Import-Csv $file | Select-Object *,@{Name='FileName';Expression={$file}} | Where-Object { $_.'Name' -match $name}}
        $i++
        $resultsCount = $results.Count
    
    }

    #if the loop ends and $results array is empty, return "No matches."
    if(!$results){Write-Host 'No matches found.' -ForegroundColor Yellow}
    #return results stored in $results variable
    else{$results 
    Write-Host $resultsCount 'matches found.' -ForegroundColor Green
    Write-Progress -Activity "Completed" -Completed}

}

CSVSearch

以下是 CSV 文件的外观。显然,下面的数据量并不等于文件的实际大小。但下面是基本结构:

CSV 文件

File1.csv
1|Moonknight|QWEPP|L
2|Star Wars|QWEPP|T
3|Toy Story|QWEPP|U
File2.csv
JKLH|1|Moonknight
ASDF|2|Star Wars
QWER|3|Toy Story
File3.csv
1,Moonknight,AA,DDD
2,Star Wars,BB,CCC
3,Toy Story,CC,EEE
File4.csv
1,Moonknight,QWE
2,Star Wars,QWE
3,Toy Story,QWE

脚本运行良好。以下是$name = Moonknight 时我会收到的输出示例:

结果示例

A : 1
Name : Moonknight
C: QWE
FileName: Path\to\root\folder\File4.csv

A : 1
Name : Moonknight
B : AA
C : DDD
FileName: Path\to\root\folder\File3.csv

A : JKLH
B : 1
Name : Moonknight
FileName: Path\to\root\folder\File2.csv

A : 1
Name : Moonknight
C : QWEPP
D : L
FileName: Path\to\root\folder\File1.csv

4 matches found.

但是,它很慢,而且我有很多文件要搜索。关于如何加快我的脚本速度的任何想法?

编辑:我必须提到。我尝试将数据导入哈希表,然后搜索哈希表,但这要慢得多

更新的脚本 - 我的解决方案(22 年 6 月 21 日):

本次更新使用了 Santiago 下面的一些脚本。我很难解码他所做的一切,因为我是 PowerShell 的新手。所以我有点杰瑞操纵了我自己的解决方案,使用了他的很多脚本/想法。

产生巨大差异的一件事是输出$results[$i],它会在脚本运行时返回最近的匹配项。可能不是最有效的方法,但它适用于我正在尝试做的事情。谢谢!

function CSVSearch{
[cmdletbinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Name
    )

$files = Get-ChildItem 'Path\to\root\folder\' -Filter *.csv -Recurse | %{$_.FullName}

$results = @()
$i = 0

foreach($file in $files){
if($file -like '*File1*'){$results += Import-Csv $file -Header A, Name, C, D -Delimiter '|' | Where-Object { $_.'Name' -match $Name} | Select-Object *,@{Name='FileName';Expression={$file}}}
if($file -like' *File2*'){$results += Import-Csv $file -Header A, B, Name  -Delimiter '|' | Where-Object { $_.'Name' -match $Name} | Select-Object *,@{Name='FileName';Expression={$file}}} 
if($file -like '*File3*'){$results += Import-Csv $file | Where-Object { $_.'Name' -match $Name} | Select-Object *,@{Name='FileName';Expression={$file}}}
if($file -like '*File4*'){$results += Import-Csv $file | Where-Object { $_.'Name' -match $Name} | Select-Object *,@{Name='FileName';Expression={$file}}}

$results[$i]
$i++
}

if(-not $results) {
        Write-Host 'No matches found.' -ForegroundColor Yellow
        return 
    }
        
    Write-Host "$($results.Count) matches found." -ForegroundColor Green

    }

【问题讨论】:

  • 对于初学者来说,在重构之前过滤(Where-Object应该在Select-Object之前)
  • 你能发布你使用哈希表的尝试吗?如果结果变慢,你可能是用错了:)
  • 仅供参考:将 115 个 csv 文件合并到一个 csv 中,花了将近 10 分钟。在切换到内联 C# 和优化文件读取之间,将其缩短到大约 1 分 20 秒。单独切换到内联 C# 将时间减少到原来的三分之一。

标签: powershell csv foreach


【解决方案1】:

试试这个,应该会快一点。 Select-Object 必须重建你的对象,如果你在过滤之前使用它,你实际上是在重建你的整个 CSV,你想在重建之前先过滤(Where-Object/.Where)。

这里.Where 应该比Where-Object 快,需要注意的是内部方法要求集合已经存在于内存中,没有管道处理和流式处理。

Write-Progress 只会减慢你的脚本,最好删除它。

最后,您可以使用splatting 来避免出现多个if 条件。

function CSVSearch {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Name,

        [Parameter()]
        [string] $Path = 'Path\to\root\folder\'
    )

    $param = @{
        File1 = @{ Header = 'A', 'Name', 'C', 'D'; Delimiter = '|' }
        File2 = @{ Header = 'A', 'B', 'Name' ; Delimiter = '|' }
        File3 = @{}; File4 = @{} # File3 & 4 should have headers ?
    }
    $results = foreach($file in Get-ChildItem . -Filter file*.csv) {
        $thisparam = $param[$file.BaseName]
        $thisparam['LiteralPath'] = $file.FullName
        (Import-Csv @thisparam).where{ $_.Name -match $name } |
            Select-Object *, @{Name='FileName';Expression={$file}}
    }

    if(-not $results) {
        Write-Host 'No matches found.' -ForegroundColor Yellow
        return 
    }
        
    Write-Host "$($results.Count) matches found." -ForegroundColor Green
    $results
}

CSVSearch -Name Moonknight

如果您希望函数在找到时流式传输结果,您可以使用Filter,这是一种非常有效的过滤技术,肯定比Where-Object 更快:

function CSVSearch {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Name,

        [Parameter()]
        [string] $Path = 'Path\to\root\folder\'
    )

    begin {
        $param = @{
            File1 = @{ Header = 'A', 'Name', 'C', 'D'; Delimiter = '|' }
            File2 = @{ Header = 'A', 'B', 'Name' ; Delimiter = '|' }
            File3 = @{}; File4 = @{} # File3 & 4 should have headers ?
        }
        $counter = [ref] 0
        filter myFilter {
            if($_.Name -match $name) {
                $counter.Value++
                $_ | Select-Object *, @{N='FileName';E={$file}}
            }
        }
    }
    process {
        foreach($file in Get-ChildItem $path -Filter *.csv) {
            $thisparam = $param[$file.BaseName]
            $thisparam['LiteralPath'] = $file.FullName
            Import-Csv @thisparam | myFilter
        }
    }
    end {
        if(-not $counter.Value) {
            Write-Host 'No matches found.' -ForegroundColor Yellow
            return 
        }
        Write-Host "$($counter.Value) matches found." -ForegroundColor Green
    }
}

【讨论】:

  • 您可以使用哈希表来保存必须用于 X 和 Y 的逻辑的引用,而不是使用多个条件来检查文件基名是 X 还是 Y(这是哈希表/文件 BaseName 的键)。由于逻辑已经存在,您可以简单地将文件的绝对路径添加到哈希表中以读取它,然后使用 splatting。但是,如果您是 powershell 新手,我并不容易理解。 @Crimp
猜你喜欢
  • 2011-11-26
  • 2016-01-20
  • 2018-10-02
  • 1970-01-01
  • 2016-01-21
  • 1970-01-01
  • 1970-01-01
  • 2013-10-27
  • 2020-01-16
相关资源
最近更新 更多