So verarbeiten Sie eine Datei in PowerShell zeilenweise als Stream

87

Ich arbeite mit einigen Multi-Gigabyte-Textdateien und möchte mit PowerShell Stream-Verarbeitung für sie durchführen. Es ist ganz einfach, jede Zeile zu analysieren, einige Daten abzurufen und sie dann in einer Datenbank zu speichern.

Leider get-content | %{ whatever($_) }scheint der gesamte Satz von Zeilen in dieser Phase der Pipe im Speicher zu bleiben. Es ist auch überraschend langsam und es dauert sehr lange, alles tatsächlich einzulesen.

Meine Frage besteht also aus zwei Teilen:

  1. Wie kann ich dafür sorgen, dass der Stream zeilenweise verarbeitet wird und nicht das gesamte Objekt im Speicher gepuffert wird? Ich möchte vermeiden, mehrere GB RAM für diesen Zweck zu verbrauchen.
  2. Wie kann ich es schneller laufen lassen? PowerShell, das über a iteriert, get-contentscheint 100-mal langsamer zu sein als ein C # -Skript.

Ich hoffe, ich mache hier etwas Dummes, wie das Fehlen eines -LineBufferSizeParameters oder so ...

scobi
quelle
9
Um dies zu beschleunigen get-content, setzen Sie -ReadCount auf 512. Beachten Sie, dass $ _ im Foreach zu diesem Zeitpunkt ein Array von Zeichenfolgen ist.
Keith Hill
1
Trotzdem würde ich Romans Vorschlag folgen, den .NET-Reader zu verwenden - viel schneller.
Keith Hill
Was passiert aus Neugier, wenn mir Geschwindigkeit nicht wichtig ist, sondern nur das Gedächtnis? Höchstwahrscheinlich werde ich dem Vorschlag für einen .NET-Reader folgen, aber ich bin auch daran interessiert zu wissen, wie verhindert werden kann, dass die gesamte Pipe im Speicher gepuffert wird.
Scobi
7
Um die Pufferung zu minimieren, vermeiden Sie es, das Ergebnis Get-Contenteiner Variablen zuzuweisen, da dadurch die gesamte Datei in den Speicher geladen wird. Standardmäßig Get-Contentverarbeitet die Datei in einer Pipleline zeilenweise. Solange Sie die Ergebnisse nicht akkumulieren oder ein intern akkumuliertes Cmdlet verwenden (wie Sort-Object und Group-Object), sollte der Speicher-Hit nicht zu schlecht sein. Foreach-Object (%) ist eine sichere Methode, um jede Zeile einzeln zu verarbeiten.
Keith Hill
2
@ Dwarfsoft das macht keinen Sinn. Der Block -End wird nur einmal ausgeführt, nachdem die gesamte Verarbeitung abgeschlossen ist. Sie können sehen, dass get-content | % -End { }es sich beschwert , wenn Sie versuchen, es zu verwenden , weil Sie keinen Prozessblock angegeben haben. Daher kann standardmäßig nicht -End verwendet werden, sondern standardmäßig -Process. Und versuchen Sie zu 1..5 | % -process { } -end { 'q' }sehen, dass der gc | % { $_ }Endblock nur einmal vorkommt. Das Übliche würde nicht funktionieren, wenn der Skriptblock standardmäßig -End ...
TessellatingHeckler

Antworten:

91

Wenn Sie wirklich an Textdateien mit mehreren Gigabyte arbeiten möchten, verwenden Sie PowerShell nicht. Selbst wenn Sie einen Weg finden, es schneller zu lesen, wird die Verarbeitung einer großen Anzahl von Zeilen in PowerShell ohnehin langsam sein, und Sie können dies nicht vermeiden. Selbst einfache Schleifen sind teuer, sagen wir für 10 Millionen Iterationen (in Ihrem Fall ziemlich real), die wir haben:

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

UPDATE: Wenn Sie immer noch keine Angst haben, versuchen Sie, den .NET-Reader zu verwenden:

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

UPDATE 2

Es gibt Kommentare zu möglicherweise besserem / kürzerem Code. Es ist nichts falsch mit dem Originalcode forund es ist kein Pseudocode. Die kürzere (kürzeste?) Variante der Leseschleife ist jedoch

$reader = [System.IO.File]::OpenText("my.log")
while($null -ne ($line = $reader.ReadLine())) {
    $line
}
Roman Kuzmin
quelle
3
Zu Ihrer Information, die Skriptkompilierung in PowerShell V3 verbessert die Situation ein wenig. Die "Real Job" -Schleife ging von 117 Sekunden auf V2 auf 62 Sekunden auf V3, die an der Konsole eingegeben wurden. Wenn ich die Schleife in ein Skript einfüge und die Skriptausführung in V3 messe, sinkt sie auf 34 Sekunden.
Keith Hill
Ich habe alle drei Tests in ein Skript geschrieben und folgende Ergebnisse erhalten: V3 Beta: 20/27/83 Sekunden; V2: 14/21/101. Es sieht so aus, als ob in meinem Experiment V3 in Test 3 schneller ist, aber in den ersten beiden ist es ziemlich langsamer. Nun, es ist Beta, hoffentlich wird die Leistung in RTM verbessert.
Roman Kuzmin
Warum bestehen die Leute darauf, eine Pause in einer solchen Schleife zu machen? Warum nicht eine Schleife verwenden, die dies nicht erfordert und besser liest, z. B. die for-Schleife durchdo { $line = $reader.ReadLine(); $line } while ($line -neq $null)
BeowulfNode42
1
Hoppla, das soll -ne für nicht gleich sein. Diese spezielle do..while-Schleife hat das Problem, dass die Null am Ende der Datei verarbeitet wird (in diesem Fall die Ausgabe). Um das auch zu for ( $line = $reader.ReadLine(); $line -ne $null; $line = $reader.ReadLine() ) { $line }
umgehen,
4
@ BeowulfNode42, wir können dies noch kürzer machen : while($null -ne ($line = $read.ReadLine())) {$line}. Aber das Thema handelt nicht wirklich von solchen Dingen.
Roman Kuzmin
49

System.IO.File.ReadLines()ist perfekt für dieses Szenario. Es gibt alle Zeilen einer Datei zurück, aber Sie können sofort mit dem Durchlaufen der Zeilen beginnen, sodass nicht der gesamte Inhalt im Speicher gespeichert werden muss.

Benötigt .NET 4.0 oder höher.

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

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

Despertar
quelle
6
Ein Hinweis ist erforderlich: .NET Framework - Unterstützt in: 4.5, 4. Daher funktioniert dies auf einigen Computern möglicherweise nicht in V2 oder V1.
Roman Kuzmin
Dies gab mir System.IO.File existiert kein Fehler, aber der Code oben von Roman funktionierte für mich
Kolob Canyon
Dies war genau das, was ich brauchte, und es war einfach, es direkt in ein vorhandenes Powershell-Skript zu integrieren.
user1751825
5

Wenn Sie PowerShell verwenden möchten, lesen Sie den folgenden Code.

$content = Get-Content C:\Users\You\Documents\test.txt
foreach ($line in $content)
{
    Write-Host $line
}
Chris Blydenstein
quelle
16
Das wollte das OP loswerden, weil Get-Contentes bei großen Dateien sehr langsam ist.
Roman Kuzmin