PowerShellでファイルをストリームとして1行ずつ処理する方法


87

マルチギガバイトのテキストファイルを使用していて、PowerShellを使用してそれらのストリーム処理を実行したいと考えています。それは単純なもので、各行を解析してデータを取り出し、データベースに保存するだけです。

残念ながら、get-content | %{ whatever($_) }パイプのこの段階で行のセット全体をメモリに保持しているようです。また、これは驚くほど遅く、実際にすべてを読み取るには非常に長い時間がかかります。

だから私の質問は2つの部分です:

  1. どのようにしてストリーム全体を1行ずつ処理し、全体をバッファリングしてメモリに保持しないようにできますか?この目的のために数ギガのRAMを使い切るのを避けたいと思います。
  2. どうすれば速く実行できますか?PowerShellの反復処理get-contentは、C#スクリプトよりも100倍遅いようです。

私はここでやっている-LineBufferSize何かパラメーターが欠けているような何かばかげていることを願っています...


9
速度をget-content上げるには、-ReadCountを512に設定します。この時点で、Foreachの$ _は文字列の配列になることに注意してください。
キース・ヒル

1
それでも、私は.NETリーダーを使用するというローマの提案に従います-はるかに高速です。
キース・ヒル

好奇心から、速度を気にせずにメモリだけを気にするとどうなりますか?ほとんどの場合、.NETリーダーの提案を使用しますが、パイプ全体がメモリにバッファリングされないようにする方法についても知りたいです。
scobi 2010年

7
バッファリングを最小限に抑えるにGet-Contentは、ファイル全体をメモリにロードするため、変数の結果を変数に割り当てないでください。デフォルトでは、piplelineではGet-Content、ファイルを一度に1行ずつ処理します。結果を蓄積していないか、内部的に蓄積するコマンドレット(Sort-ObjectやGroup-Objectなど)を使用していない限り、メモリヒットはそれほど悪くないはずです。Foreachオブジェクト(%)は、各行を1つずつ処理する安全な方法です。
キース・ヒル

2
意味をなさない@dwarfsoft。-Endブロックは、すべての処理が完了した後に一度だけ実行されます。使用しようとするとget-content | % -End { }、プロセスブロックを提供していないため、文句を言うことがわかります。したがって、デフォルトで-Endを使用することはできません。デフォルトで-Processを使用する必要があります。そして1..5 | % -process { } -end { 'q' }、エンドブロックが1回だけ発生することを確認してみてください。gc | % { $_ }スクリプトブロックのデフォルトが-Endである場合、通常は機能しません...
TessellatingHeckler

回答:


91

マルチギガバイトのテキストファイルを実際に操作する場合は、PowerShellを使用しないでください。それを読む方法を見つけたとしても、大量の行の処理がPowerShellでとにかく遅くなり、これを回避することはできません。単純なループでさえ、1,000万回の反復(たとえば、実際には非常に現実的)の場合、コストが高くなります。

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

更新:それでも怖くない場合は、.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()
}

アップデート2

おそらくより良い/より短いコードについてのコメントがあります。元のコードに問題forはなく、疑似コードではありません。しかし、読み取りループのより短い(最短?)バリアントは

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

3
参考までに、PowerShell V3でスクリプトをコンパイルすると、状況が少し改善されます。「実際のジョブ」ループは、コンソールで入力されたV2の117秒からV3の62秒になりました。ループをスクリプトに入れ、V3でスクリプトの実行を測定すると、34秒になりました。
キース・ヒル

3つのテストすべてをスクリプトに入れて、次の結果を得ました。V3ベータ:20/27/83秒。V2:14/21/101。私の実験では、V3はテスト3でより高速に見えますが、最初の2つではかなり低速です。まあ、それはベータ版です。うまくいけば、RTMでパフォーマンスが改善されるでしょう。
ローマンクズミン

なぜ人々はそのようなループでブレークを使用することを主張するのですか?それを必要とせず、forループを次のように置き換えるなどの読みやすいループを使用しないのはなぜですかdo { $line = $reader.ReadLine(); $line } while ($line -neq $null)
BeowulfNode42

1
等しくないために-neになるはずのエラー。この特定のdo..whileループには、ファイルの最後のnullが処理される(この場合は出力)という問題があります。あまりにもあなたが持っている可能性があることを回避するにはfor ( $line = $reader.ReadLine(); $line -ne $null; $line = $reader.ReadLine() ) { $line }
BeowulfNode42

4
@ BeowulfNode42、これをさらに短くできます:while($null -ne ($line = $read.ReadLine())) {$line}。しかし、トピックは実際にはそのようなことについてではありません。
ローマクズミン

51

System.IO.File.ReadLines()このシナリオに最適です。ファイルのすべての行を返しますが、行全体をすぐに繰り返し始めることができるため、内容全体をメモリに保存する必要はありません。

.NET 4.0以降が必要です。

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

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


6
メモが必要です:.NET Framework-サポート対象:4.5、4。したがって、一部のマシンのV2またはV1では機能しない可能性があります。
ローマクズミン2012年

これにより、System.IO.Fileが存在しないというエラーが発生しましたが、上記のRomanのコード
Canyon

これはまさに私が必要としたものであり、既存のpowershellスクリプトに直接ドロップするのは簡単でした。
user1751825

5

そのままPowerShellを使用する場合は、以下のコードを確認してください。

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

16
これは、OPがGet-Content大きなファイルでは非常に遅いため、OPが削除したかったものです。
ローマクズミン2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.