过滤大型 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 文件一行,使用 StreamReader
和 StreamWriter
:
$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-ChildItem
到Import-Csv
而不是必须从脚本块调用 Import-Csv
(辅助 ForEach-Object
调用的 { ... }
,是 bug Windows PowerShell 已在 PowerShell Core 中修复 - 请参阅底部部分以获得更简洁的解决方法。
但是,即使是 ForEach-Object
脚本块的输出也应该 流 到剩余的管道命令,所以你 不应该 运行 内存不足 - 毕竟,PowerShell 管道的一个显着特征是 逐个对象 处理,这使内存使用保持 恒定,无论(流式)输入集合的大小如何。
你已经确认避开辅助。 ForEach-Object
调用没有解决问题,所以我们仍然不知道是什么原因导致你的内存不足异常。
更新:
This GitHub issue 包含有关过度使用内存的原因的线索,尤其是对于包含少量数据的许多属性。
This GitHub feature request 建议使用 强类型 输出对象来帮助解决问题。
以下解决方法,其中使用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()
$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 文件一行,使用 StreamReader
和 StreamWriter
:
$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-ChildItem
到Import-Csv
而不是必须从脚本块调用 Import-Csv
(辅助 ForEach-Object
调用的 { ... }
,是 bug Windows PowerShell 已在 PowerShell Core 中修复 - 请参阅底部部分以获得更简洁的解决方法。
但是,即使是 ForEach-Object
脚本块的输出也应该 流 到剩余的管道命令,所以你 不应该 运行 内存不足 - 毕竟,PowerShell 管道的一个显着特征是 逐个对象 处理,这使内存使用保持 恒定,无论(流式)输入集合的大小如何。
你已经确认避开辅助。 ForEach-Object
调用没有解决问题,所以我们仍然不知道是什么原因导致你的内存不足异常。
更新:
This GitHub issue 包含有关过度使用内存的原因的线索,尤其是对于包含少量数据的许多属性。
This GitHub feature request 建议使用 强类型 输出对象来帮助解决问题。
以下解决方法,其中使用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()