使用PowerShell对非常大的文本文件进行排序

10

我有标准的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分钟。

我尝试了上面的命令,确实花费了很长时间(大约在460MB上花费了10分钟),而且最终结果并不是我所需要的,另外目标文件(sorted.txt)的大小是源文件的两倍。 - Predrag Vasić
大小的差异可能是由于使用不同的编码方式导致的。用类似于| Set-Content sorted.txt的内容替换> sorted.txt可能会起作用,否则您可以尝试| Out-File sorted.txt -Encoding <your choice> - notjustme
我可以问一下(也许我应该首先这样问),如果日期和时间本身不是唯一的,你到底要对什么进行排序和查找唯一行?我最近没有检查过任何Apache日志(几年前),所以我不知道问题出在哪里... - notjustme
你应该分别测量阅读和排序时间。我猜阅读部分较慢。 - n0rd
我刚刚使用 Measure-Command 测试了一下 gc file.txt | sort | get-uniquegc file.txt | sort -Unique 两个版本的性能,结果发现第二个版本更快(我猜测是因为去除了额外管道的开销)。 - E.Z. Hart
显示剩余5条评论
7个回答

9

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);

使用一个大小为40MB,由100,000个唯一行重复16次组成的文本文件(共1.6百万行),在我的机器上运行此脚本将产生以下输出:

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秒钟,但我仍然感到惊讶。


这是一个显著的性能改进,但是目标文件比源文件明显要小。重复的条目似乎被删除了,这不是我想要的结果。我只需要它按字母顺序排序行;如果有多个相同的行,请保留它们所有。谢谢帮忙! - Predrag Vasić
3
你的示例代码中有一个名为 Get-Unique 的函数,它可以去除重复项。如果不需要这个函数,直接对 List 进行排序即可,不需要在此处使用 HashSet - n0rd
也许如果整个文件一次性读取而不是逐行读取,文件读取会有所改善。 - Jakub P
@JakubP,我非常怀疑。必须在某个时刻进行换行,无论是从磁盘读取还是从内存读取,我预计缓冲将使这两者之间的差异可以忽略不计。 - n0rd
这不是PowerShell,而是C#。 - mark

3
我开始讨厌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 处理速度非常快,这让我想知道Microsoft在设置方面在想什么。

虽然这种方法不是所有情况下都可行,但如果您有一台Linux机器,您可以将500兆字节的数据复制到该机器上,在几分钟内完成排序和去重,并将其复制回来。

0
似乎在PowerShell中没有一个很好的方法来做到这一点,包括[IO.File]::ReadLines(),但使用本地的Windows sort.exe或GNU sort.exe,在cmd.exe中可以在大约5分钟内用大约1 GB的RAM对3000万个随机数进行排序。GNU sort会自动将事物分成临时文件以节省RAM。两个命令都有选项来从某个字符列开始进行排序。Gnu sort可以合并排序后的文件。请参见external sorting
3000万行测试文件:
& { foreach ($i in 1..300kb) { get-random } } | set-content file.txt

然后在命令提示符中:

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

或者在 cmd.exe 中使用 Windows 的 sort.exe:

sort.exe < file8.txt > filesorted.txt

0

使用以下函数:

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
}

0
"

Get-Content(获取内容)

可能比你想象的更快。除了上面的解决方案,还可以检查此代码片段:"
foreach ($block in (get-content $file -ReadCount 100)) {
    foreach ($line in $block){[void] $hs.Add($line)}
}

当然,它不像StreamReader或[System.IO.File] :: OpenText那样快,但另一方面,在块中使用它时不会对文件系统产生任何峰值负载。 - Carsten

0

这可能是一个内存问题。由于您将整个文件加载到内存中进行排序(并添加管道到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使用非常低效的算法)时才有意义。


如果所有数据都适合内存(即没有溢出到交换空间),那么排序一个大块并不比排序几个较小的块慢。 - n0rd
@n0rd - 这将取决于文件大小、机器可用内存、Sort-Object 使用的算法以及数据事先排序的接近程度。 - E.Z. Hart
在相同的输入数据上,整个集合的排序速度永远不会比使用相同算法对块进行排序然后合并要慢。对于外部排序(当所有数据无法放入内存时),是的,您必须拆分、排序和合并。否则,这样做没有任何好处。 - n0rd
修订:对于任何体面的(O(nlogn)时间复杂度)排序算法都是正确的(否则它们可以通过拆分、排序和合并来加速),但对于任何更糟糕的算法则不是。我非常确定Sort-Object使用了一些体面的算法。然而,通过管道推送数据可能会对执行时间产生很大影响。 - n0rd
我会更新我的答案,以更清楚地解释它所修复的(潜在)问题。 - E.Z. Hart

0
如果日志的每一行都以时间戳为前缀,并且日志消息不包含嵌入式换行符(这将需要特殊处理),我认为在排序之前将时间戳从 [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-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';

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接