在Powershell中,将大型文本文件按记录类型拆分的最有效方法是什么?

10
我正在使用PowerShell进行一些ETL工作,读取压缩的文本文件并根据每行的前三个字符将其拆分出来。
如果我只是过滤输入文件,我可以将过滤后的流传递到Out-File并完成操作。但我需要将输出重定向到多个目标,据我所知,这不能通过简单的管道实现。我已经在使用.NET streamreader读取压缩的输入文件,我想知道是否需要使用streamwriter来编写输出文件。
天真的版本看起来像这样:
while (!$reader.EndOfFile) {
  $line = $reader.ReadLine();
  switch ($line.substring(0,3) {
    "001" {Add-Content "output001.txt" $line}
    "002" {Add-Content "output002.txt" $line}
    "003" {Add-Content "output003.txt" $line}
    }
  }

那看起来很不好:每一行都要找、打开、写入和关闭一个文件。输入文件非常大,超过500MB。
有没有使用Powershell结构处理这个问题的惯用方法,或者我应该转向.NET streamwriter?
是否可以使用(New-Item "path" -type "file")对象的方法来处理这个问题?
编辑内容如下:
我正在使用DotNetZip库将ZIP文件读取为流;因此使用streamreader而不是Get-Content/gc。示例代码:
[System.Reflection.Assembly]::LoadFrom("\Path\To\Ionic.Zip.dll") 
$zipfile = [Ionic.Zip.ZipFile]::Read("\Path\To\File.zip")

foreach ($entry in $zipfile) {
  $reader = new-object system.io.streamreader $entry.OpenReader();
  while (!$reader.EndOfFile) {
    $line = $reader.ReadLine();
    #do something here
  }
}

我应该释放 $zipfile 和 $reader,但这是另一个问题!

2个回答

15

读取

关于读取文件和解析,我会选择使用switch语句:

switch -file c:\temp\stackoverflow.testfile2.txt -regex {
  "^001" {Add-Content c:\temp\stackoverflow.testfile.001.txt $_}
  "^002" {Add-Content c:\temp\stackoverflow.testfile.002.txt $_}
  "^003" {Add-Content c:\temp\stackoverflow.testfile.003.txt $_}
}

我认为这是更好的方法,因为:

  • 它支持正则表达式,您不必使用子字符串(可能很昂贵)
  • -file参数非常方便;)

写入

至于编写输出,我将尝试使用streamwriter,但是如果Add-Content的性能对您来说足够好,我会坚持使用它。

补充: Keith建议使用>>运算符,但是它似乎非常慢。此外,它以Unicode编写输出,这将使文件大小翻倍。

看看我的测试:

[1]: (measure-command {
>>     gc c:\temp\stackoverflow.testfile2.txt  | %{$c = $_; switch ($_.Substring(0,3)) {
>>             '001'{$c >> c:\temp\stackoverflow.testfile.001.txt} `
>>             '002'{$c >> c:\temp\stackoverflow.testfile.002.txt} `
>>             '003'{$c >> c:\temp\stackoverflow.testfile.003.txt}}}
>> }).TotalSeconds
>>
159,1585874
[2]: (measure-command {
>>     gc c:\temp\stackoverflow.testfile2.txt  | %{$c = $_; switch ($_.Substring(0,3)) {
>>             '001'{$c | Add-content c:\temp\stackoverflow.testfile.001.txt} `
>>             '002'{$c | Add-content c:\temp\stackoverflow.testfile.002.txt} `
>>             '003'{$c | Add-content c:\temp\stackoverflow.testfile.003.txt}}}
>> }).TotalSeconds
>>
9,2696923

差别非常大。

仅作比较:

[3]: (measure-command {
>>     $reader = new-object io.streamreader c:\temp\stackoverflow.testfile2.txt
>>     while (!$reader.EndOfStream) {
>>         $line = $reader.ReadLine();
>>         switch ($line.substring(0,3)) {
>>             "001" {Add-Content c:\temp\stackoverflow.testfile.001.txt $line}
>>             "002" {Add-Content c:\temp\stackoverflow.testfile.002.txt $line}
>>             "003" {Add-Content c:\temp\stackoverflow.testfile.003.txt $line}
>>             }
>>         }
>>     $reader.close()
>> }).TotalSeconds
>>
8,2454369
[4]: (measure-command {
>>     switch -file c:\temp\stackoverflow.testfile2.txt -regex {
>>         "^001" {Add-Content c:\temp\stackoverflow.testfile.001.txt $_}
>>         "^002" {Add-Content c:\temp\stackoverflow.testfile.002.txt $_}
>>         "^003" {Add-Content c:\temp\stackoverflow.testfile.003.txt $_}
>>     }
>> }).TotalSeconds
8,6755565

