Powershell: замена строк в CSV-файлах приводит к «Исключению типа« System.OutOfMemoryException »было сгенерировано». - PullRequest
1 голос
/ 30 октября 2019

Я пишу простой скрипт (как я думал) для замены некоторых строк в файлах 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 МБ. Каждый исходный файл составляет около 1,5 МБ

Я не могу понять, как я сталкиваюсь с проблемами памяти с этими размерами файлов. И оф. Я понятия не имею, как избежать этой проблемы

Я нашел несколько блогов, рассказывающих о проблемах с памятью в PS. Все они в конечном итоге изменяют значения по умолчанию квоты PowerShell MaxMemoryPerShellMB. Это как-то совсем не работает для меня, так как я сталкиваюсь с ошибкой с

get-item WSMAN:\localhost\shell\MaxMemoryPerShellMB

Говоря "get-item: не удается найти путь" WSMan: \ localhost \ Shell \ MaxMemorPerShellMB ', потому что он не существует. "

Я работаю в VS Code.

Ответы [ 2 ]

1 голос
/ 31 октября 2019

Как указывает @BACON, основная проблема здесь вызвана циклом (вероятно) нескольких тысяч замен.

Каждый раз, когда выполняется строка замены:

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

Сначала PowerShell имееткусок памяти для $txtsourceFile. Он выделяет новый кусок памяти для хранения копии данных после замены текста.

Обычно это нормально, так как у вас будет один действительный кусок памяти с текстом замены и «недействительным»скопировать с оригинальным текстом. Поскольку у большинства людей (относительно) много памяти, и мы обычно можем справиться с этой «утечкой» в .NET, периодически запуская сборщик мусора в фоновом режиме, чтобы «очистить» эти недопустимые данные.

Проблема, с которой мы столкнулисьдело в том, что когда мы зацикливаемся несколько тысяч раз быстро, мы также быстро генерируем несколько тысяч копий данных. В конечном итоге у вас заканчивается свободная память, прежде чем сборщик мусора сможет запустить и очистить тысячи недействительных копий данных (например, 3,2 ГБ). См .: Нет сборки мусора во время выполнения конвейера PowerShell

Существует несколько способов обойти это:

Решение 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++
}

Это делает 2 вещи:

  1. Запуск сборщика мусора каждые 200 циклов ($count модуль 200).
  2. Остановка текущего выполнения и принудительное выполнениеколлекция.

Примечание:

Обычно вы используете:

[GC]::Collect()

Но в соответствии с Обращение к сборке мусора PowerShellошибка в 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()
    }
}

Это должно работать на порядок быстрее, потому что вы будете работать со строкой за раз и не будете создавать тысячи копий всего файлас каждой заменой.

0 голосов
/ 01 ноября 2019

Я нашел ответ и комментарии, приведенные выше, очень полезными и реализовал решение, близкое к ответу здесь: я разделил $ findReplaceList на несколько пакетов (длиной около 37000 записей, я начал разбивать на 1000) и работаю над ваннойпартиями с промежуточным GC. Теперь я могу наблюдать, как увеличивается использование памяти во время пакета, и снова спрыгивать, когда он сделан.

С этим я обнаружил интересное поведение: проблема с памятью все еще возникала в нескольких партиях ... Я проанализировал findReplaceList далее со следующим результатом:

Есть случаи, когда в файле нет $ $ OldIssueKey ..

Может ли быть так, что PS тогда видит это как пустую строку и пытаетсязаменить все это?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...