Ler arquivos grandes na memória simplesmente para dividi-los, embora seja fácil, nunca será o método mais eficiente, e você encontrará os limites de memória em algum lugar .
Isto é ainda mais evidente aqui porque Get-Content
trabalha em strings - e, como você mencionou nos comentários, você está lidando com arquivos binários.
O .NET (e, portanto, o PowerShell) armazena todas as cadeias de caracteres na memória como unidades de código UTF-16. Isto significa que cada unidade de código ocupa 2 bytes na memória.
Ocorre que uma única string do .NET pode armazenar apenas unidades de código (2 ^ 31 - 1), já que o comprimento de uma string é rastreado por um Int32
(mesmo em versões de 64 bits). Multiplique isso por 2 e uma única string do .NET pode (teoricamente) usar cerca de 4 GB.
Get-Content
armazenará todas as linhas em sua própria sequência. Se você tiver uma única linha com > 2 bilhões de caracteres ... é provável que você esteja recebendo esse erro apesar de ter memória "suficiente".
Como alternativa, pode ser porque há um limite de 2 GB para qualquer objeto , a menos que tamanhos maiores sejam explicitamente ativados ( eles são para o PowerShell?). Sua OOM de 4 GB poderia também ser porque há duas cópias / buffers em volta, pois Get-Content
tenta encontrar uma quebra de linha para dividir.
A solução, claro, é trabalhar com bytes e não com caracteres (strings).
Se você quiser evitar programas de terceiros, a melhor maneira de fazer isso é entrar nos métodos .NET. Isso é feito mais facilmente com uma linguagem completa como o C # (que pode ser incorporada ao PowerShell), mas é possível fazer apenas com o PS.
A ideia é que você queira trabalhar com matrizes de bytes, não com fluxos de texto. Existem duas maneiras de fazer isso:
-
Use
[System.IO.File]::ReadAllBytes
e[System.IO.File]::WriteAllBytes
. Isso é muito fácil, e melhor que strings (sem conversão, sem uso de memória 2x), mas ainda terá problemas com arquivos muito grandes - digamos que você queira processar arquivos de 100 GB? -
Use fluxos de arquivos e leia / escreva em blocos menores. Isso requer um pouco mais de matemática, já que você precisa manter o controle de sua posição, mas evita ler o arquivo inteiro na memória de uma só vez. Essa provavelmente será a abordagem mais rápida: a alocação de objetos muito grandes provavelmente superará a sobrecarga de várias leituras.
Assim, você lê trechos de tamanho razoável (atualmente, o mínimo é de 4kB por vez) e os copia para o arquivo de saída um trecho de cada vez, em vez de ler todo o arquivo na memória e dividi-lo. Você pode querer ajustar o tamanho para cima, por exemplo 8kB, 16kB, 32kB, etc., se você precisar extrair até a última gota de desempenho - mas você precisaria fazer benchmark para encontrar o tamanho ideal, pois alguns tamanhos maiores são mais lentos.
Um exemplo de script é o seguinte. Para reusabilidade, ele deve ser convertido em um cmdlet ou pelo menos uma função PS, mas isso é suficiente para servir como um exemplo prático.
$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()