3

Я хочу Get-Content большого (1 ГБ - 10 ГБ) .txt файла (который имеет только 1 строку!) и разделить его на несколько файлов с несколькими строками, но всякий раз, когда я пытаюсь это сделать, я получаю System.OutOfMemoryException .

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

Хотя PowerShell занимает до 4 ГБ оперативной памяти при загрузке файла объемом 1 ГБ, проблема не связана с моей оперативной памятью, так как у меня всего 16 ГБ, и даже если игра работает в фоновом режиме, пиковое использование составляет около 60%.

Я использую Windows 10 с PowerShell 5.1 (64-разрядная версия ), и для моего MaxMemoryPerShellMB установлено значение по умолчанию 2147483647 .


Это сценарий, который я написал и использую, он отлично работает с размером файла, например, 100 МБ:

$source = "C:\Users\Env:USERNAME\Desktop\Test\"
$input = "test_1GB.txt"
$temp_dir = "_temp"

# 104'857'600 bytes (or characters) are exactly 100 MB, so a 1 GB file has exactly
# 10 temporary files, which have all the same size, and amount of lines and line lenghts.

$out_size = 104857600

# A line length of somewhere around 18'000 characters seems to be the sweet spot, however
# the line length needs to be dividable by 4 and at best fit exactly n times into the
# temporary file, so I use 16'384 bytes (or characters) which is exactly 16 KB.

$line_length = 16384



$file = (gc $input)
$in_size = (gc $input | measure -character | select -expand characters)
if (!(test-path $source$temp_dir)) {ni -type directory -path "$source$temp_dir" >$null 2>&1}

$n = 1
$i = 0

if ($out_size -eq $in_size) {
    $file -replace ".{$line_length}", "$&`r`n" | out-file -filepath "$temp_dir\_temp_0001.txt" -encoding ascii
} else {
    while ($i -le ($in_size - $out_size)) {
        $new_file = $file.substring($i,$out_size)
        if ($n -le 9) {$count = "000$n"} elseif ($n -le 99) {$count = "00$n"} elseif ($n -le 999) {$count = "0$n"} else {$count = $n}
        $temp_name = "_temp_$count.txt"
        $i += $out_size
        $n += 1
        $new_file -replace ".{$line_length}", "$&`r`n" | out-file -filepath "$temp_dir\$temp_name" -encoding ascii
    }
    if ($i -ne $in_size) {
        $new_file = $file.substring($i,($in_size-$i))
        if ($n -le 9) {$count = "000$n"} elseif ($n -le 99) {$count = "00$n"} elseif ($n -le 999) {$count = "0$n"} else {$count = $n}
        $temp_name = "_temp_$count.txt"
        $new_file -replace ".{$line_length}", "$&`r`n" | out-file -filepath "$temp_dir\$temp_name" -encoding ascii
    }
}

Если есть более простое решение, которое не использует Get-Content я также с радостью приму его. На самом деле не имеет большого значения, как я добиваюсь результата, если это возможно с каждой современной машиной Windows и без дополнительного программного обеспечения. Если это, однако, не будет возможно, я бы также рассмотрел другие решения.

1 ответ1

5

Чтение больших файлов в память просто для разделения, хотя и легкое, никогда не будет самым эффективным методом, и вы где-нибудь столкнетесь с ограничениями памяти.

Это еще более очевидно здесь, потому что Get-Content работает со строками - и, как вы упоминаете в комментариях, вы имеете дело с двоичными файлами.

.NET (и, следовательно, PowerShell) хранит все строки в памяти в виде единиц кода UTF-16. Это означает, что каждая единица кода занимает 2 байта в памяти.

Бывает, что одна строка .NET может хранить только (2 ^ 31 - 1) единиц кода, поскольку длина строки отслеживается Int32 (даже в 64-битных версиях). Умножьте это на 2, и одна строка .NET может (теоретически) использовать около 4 ГБ.

Get-Content будет хранить каждую строку в своей собственной строке. Если у вас есть одна строка с> 2 миллиардами символов ... вероятно, именно поэтому вы получаете эту ошибку, несмотря на то, что у вас "достаточно" памяти.

Кроме того, это может быть связано с тем, что для любого данного объекта существует ограничение в 2 ГБ, если только явно не включены большие размеры (для PowerShell?). Ваша OOM на 4 ГБ также может быть вызвана тем, что две копии / буферы хранятся, когда Get-Content пытается найти разрыв строки для разделения.

Решение, конечно же, заключается в работе с байтами, а не с символами (строками).


Если вы хотите избежать сторонних программ, лучший способ сделать это - обратиться к методам .NET. Это проще всего сделать с полным языком, таким как C # (который может быть встроен в PowerShell), но это можно сделать исключительно с помощью PS.

Идея в том, что вы хотите работать с байтовыми массивами, а не с текстовыми потоками. Есть два способа сделать это:

  • Используйте [System.IO.File]::ReadAllBytes и [System.IO.File]::WriteAllBytes . Это довольно просто и лучше, чем строки (без преобразования, без использования памяти в 2 раза), но все равно возникнут проблемы с очень большими файлами - скажем, вы хотели обработать файлы размером 100 ГБ?

  • Используйте файловые потоки и читайте / пишите небольшими порциями. Это требует немного больше математики, так как вам нужно отслеживать свою позицию, но вы избегаете чтения всего файла в память за один раз. Вероятно, это будет самый быстрый подход: выделение очень больших объектов, вероятно, перевесит накладные расходы при многократном чтении.

Таким образом, вы читаете порции разумного размера (в наши дни минимум 4 КБ за раз) и копируете их в выходной файл по одному порцию, а не считываете весь файл в память и разбиваете его. Вы можете настроить размер в сторону увеличения, например, 8 КБ, 16 КБ, 32 КБ и т.д., Если вам нужно выжать каждую последнюю каплю производительности, но вам нужно будет провести тестирование, чтобы найти оптимальный размер, так как некоторые большие размеры медленнее ,

Пример сценария приведен ниже. Для повторного использования его следует преобразовать в командлет или хотя бы функцию PS, но этого достаточно, чтобы служить рабочим примером.

$fileName = "foo"
$splitSize = 100MB

# need to sync .NET CurrentDirectory with PowerShell CurrentDirectory
# https://stackoverflow.com/questions/18862716/current-directory-from-a-dll-invoked-from-powershell-wrong
[Environment]::CurrentDirectory = Get-Location
# 4k is a fairly typical and 'safe' chunk size
# partial chunks are handled below
$bytes = New-Object byte[] 4096

$inFile = [System.IO.File]::OpenRead($fileName)

# track which output file we're up to
$fileCount = 0

# better to use functions but a flag is easier in a simple script
$finished = $false

while (!$finished) {
    $fileCount++
    $bytesToRead = $splitSize

    # Just like File::OpenWrite except CreateNew instead to prevent overwriting existing files
    $outFile = New-Object System.IO.FileStream "${fileName}_$fileCount",CreateNew,Write,None

    while ($bytesToRead) {
        # read up to 4k at a time, but no more than the remaining bytes in this split
        $bytesRead = $inFile.Read($bytes, 0, [Math]::Min($bytes.Length, $bytesToRead))

        # 0 bytes read means we've reached the end of the input file
        if (!$bytesRead) {
            $finished = $true
            break
        }

        $bytesToRead -= $bytesRead

        $outFile.Write($bytes, 0, $bytesRead)
    }

    # dispose closes the stream and releases locks
    $outFile.Dispose()
}

$inFile.Dispose()

Всё ещё ищете ответ? Посмотрите другие вопросы с метками .