【问题标题】:Memory exception while filtering large CSV files过滤大型 CSV 文件时出现内存异常
【发布时间】:2019-11-01 14:22:08
【问题描述】:

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

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

【问题讨论】:

  • CSV 只是文本。 ImportCSV cmdlet 对于在 PowerShell 中操作 CSV 数据很有用,但如果您只想将一个 CSV 附加到另一个 CSV(假设它们的宽度相同),您可以像阅读文本一样阅读它们。 get-content *.csv | set-content combined.csv 似乎应该可以。可能不得不弄乱行尾?
  • @Joe:我只需要使用 machine_type -eq "workstation" 过滤 combine.csv
  • 您的 CSV 总是以相同的顺序排列相同的列?

标签: powershell csv out-of-memory


【解决方案1】:

您是否可以一一导出和过滤文件并将结果附加到输出文件中,如下所示:

$inputFolder = "C:\Change\2019\October"
$outputFile = "C:\Change\2019\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 }

【讨论】:

  • 感谢您。即使它仍然会引发内存错误,它也会设法创建 csv 文件并附加数据。我还在 export-csv 行中添加了 -ErrorAction SilentlyContinue,以便它可以继续处理数据。
  • @Enigma:内存不足错误是语句终止错误,因此输出 CSV 文件中可能会丢失数据。
  • mklement0 是对的,如果你仍然抛出错误你不能使用这个解决方案
  • @Enigma:在Export-Csv 命令之后,您可以放置​​一个强制垃圾收集的命令,以查看是否可以消除错误:[gc]::Collect(); [gc]::WaitForPendingFinalizers();另外,我在 GitHub 上发现了与该问题相关的问题 - 请参阅我的更新答案(说到:我仍然不明白为什么基于 switch 的解决方法对您不起作用)。
【解决方案2】:

注意:not 使用 Get-ChildItem ... | Import-Csv ... 的原因 - 即 not 直接将 Get-ChildItem 管道传输到 Import-Csv 而必须调用 Import-Csv脚本块(辅助ForEach-Object 调用的{ ... }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 的错误,方法是将其作为参数传递 em> 代替:

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

【讨论】:

  • 非常感谢您对此提供的帮助。但除了opn 之外,我仍然记忆犹新。PS C:\change\2019\October> $inputFolder = "C:\Change\2019\October" PS C:\change\2019\October> $outputFile = "C:\Change\2019\output.csv" PS C:\change\2019\October> Import-Csv -LiteralPath (Get-ChildItem $inputFolder -File -Filter *.csv) | >> Where-Object { $_.machine_type -eq 'workstation' } | >> Export-Csv $outputFile -NoType Exception of type 'System.OutOfMemoryException' was thrown. At line:1 char:1, OutOfMemoryException + FullyQualifiedErrorId : System.OutOfMemoryException
  • PS C:\WINDOWS\system32> $PSVersionTable 名称值 ---- ----- PSVersion 5.1.17763.771 PSEdition Desktop PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...} BuildVersion 10.0 .17763.771 CLRVersion 4.0.30319.42000 WSManStackVersion 3.0 PSRemotingProtocolVersion 2.3 SerializationVersion 1.1.0.1
  • @Enigma:请查看我的更新,其中显示了另一种解决方法。如果这没有帮助,我们可以尝试强制定期垃圾回收来强制释放之前分配的对象。
  • @Enigma:如果没有其他列包含单词 workstations,您可以通过省略 ConvertFrom-Csv 调用来简化和加速命令(您可以使用类似 @ 的内容使通配符匹配更加健壮987654340@)
  • 解决方法有效,但输出 csv 似乎不会在每一行之后截断,并且在表格标题数据列水平之后填充数据。
【解决方案3】:

解决方案 2:

$inputFolder = "C:\Change\2019\October"
$outputFile = "C:\Change\2019\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()

【讨论】:

  • 你试过我的第二个提议了吗?
【解决方案4】:

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

$filepath = "C:\Change\2019\October"
$outputfile = "C:\Change\2019\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()

【讨论】:

  • 我们是否在行中缺少 Import-csv...'$files = Get-ChildItem -Path $filePath -Filter *.csv | Where-Object { $_.machine_type -eq '工作站' }'
【解决方案5】:

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

确保在运行此程序时 combine.csv 不存在,否则它将充满 Ouroboros。

【讨论】:

  • 这将在输出文件中复制标题行,并且它也不能满足过滤器的要求。 (除此之外,应该使用Set-Content)。
  • 不会Set-Content 覆盖combined.csv 的已设置内容,使其成为上次提取的CSV 文件的副本?
  • 不,Set-Content 通过管道接收到的任何内容都会进入目标文件。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-04
  • 2021-11-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多