过滤大型 CSV 文件时出现内存异常

Memory exception while filtering large CSV files

在 运行 此代码时出现内存异常。有没有办法一次过滤一个文件并在处理每个文件后写入输出和追加。似乎下面的代码将所有内容加载到内存中。

$inputFolder = "C:\Change19\October"
$outputFile = "C:\Change19\output.csv"
Get-ChildItem $inputFolder -File -Filter '*.csv' |
    ForEach-Object { Import-Csv $_.FullName } |
    Where-Object { $_.machine_type -eq 'workstations' } |
    Export-Csv $outputFile -NoType

get-content *.csv | add-content combined.csv

当你 运行 这样做时,确保 combined.csv 不存在,否则它将充满 Ouroboros。

您必须一次读取和写入 .csv 文件一行,使用 StreamReaderStreamWriter:

$filepath = "C:\Change19\October"
$outputfile = "C:\Change19\output.csv"
$encoding = [System.Text.Encoding]::UTF8

$files = Get-ChildItem -Path $filePath -Filter *.csv | 
         Where-Object { $_.machine_type -eq 'workstations' }

$w = New-Object System.IO.StreamWriter($outputfile, $true, $encoding)

$skiprow = $false
foreach ($file in $files)
{
    $r = New-Object System.IO.StreamReader($file.fullname, $encoding)
    while (($line = $r.ReadLine()) -ne $null) 
    {
        if (!$skiprow)
        {
            $w.WriteLine($line)
        }
        $skiprow = $false
    }
    $r.Close()
    $r.Dispose()
    $skiprow = $true
}

$w.close()
$w.Dispose()

注意:not 使用 Get-ChildItem ... | Import-Csv ... 的原因 - 即 not 直接管道 Get-ChildItemImport-Csv 而不是必须从脚本块调用 Import-Csv(辅助 ForEach-Object 调用的 { ... },是 bug Windows PowerShell 已在 PowerShell Core 中修复 - 请参阅底部部分以获得更简洁的解决方法。

但是,即使是 ForEach-Object 脚本块的输出也应该 到剩余的管道命令,所以你 不应该 运行 内存不足 - 毕竟,PowerShell 管道的一个显着特征是 逐个对象 处理,这使内存使用保持 恒定,无论(流式)输入集合的大小如何。

你已经确认避开辅助。 ForEach-Object调用没有解决问题,所以我们仍然不知道是什么原因导致你的内存不足异常。

更新

以下解决方法,其中使用switch语句 将文件处理为文本文件,可能帮助:

$header = ''
Get-ChildItem $inputFolder -Filter *.csv | ForEach-Object {
  $i = 0
  switch -Wildcard -File $_.FullName {
    '*workstations*' {
      # NOTE: If no other columns contain the word `workstations`, you can 
      # simplify and speed up the command by omitting the `ConvertFrom-Csv` call 
      # (you can make the wildcard matching more robust with something 
      # like '*,workstations,*')
      if ((ConvertFrom-Csv "$header`n$_").machine_type -ne 'workstations') { continue }
      $_ # row whose 'machine_type' column value equals 'workstations'
    }
    default {
      if ($i++ -eq 0) {
        if ($header) { continue } # header already written
        else { $header = $_; $_ } # header row of 1st file
      }
    }
  }
} | Set-Content $outputFile

这里有一个 解决方法,解决无法将 Get-ChildItem 输出 直接 Import-Csv 的问题,通过将其作为 参数 传递而不是:

Import-Csv -LiteralPath (Get-ChildItem $inputFolder -File -Filter *.csv) |
    Where-Object { $_.machine_type -eq 'workstations' } |
    Export-Csv $outputFile -NoType

请注意,在 PowerShell Core 中,您可以更自然地编写:

Get-ChildItem $inputFolder -File -Filter *.csv | Import-Csv |
  Where-Object { $_.machine_type -eq 'workstations' } |
    Export-Csv $outputFile -NoType

也许您可以一个一个地导出和过滤您的文件,然后将结果附加到您的输出文件中,如下所示:

$inputFolder = "C:\Change19\October"
$outputFile = "C:\Change19\output.csv"

Remove-Item $outputFile -Force -ErrorAction SilentlyContinue

Get-ChildItem $inputFolder -Filter "*.csv" -file | %{import-csv $_.FullName | where machine_type -eq 'workstations' | export-csv $outputFile -Append -notype }

解决方案 2:

$inputFolder = "C:\Change19\October"
$outputFile = "C:\Change19\output.csv"
$encoding = [System.Text.Encoding]::UTF8  # modify encoding if necessary
$Delimiter=','

#find header for your files => i take first row of first file with data
$Header = Get-ChildItem -Path $inputFolder -Filter *.csv | Where length -gt 0 | select -First 1 | Get-Content -TotalCount 1

#if not header founded then not file with sise >0 => we quit
if(! $Header) {return}

#create array for header
$HeaderArray=$Header -split $Delimiter -replace '"', ''

#open output file
$w = New-Object System.IO.StreamWriter($outputfile, $true, $encoding)

#write header founded
$w.WriteLine($Header)


#loop on file csv
Get-ChildItem $inputFolder -File -Filter "*.csv" | %{

    #open file for read
    $r = New-Object System.IO.StreamReader($_.fullname, $encoding)
    $skiprow = $true

    while ($line = $r.ReadLine()) 
    {
        #exclude header
        if ($skiprow) 
        {
            $skiprow = $false
            continue
        }

        #Get objet for current row with header founded
        $Object=$line | ConvertFrom-Csv -Header $HeaderArray -Delimiter $Delimiter

        #write in output file for your condition asked
        if ($Object.machine_type -eq 'workstations') { $w.WriteLine($line) }

    }

    $r.Close()
    $r.Dispose()

}

$w.close()
$w.Dispose()