500Mbから2GBのサイズの標準のApacheログファイルがあります。それらの行を並べ替える必要があります(各行は日付yyyy-MM-dd hh:mm:ssで始まるため、並べ替えに処理は必要ありません。
頭に浮かぶ最も単純で最も明白なことは
Get-Content unsorted.txt | sort | get-unique > sorted.txt
Get-Content
を使用してこれを行うと、1GBのファイルで永久に時間がかかると(試していないで)推測しています。 System.IO.StreamReader
を回避する方法はよくわかりませんが、それを使用して効率的なソリューションをまとめることができるかどうか知りたいのですが。
より効率的なアイデアを持っているかもしれない人に感謝します。
[編集]
その後、これを試しましたが、非常に長い時間がかかりました。 400MBで約10分。
Get-Content
は、大きなファイルの読み取りにはひどく効果がありません。 Sort-Object
もそれほど速くはありません。
ベースラインを設定しましょう:
$sw = [System.Diagnostics.Stopwatch]::StartNew();
$c = Get-Content .\log3.txt -Encoding Ascii
$sw.Stop();
Write-Output ("Reading took {0}" -f $sw.Elapsed);
$sw = [System.Diagnostics.Stopwatch]::StartNew();
$s = $c | Sort-Object;
$sw.Stop();
Write-Output ("Sorting took {0}" -f $sw.Elapsed);
$sw = [System.Diagnostics.Stopwatch]::StartNew();
$u = $s | Get-Unique
$sw.Stop();
Write-Output ("uniq took {0}" -f $sw.Elapsed);
$sw = [System.Diagnostics.Stopwatch]::StartNew();
$u | Out-File 'result.txt' -Encoding ascii
$sw.Stop();
Write-Output ("saving took {0}" -f $sw.Elapsed);
160万行の40MBファイル(16回繰り返される100kの一意の行で構成)を使用すると、このスクリプトは私のマシンで次の出力を生成します。
Reading took 00:02:16.5768663
Sorting took 00:02:04.0416976
uniq took 00:01:41.4630661
saving took 00:00:37.1630663
まったく印象的ではありません:小さなファイルを並べ替えるのに6分以上かかります。すべてのステップを大幅に改善できます。 StreamReader
を使用してファイルを1行ずつHashSet
に読み込み、重複を削除してから、データをList
にコピーしてそこで並べ替え、StreamWriter
を使用して結果をダンプします。
$hs = new-object System.Collections.Generic.HashSet[string]
$sw = [System.Diagnostics.Stopwatch]::StartNew();
$reader = [System.IO.File]::OpenText("D:\log3.txt")
try {
while (($line = $reader.ReadLine()) -ne $null)
{
$t = $hs.Add($line)
}
}
finally {
$reader.Close()
}
$sw.Stop();
Write-Output ("read-uniq took {0}" -f $sw.Elapsed);
$sw = [System.Diagnostics.Stopwatch]::StartNew();
$ls = new-object system.collections.generic.List[string] $hs;
$ls.Sort();
$sw.Stop();
Write-Output ("sorting took {0}" -f $sw.Elapsed);
$sw = [System.Diagnostics.Stopwatch]::StartNew();
try
{
$f = New-Object System.IO.StreamWriter "d:\result2.txt";
foreach ($s in $ls)
{
$f.WriteLine($s);
}
}
finally
{
$f.Close();
}
$sw.Stop();
Write-Output ("saving took {0}" -f $sw.Elapsed);
このスクリプトは以下を生成します。
read-uniq took 00:00:32.2225181
sorting took 00:00:00.2378838
saving took 00:00:01.0724802
同じ入力ファイルで、10倍以上高速に実行されます。ディスクからファイルを読み取るのに30秒かかりますが、私はまだ驚いています。
(n0rdのコメントに基づいてより明確になるように編集)
メモリの問題である可能性があります。ファイル全体をメモリにロードして並べ替える(そしてパイプのオーバーヘッドをSort-Objectに追加し、パイプをGet-Uniqueに追加する)ため、マシンのメモリ制限に達して強制的に実行されている可能性があります。ディスクにページングすると、処理速度が大幅に低下します。検討する可能性のあることの1つは、ログを並べ替える前に分割してから、それらをつなぎ合わせることです。
これはおそらくあなたのフォーマットと正確には一致しませんが、たとえば2012年8月16日の数時間にわたる大きなログファイルがある場合は、次のようなものを使用して、1時間ごとに異なるファイルに分割できます。 :
for($i=0; $i -le 23; $i++){ Get-Content .\u_ex120816.log | ? { $_ -match "^2012-08-16 $i`:" } | Set-Content -Path "$i.log" }
これは、その日の各時間の正規表現を作成し、一致するすべてのログエントリを時間で名前が付けられた小さなログファイル(16.log、17.logなど)にダンプします。
次に、はるかに小さいサブセットで一意のエントリを並べ替えて取得するプロセスを実行できます。これにより、実行速度が大幅に向上します。
for($i=0; $i -le 23; $i++){ Get-Content "$i.log" | sort | get-unique > "$isorted.txt" }
そして、それらをつなぎ合わせることができます。
ログの頻度によっては、ログを日単位または分単位で分割する方が理にかなっている場合があります。主なことは、ソートのためにそれらをより管理しやすいチャンクに入れることです。
繰り返しますが、これは、マシンのメモリ制限に達している場合(または、Sort-Objectが非常に非効率的なアルゴリズムを使用している場合)にのみ意味があります。
ログの各行の前にタイムスタンプが付いていて、ログメッセージに改行が埋め込まれていない場合(特別な処理が必要になります)、タイムスタンプを[String]
から変換するのにかかるメモリと実行時間は少なくなると思います。ソートする前に[DateTime]
に。以下では、各ログエントリがyyyy-MM-dd HH:mm:ss: <Message>
の形式であると想定しています( HH
形式指定子 は24時間制に使用されることに注意してください)。
Get-Content unsorted.txt
| ForEach-Object {
# Ignore empty lines; can substitute with [String]::IsNullOrWhitespace($_) on PowerShell 3.0 and above
if (-not [String]::IsNullOrEmpty($_))
{
# Split into at most two fields, even if the message itself contains ': '
[String[]] $fields = $_ -split ': ', 2;
return New-Object -TypeName 'PSObject' -Property @{
Timestamp = [DateTime] $fields[0];
Message = $fields[1];
};
}
} | Sort-Object -Property 'Timestamp', 'Message';
インタラクティブな表示目的で入力ファイルを処理している場合は、上記をOut-GridView
またはFormat-Table
にパイプして、結果を表示できます。ソートされた結果を保存する必要がある場合は、上記を以下にパイプできます。
| ForEach-Object {
# Reconstruct the log entry format of the input file
return '{0:yyyy-MM-dd HH:mm:ss}: {1}' -f $_.Timestamp, $_.Message;
} `
| Out-File -Encoding 'UTF8' -FilePath 'sorted.txt';
私はWindowsPowerShellのこの部分を嫌うようになりました。これは、これらの大きなファイルのメモリを大量に消費します。 1つのトリックは、行[System.IO.File]::ReadLines('file.txt') | sort -u | out-file file2.txt -encoding ascii
を読み取ることです。
もう1つのトリックは、真剣にLinuxを使用することです。
cat file.txt | sort -u > output.txt
Linuxはこれでめちゃくちゃ速いので、Microsoftがこの設定で一体何を考えているのか不思議に思う。
すべての場合に実行可能であるとは限りません。理解していますが、Linuxマシンを使用している場合は、500メガバイトをコピーして並べ替えて一意にし、数分以内にコピーして戻すことができます。