Cómo procesar un archivo en PowerShell línea por línea como una secuencia

87

Estoy trabajando con algunos archivos de texto de varios gigabytes y quiero hacer un procesamiento de transmisión en ellos usando PowerShell. Es algo simple, simplemente analizar cada línea y extraer algunos datos, luego almacenarlos en una base de datos.

Desafortunadamente, get-content | %{ whatever($_) }parece mantener todo el conjunto de líneas en esta etapa de la tubería en la memoria. También es sorprendentemente lento, y lleva mucho tiempo leerlo todo.

Entonces mi pregunta tiene dos partes:

  1. ¿Cómo puedo hacer que procese la secuencia línea por línea y no mantenga todo en búfer en la memoria? Me gustaría evitar utilizar varios gigas de RAM para este propósito.
  2. ¿Cómo puedo hacer que funcione más rápido? La iteración de PowerShell sobre un get-contentparece ser 100 veces más lenta que un script de C #.

Espero que haya algo tonto que estoy haciendo aquí, como perder un -LineBufferSizeparámetro o algo ...

Scobi
fuente
9
Para acelerar get-content, establezca -ReadCount en 512. Tenga en cuenta que en este punto, $ _ en Foreach será una matriz de cadenas.
Keith Hill
1
Aún así, seguiría la sugerencia de Roman de usar el lector .NET, mucho más rápido.
Keith Hill
Por curiosidad, ¿qué pasa si no me importa la velocidad, sino solo la memoria? Lo más probable es que siga la sugerencia del lector de .NET, pero también me interesa saber cómo evitar que almacene en búfer toda la tubería en la memoria.
Scobi
7
Para minimizar el almacenamiento en búfer, evite asignar el resultado de Get-Contenta una variable, ya que cargará todo el archivo en la memoria. De forma predeterminada, en una pipleline, Get-Contentprocesa el archivo una línea a la vez. Siempre que no esté acumulando los resultados o utilizando un cmdlet que se acumula internamente (como Sort-Object y Group-Object), el impacto de la memoria no debería ser tan malo. Foreach-Object (%) es una forma segura de procesar cada línea, una a la vez.
Keith Hill
2
@dwarfsoft eso no tiene ningún sentido. El bloque -End solo se ejecuta una vez después de que se realiza todo el procesamiento. Puede ver que si intenta usarlo get-content | % -End { }, se queja porque no ha proporcionado un bloque de proceso. Por lo tanto, no puede usar -End por defecto, debe usar -Process por defecto. Y trate de 1..5 | % -process { } -end { 'q' }ver que el bloque final solo ocurre una vez, lo habitual gc | % { $_ }no funcionaría si el bloque de secuencia de comandos estuviera predeterminado en -End ...
TessellatesHeckler

Respuestas:

92

Si realmente está a punto de trabajar con archivos de texto de varios gigabytes, no utilice PowerShell. Incluso si encuentra una manera de leerlo, el procesamiento más rápido de una gran cantidad de líneas será lento en PowerShell de todos modos y no puede evitarlo. Incluso los bucles simples son costosos, digamos para 10 millones de iteraciones (bastante reales en su caso) tenemos:

# "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' } }

ACTUALIZACIÓN: Si aún no tiene miedo, intente usar el lector .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()
}

ACTUALIZACIÓN 2

Hay comentarios sobre un código posiblemente mejor / más corto. No hay nada de malo con el código original fory no es un pseudocódigo. Pero la variante más corta (¿más corta?) Del ciclo de lectura es

$reader = [System.IO.File]::OpenText("my.log")
while($null -ne ($line = $reader.ReadLine())) {
    $line
}
Roman Kuzmin
fuente
3
Para su información, la compilación de scripts en PowerShell V3 mejora un poco la situación. El ciclo de "trabajo real" pasó de 117 segundos en V2 a 62 segundos en V3 tecleado en la consola. Cuando coloco el bucle en un script y mido la ejecución del script en V3, cae a 34 segundos.
Keith Hill
Puse las tres pruebas en un script y obtuve estos resultados: V3 Beta: 20/27/83 segundos; V2: 14/21/101. Parece que en mi experimento, la V3 es más rápida en la prueba 3, pero es bastante más lenta en las dos primeras. Bueno, es Beta, espero que el rendimiento mejore en RTM.
Roman Kuzmin
¿Por qué la gente insiste en utilizar una ruptura en un bucle como ese? ¿Por qué no usar un bucle que no lo requiera y que se lea mejor, como reemplazar el bucle for condo { $line = $reader.ReadLine(); $line } while ($line -neq $null)
BeowulfNode42
1
Ups, se supone que es -ne por no igual. Ese bucle do.. while en particular tiene el problema de que se procesará el nulo al final del archivo (en este caso, la salida). Para for ( $line = $reader.ReadLine(); $line -ne $null; $line = $reader.ReadLine() ) { $line }
solucionar
4
@ BeowulfNode42, podemos hacer esto aún más corto: while($null -ne ($line = $read.ReadLine())) {$line}. Pero el tema no se trata realmente de esas cosas.
Roman Kuzmin
51

System.IO.File.ReadLines()es perfecto para este escenario. Devuelve todas las líneas de un archivo, pero le permite comenzar a iterar sobre las líneas inmediatamente, lo que significa que no tiene que almacenar todo el contenido en la memoria.

Requiere .NET 4.0 o superior.

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

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

Despertar
fuente
6
Se necesita una nota: .NET Framework: compatible con: 4.5, 4. Por lo tanto, es posible que esto no funcione en V2 o V1 en algunas máquinas.
Roman Kuzmin
Esto me dio el error System.IO.File no existe, pero el código anterior de Roman funcionó para mí
Kolob Canyon
Esto era justo lo que necesitaba y era fácil de colocar directamente en un script de PowerShell existente.
user1751825
5

Si desea utilizar PowerShell directo, consulte el siguiente código.

$content = Get-Content C:\Users\You\Documents\test.txt
foreach ($line in $content)
{
    Write-Host $line
}
Chris Blydenstein
fuente
16
Eso es de lo que el OP quería deshacerse porque Get-Contentes muy lento en archivos grandes.
Roman Kuzmin