Cara memproses file di PowerShell baris demi baris sebagai aliran

90

Saya bekerja dengan beberapa file teks multi-gigabyte dan ingin melakukan pemrosesan streaming menggunakan PowerShell. Ini hal sederhana, hanya mengurai setiap baris dan menarik beberapa data, lalu menyimpannya dalam database.

Sayangnya, get-content | %{ whatever($_) }tampaknya menyimpan seluruh rangkaian garis pada tahap pipa ini dalam memori. Ini juga sangat lambat, membutuhkan waktu yang sangat lama untuk benar-benar membaca semuanya.

Jadi pertanyaan saya ada dua bagian:

  1. Bagaimana cara membuatnya memproses baris demi baris dan tidak menyimpan semuanya dalam buffer di memori? Saya ingin menghindari penggunaan beberapa gigs RAM untuk tujuan ini.
  2. Bagaimana cara membuatnya berjalan lebih cepat? PowerShell yang melakukan iterasi pada a get-contenttampaknya 100x lebih lambat daripada skrip C #.

Saya berharap ada sesuatu yang bodoh yang saya lakukan di sini, seperti melewatkan -LineBufferSizeparameter atau sesuatu ...

scobi
sumber
9
Untuk mempercepat get-content, set -ReadCount ke 512. Perhatikan bahwa pada titik ini, $ _ di Foreach akan menjadi sebuah array string.
Keith Hill
1
Namun, saya akan mengikuti saran Roman untuk menggunakan pembaca NET - jauh lebih cepat.
Keith Hill
Karena penasaran, apa yang terjadi jika saya tidak peduli dengan kecepatan, tetapi hanya memori? Kemungkinan besar saya akan pergi dengan saran pembaca .NET, tetapi saya juga tertarik untuk mengetahui bagaimana mencegahnya menyangga seluruh pipa dalam memori.
scobi
7
Untuk meminimalkan buffering, hindari menetapkan hasil Get-Contentke variabel karena akan memuat seluruh file ke dalam memori. Secara default, dalam pipeline, Get-Contentmemproses file satu baris dalam satu waktu. Selama Anda tidak mengumpulkan hasil atau menggunakan cmdlet yang terakumulasi secara internal (seperti Sort-Object dan Group-Object) maka hit memori seharusnya tidak terlalu buruk. Foreach-Object (%) adalah cara yang aman untuk memproses setiap baris, satu per satu.
Keith Hill
2
@dwarfsoft itu tidak masuk akal. Blok -End hanya berjalan sekali setelah semua pemrosesan selesai. Anda dapat melihat bahwa jika Anda mencoba menggunakan get-content | % -End { }maka ia mengeluh karena Anda belum menyediakan blok proses. Jadi tidak bisa menggunakan -End secara default, itu harus menggunakan -Process secara default. Dan coba 1..5 | % -process { } -end { 'q' }lihat bahwa blok akhir hanya terjadi sekali, hal biasa gc | % { $_ }tidak akan berfungsi jika scriptblock default menjadi -End ...
TessellatingHeckler

Jawaban:

93

Jika Anda benar-benar akan mengerjakan file teks multi-gigabyte, jangan gunakan PowerShell. Bahkan jika Anda menemukan cara untuk membacanya, pemrosesan lebih cepat dari sejumlah besar baris akan menjadi lambat di PowerShell dan Anda tidak dapat menghindari ini. Bahkan loop sederhana pun mahal, katakanlah untuk 10 juta iterasi (cukup nyata dalam kasus Anda) kami memiliki:

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

MEMPERBARUI: Jika Anda masih tidak takut, coba gunakan pembaca .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()
}

UPDATE 2

Ada komentar tentang kode yang mungkin lebih baik / lebih pendek. Tidak ada yang salah dengan kode asli fordan ini bukan kode semu. Tetapi varian yang lebih pendek (terpendek?) Dari loop pembacaan adalah

$reader = [System.IO.File]::OpenText("my.log")
while($null -ne ($line = $reader.ReadLine())) {
    $line
}
Roman Kuzmin
sumber
3
FYI, kompilasi skrip di PowerShell V3 sedikit memperbaiki situasi. Putaran "pekerjaan nyata" berubah dari 117 detik pada V2 menjadi 62 detik pada V3 yang diketik di konsol. Ketika saya memasukkan loop ke dalam skrip dan mengukur eksekusi skrip pada V3, itu turun menjadi 34 detik.
Keith Hill
Saya menempatkan ketiga tes dalam sebuah skrip dan mendapatkan hasil ini: V3 Beta: 20/27/83 detik; V2: 14/21/101. Sepertinya dalam percobaan saya, V3 lebih cepat pada pengujian 3 tetapi cukup lambat pada dua percobaan pertama. Nah ini Beta, semoga performanya meningkat di RTM.
Roman Kuzmin
mengapa orang bersikeras menggunakan jeda dalam lingkaran seperti itu. Mengapa tidak menggunakan perulangan yang tidak memerlukannya, dan berbunyi lebih baik seperti mengganti perulangan for dengando { $line = $reader.ReadLine(); $line } while ($line -neq $null)
BeowulfNode42
1
Ups itu seharusnya menjadi -ne untuk tidak sama. Itu do.. while loop tertentu memiliki masalah bahwa null di akhir file akan diproses (dalam hal ini output). Untuk menyiasatinya juga Anda bisafor ( $line = $reader.ReadLine(); $line -ne $null; $line = $reader.ReadLine() ) { $line }
BeowulfNode42
4
@ BeowulfNode42, kita dapat melakukan hal ini bahkan lebih pendek: while($null -ne ($line = $read.ReadLine())) {$line}. Tapi sebenarnya topiknya bukan tentang hal-hal seperti itu.
Roman Kuzmin
52

System.IO.File.ReadLines()sempurna untuk skenario ini. Ini mengembalikan semua baris file, tetapi memungkinkan Anda mulai mengulang baris segera yang berarti tidak harus menyimpan seluruh konten dalam memori.

Membutuhkan .NET 4.0 atau lebih tinggi.

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

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

Despertar
sumber
6
Diperlukan catatan: .NET Framework - Didukung di: 4.5, 4. Dengan demikian, ini mungkin tidak berfungsi di V2 atau V1 pada beberapa mesin.
Roman Kuzmin
Ini memberi saya System.IO.File tidak ada kesalahan, tetapi kode di atas oleh Roman bekerja untuk saya
Kolob Canyon
Inilah yang saya butuhkan, dan mudah untuk langsung dimasukkan ke skrip PowerShell yang sudah ada.
pengguna1751825
5

Jika Anda ingin menggunakan PowerShell langsung, lihat kode di bawah ini.

$content = Get-Content C:\Users\You\Documents\test.txt
foreach ($line in $content)
{
    Write-Host $line
}
Chris Blydenstein
sumber
16
Itulah yang OP ingin singkirkan karena Get-Contentsangat lambat pada file besar.
Roman Kuzmin