Powershell:替换csv文件中的字符串会导致“错误”;类型'的例外情况;System.OutOfMemoryException';被扔出去了。”;

Powershell:替换csv文件中的字符串会导致“错误”;类型'的例外情况;System.OutOfMemoryException';被扔出去了。”;,powershell,exception,memory,Powershell,Exception,Memory,我正在编写一个简单的脚本(如我所想)来替换CSV文件中的一些字符串。这些字符串被称为对象的“键”。我基本上用“新密钥”替换文件中的“旧密钥” 我遇到的问题是:当脚本处理第一个文件(外部循环的第一次迭代)时,我得到: 此错误引用了替换代码行: $txtsourceFile = $txtsourceFile -replace $findReplaceitem.$OldIssueKey , $findReplaceitem.$IssueKey csv文件“很大”,但实际上没有那么大。 映射列表为1

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

我遇到的问题是:当脚本处理第一个文件(外部循环的第一次迭代)时,我得到:

此错误引用了替换代码行:

$txtsourceFile = $txtsourceFile -replace  $findReplaceitem.$OldIssueKey , $findReplaceitem.$IssueKey
csv文件“很大”,但实际上没有那么大。
映射列表为1.7 MB 每个源文件大约有1.5 MB

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

我发现一些博客讨论PS中的内存问题。他们最终都更改了PowerShell MaxMemoryPerShellMB配额默认值。这对我来说根本不起作用,因为我遇到了一个错误

get-item WSMAN:\localhost\shell\MaxMemoryPerShellMB
说“获取项:找不到路径'WSMan:\localhost\Shell\MaxMemorPerShellMB',因为它不存在。”


我在VS代码中工作。

正如@BACON所暗示的,这里的核心问题是通过(可能)数千个替换循环引起的

每次执行替换行时:

$txtsourceFile = $txtsourceFile -replace  $findReplaceitem.$OldIssueKey , $findReplaceitem.$IssueKey
PowerShell首先有一块内存用于
$txtsourceFile
。它分配一个新的内存块来存储文本替换后的数据副本

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

我们遇到的麻烦是,当我们快速循环数千次时,我们也会快速生成数千份数据副本。在垃圾收集器有机会运行和清理数千个无效数据拷贝(即3.2GB)之前,您的可用可用内存最终耗尽。见:

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

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

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

这有两件事:

  • 每200个循环运行一次垃圾收集(
    $count
    200)
  • 停止当前执行并强制收集
  • 注意:

    通常您使用:

    [GC]::Collect()
    
    但是根据这一点,当试图将集合强制放入循环中时,它并不总是起作用。使用:

    [System.GC]::GetTotalMemory('forceFullCollection')
    
    完全停止执行,直到垃圾收集完成后再继续

    解决方案2:更快、更节省内存的方法,一次一行

    如果可以一次一行执行所有替换,那么可以使用来流式处理文件,一次处理一行并写入它

    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()
        }
    }
    

    这应该运行快一个数量级,因为您将一次工作一行,并且不会在每次替换时创建整个文件的数千个副本。

    我发现上面的答案和注释非常有用,并在此处实现了一个接近答案的解决方案: 我将$findReplaceList分为多个批次(大约37000个条目长,我开始将其分为1000个),并使用GC进行批处理。 现在,我可以看到内存使用率在批处理过程中上升,而在批处理完成后再次下降

    于是我发现了一个有趣的行为:内存问题仍然在几批中出现。。。因此,我进一步分析了findReplaceList,得出以下结果:

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


    PS是否会将其视为一个空字符串并尝试替换所有这些字符串?

    简短更新:如果我在执行过程中检查系统内存消耗:Process Windows Powershell在异常情况下停止之前会占用高达3.2 GB的内存。
    $mappingList
    文件中可能有多少个问题密钥?对于一个给定的
    $sourceFile
    ,有多少键可以被重新映射?尽管这两个文件都小于2MB,但每次引用的错误行导致更改时,它都会生成一个稍有不同但仍然是全新的对象,表示完整的源文件。如果定义了10000个映射,并且在源文件中找到了1000个映射,那么要收集的垃圾量为1000×1.7MB=1.7GB。如果映射较短但数量较多,则数学会变得更糟。@BACON的建议与我的想法相同,但我对PowerShell中的gc了解不够。你确定那只是副本吗?错误消息中也存在同样的拼写错误?此外,当您说要替换“键”时,是重新映射整个列(单元格)值,还是任意搜索文本可能是值的子字符串(如亵渎过滤器)?这可以使用
    [Hashtable]
    /
    [Dictionary]
    逐行处理以执行映射,这将大大减少运行时和内存使用,但这需要替换整个值。我发现上面的答案和注释非常有用,并在这里实现了一个接近答案的解决方案:我将$findReplaceList拆分为多个批次(大约37000个条目,我开始拆分为1000个)然后用GC进行批处理。这产生了一个非常有趣的结果!对空字符串进行匹配:
    “abc”-replace”,“z”
    返回
    zazbzcz
    看起来它匹配每个字符(包括下线字符),并用替换文本加上现有字符替换它。所以如果你有一个拥抱
    [GC]::Collect()
    
    [System.GC]::GetTotalMemory('forceFullCollection')
    
    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()
        }
    }