补充说明:我对写入性能感到好奇,结果有点惊讶。

[8]: (measure-command {
>>     $sw1 = new-object io.streamwriter c:\temp\stackoverflow.testfile.001.txt3b
>>     $sw2 = new-object io.streamwriter c:\temp\stackoverflow.testfile.002.txt3b
>>     $sw3 = new-object io.streamwriter c:\temp\stackoverflow.testfile.003.txt3b
>>     switch -file c:\temp\stackoverflow.testfile2.txt -regex {
>>         "^001" {$sw1.WriteLine($_)}
>>         "^002" {$sw2.WriteLine($_)}
>>         "^003" {$sw3.WriteLine($_)}
>>     }
>>     $sw1.Close()
>>     $sw2.Close()
>>     $sw3.Close()
>>
>> }).TotalSeconds
>>
0,1062315

它比原方法快80倍。 现在你需要决定 - 如果速度很重要,使用StreamWriter。如果代码清晰度很重要,使用Add-Content


Substring vs. Regex

根据Keith的说法,Substring快20%。但这取决于具体情况。然而,在我的情况下,结果如下:

[102]: (measure-command {
>>     gc c:\temp\stackoverflow.testfile2.txt  | %{$c = $_; switch ($_.Substring(0,3)) {
>>             '001'{$c | Add-content c:\temp\stackoverflow.testfile.001.s.txt} `
>>             '002'{$c | Add-content c:\temp\stackoverflow.testfile.002.s.txt} `
>>             '003'{$c | Add-content c:\temp\stackoverflow.testfile.003.s.txt}}}
>> }).TotalSeconds
>>
9,0654496
[103]: (measure-command {
>>     gc c:\temp\stackoverflow.testfile2.txt  | %{$c = $_; switch -regex ($_) {
>>             '^001'{$c | Add-content c:\temp\stackoverflow.testfile.001.r.txt} `
>>             '^002'{$c | Add-content c:\temp\stackoverflow.testfile.002.r.txt} `
>>             '^003'{$c | Add-content c:\temp\stackoverflow.testfile.003.r.txt}}}
>> }).TotalSeconds
>>
9,2563681
所以这个差别并不重要,对我来说,正则表达式更易读。

实际上,子字符串比较快,速度提升了约20%。 - Keith Hill
Add-Content与>>的速度相比要快一些,使用Out-File -enc ascii似乎在我的测试中与Add-Content相当。有趣的是,使用streamwriter会更快。 - Keith Hill
是的,我也很惊讶。我添加了一些关于子字符串/正则表达式的测量。如果您想比较 StreamWriter 的速度,这是生成测试文件的代码:1..5000 | % { $n = Get-Random -Min 1 -Max 4; $x=1..(Get-Random -Min 20 -Max 150) | % { ([char](Get-Random -Min 65 -Max 120)) }; $x = $x -join ""; '{0:000} {1}' -f $n,$x } | Add-Content C:\temp\stackoverflow.testfile.txt(现在行数为5000)。 - stej
看起来对我来说是使用StreamWriter。谢谢,我是PowerShell的新手,所有特定于我的任务的示例都非常有帮助。我不能使用switch -file结构,但知道在处理未压缩文件时它是可用的很好。 - Peter Radocchia

3
考虑到输入文件的大小,您肯定希望逐行处理。我认为重新打开/关闭输出文件不会对性能产生太大影响。这确实使得使用管道实现成为可能,即使作为一行代码也并不完全不同于您的实现。在此处进行了包装以消除水平滚动条:
gc foo.log | %{switch ($_.Substring(0,3)) {
    '001'{$input | out-file output001.txt -enc ascii -append} `
    '002'{$input | out-file output002.txt -enc ascii -append} `
    '003'{$input | out-file output003.txt -enc ascii -append}}}

Keith,在 $_ >> output001.txt 语句中,$_ 变量不是来自 for-each,而是来自 switch - 它仅包含子字符串。 - stej
我只是需要睡一觉。现在已经很晚了,我感到有些疲惫。 :-) - Keith Hill

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