Como processar um arquivo no PowerShell linha por linha como um fluxo

87

Estou trabalhando com alguns arquivos de texto de vários gigabytes e quero fazer algum processamento de stream neles usando o PowerShell. É uma coisa simples, apenas analisar cada linha e extrair alguns dados, em seguida, armazená-los em um banco de dados.

Infelizmente, get-content | %{ whatever($_) }parece manter todo o conjunto de linhas neste estágio do tubo na memória. Também é surpreendentemente lento, levando muito tempo para realmente ler tudo.

Portanto, minha pergunta tem duas partes:

  1. Como posso fazer com que ele processe o fluxo linha por linha e não mantenha tudo armazenado na memória? Eu gostaria de evitar o uso de vários GB de RAM para essa finalidade.
  2. Como posso fazê-lo funcionar mais rápido? A iteração do PowerShell em um get-contentparece ser 100x mais lenta do que um script C #.

Espero que haja algo estúpido que estou fazendo aqui, como perder um -LineBufferSizeparâmetro ou algo assim ...

Scobi
fonte
9
Para acelerar get-content, defina -ReadCount como 512. Observe que, neste ponto, $ _ no Foreach será uma matriz de strings.
Keith Hill
1
Ainda assim, eu aceitaria a sugestão de Roman de usar o leitor .NET - muito mais rápido.
Keith Hill
Por curiosidade, o que acontece se eu não me importar com a velocidade, mas apenas com a memória? Provavelmente aceitarei a sugestão do leitor .NET, mas também estou interessado em saber como evitar que ele armazene todo o pipe na memória.
scobi
7
Para minimizar o buffer, evite atribuir o resultado de Get-Contenta uma variável, pois isso carregará o arquivo inteiro na memória. Por padrão, em um pipleline, Get-Contentprocessa o arquivo uma linha de cada vez. Contanto que você não esteja acumulando os resultados ou usando um cmdlet que se acumula internamente (como Sort-Object e Group-Object), a ocorrência de memória não deve ser tão ruim. Foreach-Object (%) é uma maneira segura de processar cada linha, uma de cada vez.
Keith Hill
2
@dwarfsoft isso não faz sentido. O bloco -End é executado apenas uma vez depois que todo o processamento é concluído. Você pode ver que, se tentar usar get-content | % -End { }, ele reclamará porque você não forneceu um bloco de processo. Portanto, ele não pode estar usando -End por padrão, ele deve estar usando -Process por padrão. E tente 1..5 | % -process { } -end { 'q' }ver que o bloco final só acontece uma vez, o normal gc | % { $_ }não funcionaria se o scriptblock fosse -End ...
TessellatingHeckler

Respostas:

91

Se você estiver realmente prestes a trabalhar com arquivos de texto de vários gigabytes, não use o PowerShell. Mesmo se você encontrar uma maneira de lê-lo, o processamento mais rápido de uma grande quantidade de linhas será lento no PowerShell de qualquer maneira e você não pode evitar isso. Mesmo loops simples são caros, digamos, para 10 milhões de iterações (bastante reais no seu caso), temos:

# "empty" loop: takes 10 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) {} }

# "simple" job, just output: takes 20 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) { $i } }

# "more real job": 107 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) { $i.ToString() -match '1' } }

ATUALIZAÇÃO: se ainda não tiver medo, tente usar o leitor .NET:

$reader = [System.IO.File]::OpenText("my.log")
try {
    for() {
        $line = $reader.ReadLine()
        if ($line -eq $null) { break }
        # process the line
        $line
    }
}
finally {
    $reader.Close()
}

ATUALIZAÇÃO 2

Há comentários sobre códigos possivelmente melhores / mais curtos. Não há nada de errado com o código original fore não é um pseudocódigo. Mas a variante mais curta (mais curta?) Do ciclo de leitura é

$reader = [System.IO.File]::OpenText("my.log")
while($null -ne ($line = $reader.ReadLine())) {
    $line
}
Roman Kuzmin
fonte
3
Para sua informação, a compilação de script no PowerShell V3 melhora um pouco a situação. O loop de "trabalho real" foi de 117 segundos no V2 para 62 segundos no V3 digitado no console. Quando coloco o loop em um script e mede a execução do script no V3, ele cai para 34 segundos.
Keith Hill
Eu coloquei todos os três testes em um script e obtive estes resultados: V3 Beta: 20/27/83 segundos; V2: 14/21/101. Parece que no meu experimento o V3 é mais rápido no teste 3, mas é bem mais lento nos dois primeiros. Bem, é beta, espero que o desempenho seja melhorado no RTM.
Roman Kuzmin
por que as pessoas insistem em usar uma pausa em um loop como esse. Por que não usar um loop que não exija e tenha uma leitura melhor, como substituir o loop for pordo { $line = $reader.ReadLine(); $line } while ($line -neq $null)
BeowulfNode42
1
opa, isso deveria ser -ne para não igual. Esse loop do..while em particular tem o problema de que o nulo no final do arquivo será processado (neste caso, a saída). Para contornar isso também, você poderia terfor ( $line = $reader.ReadLine(); $line -ne $null; $line = $reader.ReadLine() ) { $line }
BeowulfNode42
4
@ BeowulfNode42, podemos fazer isso ainda mais curto: while($null -ne ($line = $read.ReadLine())) {$line}. Mas o assunto não é realmente sobre essas coisas.
Roman Kuzmin de
51

System.IO.File.ReadLines()é perfeito para este cenário. Ele retorna todas as linhas de um arquivo, mas permite que você comece a iterar sobre as linhas imediatamente, o que significa que não é necessário armazenar todo o conteúdo na memória.

Requer .NET 4.0 ou superior.

foreach ($line in [System.IO.File]::ReadLines($filename)) {
    # do something with $line
}

http://msdn.microsoft.com/en-us/library/dd383503.aspx

Despertar
fonte
6
Uma observação é necessária: .NET Framework - com suporte em: 4.5, 4. Portanto, isso pode não funcionar em V2 ou V1 em algumas máquinas.
Roman Kuzmin
Isso me deu o erro System.IO.File does not exist, mas o código acima de Roman funcionou para mim
Kolob Canyon
Isso era exatamente o que eu precisava e foi fácil de inserir diretamente em um script PowerShell existente.
user1751825
5

Se você quiser usar o PowerShell direto, verifique o código abaixo.

$content = Get-Content C:\Users\You\Documents\test.txt
foreach ($line in $content)
{
    Write-Host $line
}
Chris Blydenstein
fonte
16
Era disso que o OP queria se livrar, porque Get-Contenté muito lento em arquivos grandes.
Roman Kuzmin