Come elaborare un file in PowerShell riga per riga come flusso


87

Sto lavorando con alcuni file di testo multi-gigabyte e desidero eseguire l'elaborazione del flusso su di essi utilizzando PowerShell. È roba semplice, basta analizzare ogni riga ed estrarre alcuni dati, quindi archiviarli in un database.

Sfortunatamente, get-content | %{ whatever($_) }sembra mantenere in memoria l'intero insieme di linee in questa fase del tubo. È anche sorprendentemente lento, impiegando molto tempo per leggerlo effettivamente tutto.

Quindi la mia domanda è composta da due parti:

  1. Come posso fare in modo che elabori lo stream riga per riga e non mantenga l'intera cosa memorizzata nel buffer? Vorrei evitare di utilizzare diversi giga di RAM per questo scopo.
  2. Come posso farlo funzionare più velocemente? L'iterazione di PowerShell su un get-contentsembra essere 100 volte più lenta di uno script C #.

Spero che ci sia qualcosa di stupido che sto facendo qui, come perdere un -LineBufferSizeparametro o qualcosa del genere ...


9
Per accelerare get-content, impostare -ReadCount su 512. Notare che a questo punto $ _ in Foreach sarà un array di stringhe.
Keith Hill

1
Tuttavia, seguirei il suggerimento di Roman di utilizzare il lettore .NET, molto più velocemente.
Keith Hill

Per curiosità, cosa succede se non mi interessa la velocità, ma solo la memoria? Molto probabilmente seguirò il suggerimento del lettore .NET, ma sono anche interessato a sapere come evitare che il buffering dell'intera pipe in memoria.
scobi

7
Per ridurre al minimo il buffering evitare di assegnare il risultato di Get-Contenta una variabile in quanto ciò caricherà l'intero file in memoria. Per impostazione predefinita, in una pipleline, Get-Contentelabora il file una riga alla volta. Finché non si accumulano i risultati o si utilizza un cmdlet che si accumula internamente (come Sort-Object e Group-Object), il colpo di memoria non dovrebbe essere troppo grave. Foreach-Object (%) è un modo sicuro per elaborare ogni riga, una alla volta.
Keith Hill

2
@dwarfsoft che non ha alcun senso. Il blocco -End viene eseguito solo una volta al termine dell'elaborazione. Puoi vedere che se provi a utilizzare, get-content | % -End { }si lamenta perché non hai fornito un blocco di processo. Quindi non può usare -End per impostazione predefinita, deve usare -Process per impostazione predefinita. E prova a 1..5 | % -process { } -end { 'q' }vedere che il blocco finale si verifica solo una volta, il solito gc | % { $_ }non funzionerebbe se lo scriptblock fosse impostato su -Fine ...
TessellatingHeckler

Risposte:


92

Se stai davvero per lavorare su file di testo multi-gigabyte, non utilizzare PowerShell. Anche se trovi un modo per leggerlo, l'elaborazione più rapida di enormi quantità di righe sarà comunque lenta in PowerShell e non puoi evitarlo. Anche i loop semplici sono costosi, diciamo per 10 milioni di iterazioni (abbastanza reali nel tuo caso) abbiamo:

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

AGGIORNAMENTO: se non hai ancora paura, prova a utilizzare il lettore .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()
}

AGGIORNAMENTO 2

Ci sono commenti su un codice forse migliore / più breve. Non c'è niente di sbagliato nel codice originale con fore non è uno pseudo-codice. Ma la variante più breve (più breve?) Del ciclo di lettura è

$reader = [System.IO.File]::OpenText("my.log")
while($null -ne ($line = $reader.ReadLine())) {
    $line
}

3
Cordiali saluti, la compilazione di script in PowerShell V3 migliora un po 'la situazione. Il ciclo di "lavoro reale" è passato da 117 secondi su V2 a 62 secondi su V3 digitati sulla console. Quando inserisco il ciclo in uno script e misuro l'esecuzione dello script su V3, scende a 34 secondi.
Keith Hill

Ho inserito tutti e tre i test in uno script e ho ottenuto questi risultati: V3 Beta: 20/27/83 secondi; V2: 14/21/101. Sembra che nel mio esperimento V3 sia più veloce nel test 3 ma è piuttosto più lento nei primi due. Bene, è Beta, si spera che le prestazioni saranno migliorate in RTM.
Roman Kuzmin

perché le persone insistono nell'usare un'interruzione in un ciclo del genere. Perché non utilizzare un ciclo che non lo richiede e legge meglio, come sostituire il ciclo for condo { $line = $reader.ReadLine(); $line } while ($line -neq $null)
BeowulfNode42

1
oops dovrebbe essere uno per non uguale. Quel particolare ciclo do..while ha il problema che verrà elaborato il valore nullo alla fine del file (in questo caso l'output). Per aggirare anche questo potresti averefor ( $line = $reader.ReadLine(); $line -ne $null; $line = $reader.ReadLine() ) { $line }
BeowulfNode42

4
@ BeowulfNode42, possiamo farlo ancora più breve: while($null -ne ($line = $read.ReadLine())) {$line}. Ma l'argomento non riguarda davvero queste cose.
Roman Kuzmin

51

System.IO.File.ReadLines()è perfetto per questo scenario. Restituisce tutte le righe di un file, ma consente di iniziare immediatamente l'iterazione sulle righe, il che significa che non è necessario memorizzare l'intero contenuto in memoria.

Richiede .NET 4.0 o superiore.

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

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


6
È necessaria una nota: .NET Framework - Supportato in: 4.5, 4. Pertanto, questo potrebbe non funzionare in V2 o V1 su alcune macchine.
Roman Kuzmin

Questo mi ha dato System.IO.File non esiste errore, ma il codice sopra di Roman ha funzionato per me
Kolob Canyon

Questo era proprio ciò di cui avevo bisogno ed è stato facile inserirlo direttamente in uno script PowerShell esistente.
user1751825

5

Se vuoi usare PowerShell diretto controlla il codice seguente.

$content = Get-Content C:\Users\You\Documents\test.txt
foreach ($line in $content)
{
    Write-Host $line
}

16
Questo è ciò di cui l'OP voleva sbarazzarsi perché Get-Contentè molto lento su file di grandi dimensioni.
Roman Kuzmin
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.