如何在PowerShell中逐行流式处理文件

100

我正在处理一些数GB大小的文本文件,想用 PowerShell 进行流式处理。它是简单的内容,只需解析每一行并提取一些数据,然后将其存储到数据库中。

不幸的是,在管道的这个阶段,get-content | %{ whatever($_) } 似乎会将整个行集保留在内存中。这也非常慢,需要很长时间才能真正读入所有内容。

我的问题有两个部分:

  1. 如何逐行流式处理而不将整个内容缓冲在内存中?我不想为此目的使用几GB的RAM。
  2. 如何让它运行更快?PowerShell 迭代 get-content 似乎比C#脚本慢100倍。

我希望这里有些愚蠢的地方,例如错过了 -LineBufferSize 参数之类的东西...


10
为了加快get-content的速度,将-ReadCount设置为512。请注意,在这个时候,在Foreach中的$_将是一个字符串数组。 - Keith Hill
1
不过,我会选择Roman的建议,使用.NET读取器 - 速度更快。 - Keith Hill
出于好奇,如果我不在意速度,只关心内存会发生什么?最有可能我会选择使用.NET reader建议,但我也想知道如何避免将整个管道缓冲到内存中。 - scobi
9
为了最小化缓冲,请避免将Get-Content的结果分配给一个变量,因为这会将整个文件加载到内存中。默认情况下,在管道中,Get-Content一次处理一个文件行。只要你不积累结果或使用一个内部累积的 cmdlet(如 Sort-Object 和 Group-Object),那么内存占用就不会太大。使用 Foreach-Object(%)是一种安全的方法,可以逐行处理每一行。 - Keith Hill
1
忘掉缓冲,这更多地与Foreach-Object /%块默认使用-End有关,如果没有给出属性,请尝试get-content |%-Process {whatever($ _)},如果您希望它在每行输入时执行。 - dwarfsoft
3
@dwarfsoft 这没有任何意义。-End块仅在所有处理完成后运行一次。如果您尝试使用get-content | % -End { },则会发现它会抱怨因为您没有提供处理块。因此,它不能默认使用-End,必须默认使用-Process。请尝试使用1..5 | % -process { } -end { 'q' },并查看结束块只发生一次,通常的gc | % { $_ }如果脚本块默认为-End,则不起作用... - TessellatingHeckler
4个回答

100

如果你真的要处理多GB的文本文件,就不要使用PowerShell。即使你找到了更快的读取方式,大量行数的处理在PowerShell中仍然很慢,你无法避免这一点。即使是简单的循环也很耗费资源,在10百万次迭代(在你的情况下相当现实)时:

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

4
FYI,PowerShell V3中的脚本编译情况有所改善。在控制台上键入的“真正任务”循环时间从V2的117秒减少到了V3的62秒。当我将循环放入脚本中并测量在V3上的脚本执行时间时,它降至34秒。 - Keith Hill
2
哎呀,应该是“-ne”表示不等于。那个特定的do..while循环有一个问题,就是文件末尾的null会被处理(在这种情况下输出)。为了解决这个问题,你也可以这样写:for ( $line = $reader.ReadLine(); $line -ne $null; $line = $reader.ReadLine() ) { $line } - BeowulfNode42
5
@BeowulfNode42,我们可以把这个代码缩短为:while($null -ne ($line = $read.ReadLine())) {$line}。但是这个话题并不是关于这些事情的。 - Roman Kuzmin
1
@RomanKuzmin 您对while循环片段进行的评论值得点赞,它易于理解,并且会成为不错的答案。然而,您实际上使用的for(;;)语法让我感到困惑,这是伪代码还是有效的PowerShell语法呢?如果您能多加说明,那就太感谢了。 - T_D
1
for() 意味着一个无限循环。 - Roman Kuzmin
显示剩余9条评论

53

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


7
需要说明的是:.NET Framework - 支持版本:4.5、4。因此,在某些机器上,这可能无法在V2或V1中运行。 - Roman Kuzmin
这给了我一个“System.IO.File不存在”的错误,但是Roman上面的代码对我起作用了。 - Kellen Stuart
这正是我所需要的,而且很容易直接嵌入到现有的PowerShell脚本中。 - user1751825

2
如果你想使用纯PowerShell,请查看下面的代码。
$content = Get-Content C:\Users\You\Documents\test.txt
foreach ($line in $content)
{
    Write-Host $line
}

20
这就是 OP 想要摆脱的,因为 Get-Content 在大文件上速度非常慢。 - Roman Kuzmin

0
对于那些感兴趣的人...
从我的经验来看,我曾经处理过非常大的文件。
以下是一个包含5600万行/记录的39GB XML文件的结果。查找文本是一个10位数的数字。
1) GC -rc 1000 | % -match -> 183 seconds
2) GC -rc 100 | % -match  -> 182 seconds
3) GC -rc 1000 | % -like  -> 840 seconds
4) GC -rc 100 | % -like   -> 840 seconds
5) sls -simple            -> 730 seconds
6) sls                    -> 180 seconds (sls default uses regex, but pattern in my case is passed as literal text)
7) Switch -file -regex    -> 258 seconds
8) IO.File.Readline       -> 250 seconds

1和6是明确的获胜者,但我选择了1

附注:此测试在使用Windows Server 2012 R2服务器和PS 5.1进行。该服务器具有16个虚拟CPU和64 GB内存,但在此测试中仅利用了1个CPU,而PS进程的内存占用量极低,因为上述测试仅使用了非常少的内存。


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