[System.IO.File]::ReadAllText 内存不足异常,CSV 较大

Out of memory exception on [System.IO.File]::ReadAllText with large CSV

我有一个简单的 PowerShell 脚本,可将 "false" 或 "true" 替换为“0”或“1”:

$InputFolder = $args[0];
if($InputFolder.Length -lt 3)
{
    Write-Host "Enter a path name as your first argument" -foregroundcolor Red
    return
}
if(-not (Test-Path $InputFolder)) {
    Write-Host "File path does not appear to be valid" -foregroundcolor Red
    return
}
Get-ChildItem $InputFolder
$content = [System.IO.File]::ReadAllText($InputFolder).Replace("`"false`"", "`"0`"").Replace("`"true`"", "`"1`"").Replace("`"FALSE`"", "`"0`"").Replace("`"TRUE`"", "`"1`"")
[System.IO.File]::WriteAllText($InputFolder, $content)
[GC]::Collect()

这几乎适用于我必须修改的所有文件,但一个 808MB CSV 文件除外。 我不知道这个 CSV 中有多少行,因为我没有任何东西可以正确打开它。

有趣的是,当直接通过 PowerShell 或命令提示符手动调用时,PowerShell 脚本将成功完成。 当它作为所需的 SSIS 包的一部分启动时,就会发生错误。

文件的示例数据:

"RowIdentifier","DateProfileCreated","IdProfileCreatedBy","IDStaffMemberProfileRole","StaffRole","DateEmploymentStart","DateEmploymentEnd","PPAID","GPLocalCode","IDStaffMember","IDOrganisation","GmpID","RemovedData"     
"134","09/07/1999 00:00","-1","98","GP Partner","09/07/1999 00:00","14/08/2009 15:29","341159","BRA 871","141","B83067","G3411591","0"

抛出错误信息:

我不依赖于 PowerShell - 我对其他选项持开放态度。我以前有一个拼凑的 C# 脚本,但它死于比这个小的文件 - 我不是 C# 开发人员,所以根本无法调试它。

非常感谢收到任何建议或帮助。

您可以使用 get-content -readcount 每行读取文件,Out-file 一个临时文件,然后删除旧文件和 rename-item 临时文件的旧文件名。

需要修理的小东西。这将在文件末尾添加一个新的空行。这将更改编码。您可以尝试获取当前文件编码并在 Out-file -encoding

上设置编码
function Replace-LargeFilesInFolder(){
    Param(
        [string]$DirectoryPath,
        [string]$OldString,
        [string]$NewString,
        [string]$TempExtention = "temp",
        [int]$LinesPerRead = 500
    )
    Get-ChildItem $DirectoryPath -File | %{
        $File = $_
        Get-Content $_.FullName -ReadCount $LinesPerRead |
            %{
                $_ -replace $OldString, $NewString | 
                out-file "$($File.FullName).$($TempExtention)" -Append
            }
        Remove-Item $File.FullName
        Rename-Item "$($File.FullName).$($TempExtention)" -NewName $($File.FullName)
    }
}

Replace-LargeFilesInFolder -DirectoryPath C:\TEST -LinesPerRead 1 -OldString "a" -NewString "5"
  • 一般来说,避免一次读取大文件,因为您可能会 运行 内存不足,正如您所经历的那样。

  • 相反,处理基于文本的文件逐行 - 读取和写入。

    • 虽然 PowerShell 通常擅长逐行(逐个对象)处理,但它 处理多行文件。

    • 直接使用 .NET Framework - 虽然更复杂 - 可提供更好的性能。

  • 如果你逐行处理输入文件,你不能直接写回它,而必须写到一个临时输出文件,你可以成功时将输入文件替换为。

这是一个出于性能原因直接使用 .NET 类型的解决方案:

# Be sure to use a *full* path, because .NET typically doesn't have the same working dir. as PS.
$inFile = Convert-Path $Args[0]      
$tmpOutFile = [io.path]::GetTempFileName()

$tmpOutFileWriter = [IO.File]::CreateText($tmpOutFile)
foreach ($line in [IO.File]::ReadLines($inFile)) {
  $tmpOutFileWriter.WriteLine(
    $line.Replace('"false"', '"0"').Replace('"true"', '"1"').Replace('"FALSE"', '"0"').Replace('"TRUE"', '"1"')
  )
}
$tmpOutFileWriter.Dispose()

# Replace the input file with the temporary file.
# !! BE SURE TO MAKE A BACKUP COPY FIRST.
# -WhatIf *previews* the move operation; remove it to perform the actual move.
Move-Item -Force -LiteralPath $tmpOutFile $inFile -WhatIf

注:

  • 假定UTF-8编码,重写的文件将没有有BOM。您可以通过为 .NET 方法指定所需的编码来更改此设置。

  • 顺便说一句:您对每个输入行的 .Replace() 调用链可以简化如下,使用 PowerShell 的 -replace 运算符,即 case-不敏感,所以只需要2次替换:
    $line -replace '"false"', '"0"' -replace '"true"', '"1"'
    然而,虽然它比 write 更短,但它实际上比 .Replace() 调用链 ,大概是因为 -replace是基于 regex 的,这会导致额外的处理。