如何使用 powershell 重新排序 CSV 列

Posted

技术标签:

【中文标题】如何使用 powershell 重新排序 CSV 列【英文标题】:How to use powershell to reorder CSV columns 【发布时间】:2011-09-17 05:24:12 【问题描述】:

输入文件:

column1;column2;column3
data1a;data2a;data3a
data1b;data2b;data3b

目标:输出文件列重新排序

column1;column3;column2
...

更新的问题: 使用powershell解决这个问题的好方法是什么。 我知道 CSV 相关 cmdlet 的存在,但这些有局限性。 请注意,记录的顺序不需要更改,因此不需要将整个输入/输出文件加载到内存中。

【问题讨论】:

到底是什么问题?如何重新排序列或如何使其在数百万条记录的情况下工作? (我有一些经验,直截了当的解决方案效果不佳)。 没错,如果文件大小是一个问题,我只是在解决列的重新排序问题,那么解决方案肯定会变得更加复杂。 对于数百万行,我会将文件加载到数据库中并按所需顺序导出行。 MSSQL 有相当复杂的导入/导出工具,但几乎任何数据库都可以。 测量,总是......你花在创建解决方案上的时间越少,你享受生活的时间就越多;)(请参阅我的答案) 【参考方案1】:

这是适用于数百万条记录的解决方案(假设您的数据没有嵌入';')

$reader = [System.IO.File]::OpenText('data1.csv')
$writer = New-Object System.IO.StreamWriter 'data2.csv'
for(;;) 
    $line = $reader.ReadLine()
    if ($null -eq $line) 
        break
    
    $data = $line.Split(";")
    $writer.WriteLine('0;1;2', $data[0], $data[2], $data[1])

$reader.Close()
$writer.Close()

【讨论】:

这看起来是一个更好的解决方案。 如果我们将 2 行 $data = ... $writer.WriteLine ... 替换为更隐秘的 $writer.WriteLine('0;2;1', $line.Split(";")) ,它可以更快一点(~8%) 我实际上会说这更具可读性。它使索引更靠近,使位移更容易掌握。 我同意这是对这组数据的快速解决方案。但是,它可能无法处理字段分隔符出现在字段数据中的引用字段。 这不适用于文本限定符。如果字段分隔符或 CRLF 出现在限定文本中,则解决方案中断。【参考方案2】:
Import-CSV C:\Path\To\Original.csv | Select-Object Column1, Column3, Column2 | Export-CSV C:\Path\To\Newfile.csv

【讨论】:

看我对作者的问题。 查看我对作者的问题的回复:) 为了清楚起见,我意识到我的答案实际上只是 OP 完整答案的一半。不过我打算把它留在这里,以便稍后徘徊在这个主题上并正在处理较小文件的任何人都会对他们的问题有一个更简单的答案。 恕我直言很好的答案(因为谷歌主要看到标题),将+1这个和其他的:) 幸运的是,Import-CSV 在管道传输时不会将整个文件读入内存,而且这个解决方案比我的 Powershell 版本更快(32 分 44 秒)。不过,Export-CSV 可能需要被告知不要将每个字段都括在引号中。 :)【参考方案3】:

编辑:下面的基准信息。

我不会使用与 Powershell csv 相关的 cmdlet。我会使用System.IO.StreamReaderMicrosoft.VisualBasic.FileIO.TextFieldParser 逐行读取文件以避免将整个内容加载到内存中,我会使用System.IO.StreamWriter 将其写回。 TextFieldParser 在内部使用 StreamReader,但会处理分隔字段,因此您不必这样做,如果 CSV 格式不简单(例如,引号字段中有分隔符),它会非常有用。

我也不会在 Powershell 中执行此操作,而是在 .NET 应用程序中执行此操作,因为即使它们使用相同的对象,它也会比 Powershell 脚本快得多。

这是一个简单版本的 C#,假设没有引用字段和 ASCII 编码:

static void Main()
    string source = @"D:\test.csv";
    string dest = @"D:\test2.csv";

    using ( var reader = new Microsoft.VisualBasic.FileIO.TextFieldParser( source, Encoding.ASCII ) ) 
        using ( var writer = new System.IO.StreamWriter( dest, false, Encoding.ASCII ) ) 
            reader.SetDelimiters( ";" );
            while ( !reader.EndOfData ) 
                var fields = reader.ReadFields();
                swap(fields, 1, 2);
                writer.WriteLine( string.Join( ";", fields ) );
            
        
    


