在 PowerShell 中对非常大的文本文件进行排序

Sort very large text file in PowerShell

我有标准的 Apache 日志文件,大小在 500Mb 到 2GB 之间。我需要对其中的行进行排序(每行以日期 yyyy-MM-dd hh:mm:ss 开头,因此无需进行排序处理。

想到的最简单最明显的事情是

 Get-Content unsorted.txt | sort | get-unique > sorted.txt

我猜测(没有尝试过)使用 Get-Content 执行此操作将永远占用我的 1GB 文件。我不太了解 System.IO.StreamReader,但我很好奇是否可以使用它来组合有效的解决方案?

感谢任何可能有更有效想法的人。

[编辑]

我后来试了这个,花了很长时间; 400MB 大约需要 10 分钟。

(根据 n0rd 的评论编辑得更清楚)

这可能是内存问题。由于您将整个文件加载到内存中以对其进行排序(并将管道的开销添加到 Sort-Object 并将管道的开销添加到 Get-Unique),因此您可能会达到机器的内存限制并强制执行分页到磁盘,这会大大降低速度。您可能会考虑的一件事是在对日志进行排序之前将它们分开,然后将它们重新拼接在一起。

这可能与您的格式不完全匹配,但如果我有一个大型日志文件,例如 2012 年 8 月 16 日,跨越几个小时,我可以将其拆分为每个文件的不同文件小时使用这样的东西:

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 format specifier 用于 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-GridViewFormat-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';

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 万行的 40 MB 文件(由重复 16 次的 10 万行组成),此脚本在我的机器上产生以下输出:

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 将文件逐行读取到 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 秒,但我仍然感到惊讶。

我越来越讨厌 windows powershell 的这一部分,它占用了这些较大文件的内存。一个技巧是阅读 [System.IO.File]::ReadLines('file.txt') | sort -u | out-file file2.txt -encoding ascii

另一个技巧,认真地说就是使用 linux。

cat file.txt | sort -u > output.txt

Linux 在这方面快得离谱,这让我想知道微软到底在想什么这个设置。

这可能并非在所有情况下都可行,我理解,但如果你有一台 linux 机器,你可以将 500 兆复制到它,对其进行排序和唯一化,然后将其复制回几分钟。

"Get-Content" 可能比您想象的要快。除了上述解决方案外,还检查此代码片段:

foreach ($block in (get-content $file -ReadCount 100)) {
    foreach ($line in $block){[void] $hs.Add($line)}
}

在 powershell 中似乎没有很好的方法,包括 [IO.File]::ReadLines(),但使用本机 windows sort.exe 或 gnu sort.exe ,或者在 cmd.exe 之内,大约 1 GB 的内存可以在大约 5 分钟内对 3000 万个随机数进行排序。 gnu sort 自动将内容分解为临时文件以保存 ram。这两个命令都有在特定字符列开始排序的选项。 Gnu sort 可以合并排序的文件。参见 external sorting

3000万行测试文件:

& { foreach ($i in 1..300kb) { get-random } } | set-content file.txt

然后在cmd中:

copy file.txt+file.txt file2.txt
copy file2.txt+file2.txt file3.txt
copy file3.txt+file3.txt file4.txt
copy file4.txt+file4.txt file5.txt
copy file5.txt+file5.txt file6.txt
copy file6.txt+file6.txt file7.txt
copy file7.txt+file7.txt file8.txt

使用来自 http://gnuwin32.sourceforge.net/packages/coreutils.htm 的 gnu sort.exe。不要忘记依赖 dll 的 -- libiconv2.dll & libintl3.dll。 cmd.exe:

以内
.\sort.exe < file8.txt > filesorted.txt

或windows sort.exe cmd.exe:

sort.exe < file8.txt > filesorted.txt

具有以下功能:

PS> PowerSort -SrcFile C:\windows\win.ini
function PowerSort {
    param(
        [string]$SrcFile = "",
        [string]$DstFile = "",
        [switch]$Force
    )

    if ($SrcFile -eq "") {
        write-host "USAGE: PowerSort -SrcFile (srcfile)  [-DstFile (dstfile)] [-Force]"
        return 0;
    }
    else {
        $SrcFileFullPath = Resolve-Path $SrcFile -ErrorAction SilentlyContinue -ErrorVariable _frperror        
        if (-not($SrcFileFullPath)) {
            throw "Source file not found: $SrcFile";
        }
    }

    [Collections.Generic.List[string]]$lines = [System.IO.File]::ReadAllLines($SrcFileFullPath)
    
    $lines.Sort();

    # Write Sorted File to Pipe
    if ($DstFile -eq "") {
        foreach ($line in $lines) {
            write-output $line
        }           
    }
    
    # Write Sorted File to File
    else {
        $pipe_enable = 0;
        $DstFileFullPath = Resolve-Path $DstFile -ErrorAction SilentlyContinue -ErrorVariable ev

        # Destination File doesn't exist        
        if (-not($DstFileFullPath)) {
           $DstFileFullPath = $ev[0].TargetObject       
        }
        
        # Destination Exists and -force not specified.
        elseif (-not $Force) {
            throw "Destination file already exists: ${DstFile}  (using -Force Flag to overwrite)"           
        }       
        
        write-host "Writing-File: $DstFile"
        [System.IO.File]::WriteAllLines($DstFileFullPath, $lines)
    }
    return
}