Powershell:替换csv文件中的字符串会导致“引发了'System.OutOfMemoryException'类型的异常。”

时间:2019-10-30 17:58:17

标签: powershell exception memory

我正在编写一个简单的脚本(按照我的想法)以替换CSV文件中的某些字符串。这些字符串被称为对象的“键”。我基本上将文件中的“旧密钥”替换为“新密钥”。

function simpleStringReplacement {
    param (
        $sourceFiles,  # list of csv files in which we do need to replace contents
        $mappingList,  # a file that contains 2 columns: The old key and the new key
        $exportFolder, # folder where i expect the results
        $FieldsToSelectFromTargetFilesIntoMappingFile # As the names of the fields that contain the values for replacements change, i have that in this array
    )
    $totalitems = $sourceFiles.count
    $currentrow = 0
    Write-Output "Importing mapper file $mappingList" | logText
    $findReplaceList = Import-Csv -Path $mappingList -Delimiter   ';'
    foreach ($sourceFile in $sourceFiles) {
        $currentrow += 1
        Write-Output "Working on  $currentrow : $sourceFile" | logText
        [string] $txtsourceFile = Get-Content $sourceFile.FullName | Out-String
        $IssueKey = $FieldsToSelectFromTargetFilesIntoMappingFile[0]
        $OldIssueKey = $FieldsToSelectFromTargetFilesIntoMappingFile[1]

 ForEach ($findReplaceItem in $findReplaceList) {
          $txtsourceFile = $txtsourceFile -replace  $findReplaceitem.$OldIssueKey , $findReplaceitem.$IssueKey
        }
        $outputFileName = $sourceFile.Name.Substring(0, $sourceFile.Name.IndexOf('.csv') ) + "_newIDs.csv"
        $outputFullFileName =Join-Path -Path $exportFolder -ChildPath $outputFileName
        Write-Output "Writing result to  $currentrow : $outputFullFileName" | logText
        $txtsourceFile | Set-Content -path $outputFullFileName
    }
}

我遇到的问题:当脚本在第一个文件上工作时(外循环的第一次迭代),我已经得到:

Insufficient memory to continue the execution of the program.

此错误引用我的代码行进行替换:

$txtsourceFile = $txtsourceFile -replace  $findReplaceitem.$OldIssueKey , $findReplaceitem.$IssueKey

csv文件“很大”,但实际上不那么大..
mappingList为1.7 MB 每个源文件约为1.5 MB

我真的不明白我如何遇到这些文件大小的内存问题。和ofc。我不知道如何避免这个问题

我发现一些博客谈论PS中的内存问题。它们最终都会更改PowerShell MaxMemoryPerShellMB配额默认值。当我遇到一个错误时,

get-item WSMAN:\localhost\shell\MaxMemoryPerShellMB

说“获取项目:找不到路径'WSMan:\ localhost \ Shell \ MaxMemorPerShellMB',因为它不存在。”

我正在使用VS Code。

2 个答案:

答案 0 :(得分:1)

@BACON暗示,这里的核心问题是由(可能)数千次替换循环引起的。

每次执行替换行:

$txtsourceFile = $txtsourceFile -replace  $findReplaceitem.$OldIssueKey , $findReplaceitem.$IssueKey

PowerShell首先为$txtsourceFile提供了一块内存。替换文本后,它将分配一个新的内存块来存储数据的副本。

这通常是“确定”的,因为您将拥有一个有效的内存块(带有替换文本)和一个“无效”的副本(带有原始文本)。由于大多数人拥有(相对)大量的内存,因此我们通常可以通过在后台定期运行垃圾收集器以“清理”这些无效数据来处理.NET中的“泄漏”。

我们遇到的麻烦是,当我们快速循环数千次时,我们也会快速生成数千个数据副本。在Garbage Collector有机会运行和清理数千个无效数据副本(即3.2GB)之前,您最终会耗尽可用的空闲内存。参见:No garbage collection while PowerShell pipeline is executing

有两种方法可以解决此问题:

解决方案1:大而慢的方法和低效的方式

如果您需要处理整个文件(例如,通过换行符),则可以使用相同的代码并在执行期间定期手动运行垃圾收集器以管理“更好”的内存:

$count = 0

ForEach ($findReplaceItem in $findReplaceList) {
    $txtsourceFile = $txtsourceFile -replace  $findReplaceitem.$OldIssueKey, $findReplaceitem.$IssueKey

    if(($count % 200) -eq 0)
    {
        [System.GC]::GetTotalMemory('forceFullCollection') | out-null
    }
    $count++
}

这有两件事:

  1. 每200个循环($count模数200)运行一次垃圾回收。
  2. 停止当前执行并强制执行收集。

注意:

通常使用:

[GC]::Collect()

但是根据Addressing the PowerShell Garbage Collection bug at J House Consulting,当试图将集合强制放入循环时,这并不总是有效。使用:

[System.GC]::GetTotalMemory('forceFullCollection')

完全停止执行,直到垃圾回收完成为止。

解决方案2:更快,内存效率更高的方法,一次只行一行

如果您一次可以执行所有替换操作,则可以使用[System.IO.StreamReader]来流传输文件并一次处理一行,并使用[System.IO.StreamWriter]来写入文件。

>
try
{
    $SR = New-Object -TypeName System.IO.StreamReader -ArgumentList $sourceFile.FullName
    $SW = [System.IO.StreamWriter] $outputFullFileName

    while ($line = $SR.ReadLine()) {
        #Loop through Replacements
        ForEach ($findReplaceItem in $findReplaceList) {
            $Output = $line -replace  $findReplaceitem.$OldIssueKey, $findReplaceitem.$IssueKey
        }
        $SW.WriteLine($output)
    }

    $SR.Close() | Out-Null
    $SW.Close() | Out-Null
}
finally
{
    #Cleanup
    if ($SR -ne $null)
    {
        $SR.dispose()
    }
    if ($SW -ne $null)
    {
        $SW.dispose()
    }
}

这应该更快地运行一个数量级,因为您将一次处理一行,并且每次替换都不会创建整个文件的数千个副本。

答案 1 :(得分:0)

我发现上面的答案和评论非常有帮助,并实现了一个接近此处答案的解决方案: 我将$ findReplaceList分成多个批次(大约37000个条目,我开始分成1000个条目),并在其间进行分批处理,并使用GC。 现在,我可以看到批处理期间的内存使用率攀升,完成后又再次跳下。

由此,我发现了一个有趣的行为:内存问题仍在一些批次中出现...因此,我进一步分析了findReplaceList并得出以下结果:

在某些情况下,文件中没有$ OldIssueKey。

PS会否将其视为空字符串并尝试替换所有字符串?