Jak przetworzyć plik w programie PowerShell wiersz po wierszu jako strumień

87

Pracuję z kilkoma wielogigabajtowymi plikami tekstowymi i chcę wykonać na nich przetwarzanie strumieniowe za pomocą PowerShell. To proste, wystarczy przeanalizować każdą linię i wyciągnąć trochę danych, a następnie zapisać je w bazie danych.

Niestety, get-content | %{ whatever($_) }wydaje się, że cały zestaw linii na tym etapie rury jest w pamięci. Jest również zaskakująco wolny, a jego przeczytanie zajmuje bardzo dużo czasu.

Więc moje pytanie składa się z dwóch części:

  1. Jak sprawić, by przetwarzał strumień linia po linii i nie przechowywał całej rzeczy w pamięci? Chciałbym uniknąć zużywania w tym celu kilku gigabajtów pamięci RAM.
  2. Jak mogę sprawić, by działał szybciej? Iteracja programu PowerShell po a get-contentwydaje się być 100 razy wolniejsza niż skrypt C #.

Mam nadzieję, że robię tu coś głupiego, na przykład brak -LineBufferSizeparametru lub coś ...

scobi
źródło
9
Aby przyspieszyć get-content, ustaw -ReadCount na 512. Zauważ, że w tym momencie $ _ w Foreach będzie tablicą ciągów.
Keith Hill
1
Mimo to, zgodziłbym się z sugestią Romana, aby użyć czytnika .NET - znacznie szybciej.
Keith Hill
Z ciekawości, co się stanie, jeśli nie obchodzi mnie szybkość, a tylko pamięć? Najprawdopodobniej skorzystam z sugestii czytnika .NET, ale interesuje mnie również, jak zapobiec buforowaniu całego potoku w pamięci.
scobi
7
Aby zminimalizować buforowanie, unikaj przypisywania wyniku Get-Contentdo zmiennej, ponieważ spowoduje to załadowanie całego pliku do pamięci. Domyślnie w pipleline Get-Contentprzetwarza plik po jednej linii na raz. Dopóki nie gromadzisz wyników ani nie używasz polecenia cmdlet, które gromadzi się wewnętrznie (np. Sort-Object i Group-Object), trafienie w pamięć nie powinno być takie złe. Foreach-Object (%) to bezpieczny sposób przetwarzania każdej linii, pojedynczo.
Keith Hill
2
@dwarfsoft to nie ma sensu. Blok -End jest uruchamiany tylko raz po zakończeniu całego przetwarzania. Możesz zobaczyć, że jeśli spróbujesz użyć get-content | % -End { }, narzeka, ponieważ nie podałeś bloku procesu. Więc nie może domyślnie używać -End, musi domyślnie używać -Process. I spróbuj 1..5 | % -process { } -end { 'q' }zobaczyć, że blok końcowy zdarza się tylko raz, zwykle gc | % { $_ }nie działałby, gdyby blok skryptu domyślnie był -End ...
TessellatingHeckler

Odpowiedzi:

92

Jeśli naprawdę zamierzasz pracować na wielogigabajtowych plikach tekstowych, nie używaj programu PowerShell. Nawet jeśli znajdziesz sposób, aby go przeczytać, szybsze przetwarzanie ogromnej liczby linii i tak będzie wolne w PowerShell i nie możesz tego uniknąć. Nawet proste pętle są drogie, powiedzmy na 10 milionów iteracji (całkiem realne w twoim przypadku) mamy:

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

AKTUALIZACJA: Jeśli nadal się nie boisz, spróbuj użyć czytnika .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()
}

AKTUALIZACJA 2

Pojawiają się komentarze na temat prawdopodobnie lepszego / krótszego kodu. Nie ma nic złego w oryginalnym kodzie z fori nie jest to pseudokod. Ale krótszy (najkrótszy?) Wariant pętli czytania jest

$reader = [System.IO.File]::OpenText("my.log")
while($null -ne ($line = $reader.ReadLine())) {
    $line
}
Roman Kuzmin
źródło
3
FYI, kompilacja skryptów w PowerShell V3 nieco poprawia sytuację. Pętla „prawdziwego zadania” trwała od 117 sekund w wersji V2 do 62 sekund w wersji V3 wpisywanej na konsoli. Kiedy umieszczam pętlę w skrypcie i mierzę wykonanie skryptu w V3, spada do 34 sekund.
Keith Hill
Umieściłem wszystkie trzy testy w skrypcie i otrzymałem następujące wyniki: V3 Beta: 20/27/83 sekundy; V2: 14/21/101. Wygląda na to, że w moim eksperymencie V3 jest szybszy w teście 3, ale w pierwszych dwóch jest dość wolniejszy. Cóż, to Beta, miejmy nadzieję, że wydajność zostanie poprawiona w RTM.
Roman Kuzmin
dlaczego ludzie upierają się przy używaniu przerwy w takiej pętli. Dlaczego nie użyć pętli, która jej nie wymaga i czyta lepiej, na przykład zastąpienie pętli for przezdo { $line = $reader.ReadLine(); $line } while ($line -neq $null)
BeowulfNode42
1
ups, to powinno być -ne zamiast równego. Ta konkretna pętla do..while ma problem z przetwarzaniem wartości null na końcu pliku (w tym przypadku danych wyjściowych). Aby obejść to też, możeszfor ( $line = $reader.ReadLine(); $line -ne $null; $line = $reader.ReadLine() ) { $line }
BeowulfNode42
4
@ BeowulfNode42, możemy zrobić to jeszcze krócej: while($null -ne ($line = $read.ReadLine())) {$line}. Ale temat tak naprawdę nie dotyczy takich rzeczy.
Roman Kuzmin
51

System.IO.File.ReadLines()jest idealny do tego scenariusza. Zwraca wszystkie wiersze pliku, ale pozwala natychmiast rozpocząć iterację po wierszach, co oznacza, że ​​nie musi przechowywać całej zawartości w pamięci.

Wymaga platformy .NET 4.0 lub nowszej.

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

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

Despertar
źródło
6
Konieczna jest uwaga: .NET Framework - obsługiwane w: 4.5, 4. W związku z tym może nie działać w wersji 2 lub 1 na niektórych komputerach.
Roman Kuzmin,
To dało mi System.IO.File nie istnieje błąd, ale powyższy kod Roman pracował dla mnie
kolob Canyon
To było właśnie to, czego potrzebowałem i łatwo było je przenieść bezpośrednio do istniejącego skryptu PowerShell.
user1751825
5

Jeśli chcesz korzystać z prostego programu PowerShell, zapoznaj się z poniższym kodem.

$content = Get-Content C:\Users\You\Documents\test.txt
foreach ($line in $content)
{
    Write-Host $line
}
Chris Blydenstein
źródło
16
To jest to, czego OP chciał się pozbyć, ponieważ Get-Contentdziała bardzo wolno w przypadku dużych plików.
Roman Kuzmin