static void swap( string[] arr, int a, int b ) 
    string t = arr[ a ];
    arr[ a ] = arr[ b ];
    arr[ b ] = t;

这是 Powershell 版本:

[void][reflection.assembly]::loadwithpartialname("Microsoft.VisualBasic")

$source = 'D:\test.csv'
$dest = 'D:\test2.csv'

$reader = new-object Microsoft.VisualBasic.FileIO.TextFieldParser $source
$writer = new-object System.IO.StreamWriter $dest

function swap($f,$a,$b) $t = $f[$a]; $f[$a] = $f[$b]; $f[$b] = $t

$reader.SetDelimiters(';')
while ( !$reader.EndOfData ) 
    $fields = $reader.ReadFields()
    swap $fields 1 2
    $writer.WriteLine([string]::join(';', $fields))


$reader.close()
$writer.close()

我将这两个文件与一个包含 10,000,000 行的 3 列 csv 文件进行了基准测试。 C# 版本耗时 171.132 秒(不到 3 分钟)。 Powershell 版本耗时 2,364.995 秒(39 分 25 秒)。

编辑:为什么我的花了这么长时间。

交换功能是我的 Powershell 版本中的一个巨大瓶颈。将其替换为 '0;1;2' 风格的输出,如 Roman Kuzmin 的回答,将其缩短到不到 9 分钟。替换 TextFieldParser 将剩余时间减少一半以上,不到 4 分钟。

但是,Roman Kuzmin 的答案的 .NET 控制台应用程序版本需要 20 秒。

【讨论】:

【参考方案4】:

很高兴人们提供了基于纯 .NET 的解决方案。但是,如果可能的话,我会争取简单。这就是为什么我赞成你们所有人;)

为什么?我尝试生成 1.000.000 条记录并将其存储在 CSV 中,然后重新排序列。 在我的情况下,生成 csv 比重新排序要求更高。看看结果。

重新排序列只需要 1.8 分钟。对我来说,这是相当不错的结果。 我可以吗? -> 是的,我不需要尝试找到更快的解决方案,这已经足够了 -> 节省了我的时间来做一些其他有趣的事情 ;)

# generate some csv; objects have several properties
measure-command  
    1..1mb | 
    %  
        $date = get-date
        New-Object PsObject -Property @
            Column1=$date
            Column2=$_
            Column3=$date.Ticks/$_ 
            Hour = $date.Hour
            Minute = $date.Minute
            Second = $date.Second
            ReadableTime = $date.ToLongTimeString()
            ReadableDate = $date.ToLongDateString()
         | 
    Export-Csv d:\temp\exported.csv 


TotalMinutes      : 6,100025295

# reorder the columns
measure-command  
    Import-Csv d:\temp\exported.csv | 
        Select ReadableTime, ReadableDate, Hour, Minute, Second, Column1, Column2, Column3 | 
        Export-Csv d:\temp\exported2.csv 


TotalMinutes      : 2,33151559833333

【讨论】:

【参考方案5】:

我会这样做:

$new_csv = new-object system.collections.ArrayList
get-content mycsv.csv |% 
$new_csv.add((($_ -split ";")[0,2,1]) -join ";") > $nul

$new_csv | out-file myreordered.csv

【讨论】:

由于+= 方法,一百万条记录将需要数小时。试试:) 这是假设它没有耗尽内存,因为get-content 将首先将整个内容读入内存。 import-csv 也是如此。不同之处在于 import-csv 会将其作为 [object[]] 存储在内存中,而这会将其存储为 [string[]]。 [string[]] 应该有更少的内存需求。 Roman 关于 += 是一个严重的性能问题是正确的。更改为 arraylist 类型并改用 .add 方法似乎要快得多。

以上是关于如何使用 powershell 重新排序 CSV 列的主要内容,如果未能解决你的问题,请参考以下文章

PowerShell Import-CSV“过滤掉空行”Export-CSV

如何使用 Powershell 从 csv 文件中仅读取一列

PowerShell:如何计算 csv 文件中的行数?

如何对漏斗图中的条形重新排序

如何使用 PowerShell 在 csv 文件中添加字符串并创建新列

如何从Powershell打印成csv?