需要根据列值拆分为长(1,000,000+行)CSV文件,并使用其他列中的值重命名

时间:2017-08-14 18:47:45

标签: powershell csv powershell-v2.0

我有以下格式的文件夹CSV文件:

文件-2017-08-14.csv

Ticker  Price   Date
AAPL    1   2017-08-14
AAPL    2   2017-08-14
AAPL    3   2017-08-14
AAPL    4   2017-08-14
MSFT    5   2017-08-14
MSFT    6   2017-08-14
MSFT    7   2017-08-14
GOOG    8   2017-08-14
GOOG    9   2017-08-14
...

文件-2017-08-13.csv

Ticker  Price   Date
AAPL    1   2017-08-13
AAPL    2   2017-08-13
AAPL    3   2017-08-13
AAPL    4   2017-08-13
MSFT    5   2017-08-13
MSFT    6   2017-08-13
MSFT    7   2017-08-13
GOOG    8   2017-08-13
GOOG    9   2017-08-13
...

等等。我需要将其拆分为2X3 = 6个子文件,相应地命名:

/out/AAPL-2017-08-14.csv

Ticker  Price   Date
AAPL    1   2017-08-14
AAPL    2   2017-08-14
AAPL    3   2017-08-14
AAPL    4   2017-08-14

/out/MSFT-2017-08-14.csv

Ticker  Price   Date
MSFT    5   2017-08-14
MSFT    6   2017-08-14
MSFT    7   2017-08-14

/out/GOOG-2017-08-14.csv

Ticker  Price   Date
GOOG    8   2017-08-14
GOOG    9   2017-08-14

/out/AAPL-2017-08-13.csv

Ticker  Price   Date
AAPL    1   2017-08-13
AAPL    2   2017-08-13
AAPL    3   2017-08-13
AAPL    4   2017-08-13

/out/MSFT-2017-08-13.csv

Ticker  Price   Date
MSFT    5   2017-08-13
MSFT    6   2017-08-13
MSFT    7   2017-08-13

/out/GOOG-2017-08-13.csv

Ticker  Price   Date
GOOG    8   2017-08-13
GOOG    9   2017-08-13

我编写了一个脚本,可以通过自动收报机分组并拆分为一个文件,但我无法弄清楚如何正确重命名,我不知道如何循环输入文件夹中的所有文件

Import-Csv file-2017-08-14.csv | Group-Object -Property "Ticker" | Foreach-Object {
    $path = $_.Name + ".csv";
    $_.Group | Export-Csv -Path $path -NoTypeInformation
}

有什么想法吗?

1 个答案:

答案 0 :(得分:3)

方法1

Get-ChildItem -Filter '*.csv' -File -Force `
    | Select-Object -ExpandProperty 'FullName' `
    | Import-Csv -Delimiter "`t" `
    | ForEach-Object -Process {
        $outputFilePath = "out\{0}-{1}.csv" -f $_.Ticker, $_.Date;

        $_ | Export-Csv -Path $outputFilePath -Append -NoTypeInformation;
    };

上面的行执行以下操作:

  • Get-ChildItem从当前目录中检索.csv个文件(不包括子目录)
  • Get-ChildItem的结果将是FileInfo个实例,但我们希望将代表文件路径的string个实例传递给Import-Csv,因此我们使用Select-Object仅传递管道中的FullName属性
  • Import-Csv读取管道中指定的CSV文件,并将每条记录传递到管道
  • ForEach-Object内,$_变量包含每个CSV记录。我们使用TickerDate属性构建适合该记录的输出路径(后者是string而不是DateTime,因此没有{{3}必要的)。然后,我们将记录传递给Export-Csv,并将新行追加到$outputPath的文件中。

虽然这段代码很简单,但每个输入记录打开并附加到每个输出文件一次非常慢,特别是对于一百万行,尽管内存使用量很小,因为在任何给定时间内只有一条记录在内存中。 / p>

方法2

我们可以通过仅在每1,000条记录(或您喜欢的任何值)之后附加到每个输出文件而不是每条记录来改进代码。 HashTable存储每个输出文件的挂起记录,当给定的输出文件超过挂起的记录限制或没有更多的记录要读取时(输入文件的末尾),刷新挂起的记录:< / p>

$pendingRecordsByFilePath = @{};
$maxPendingRecordsPerFilePath = 1000;

Get-ChildItem -Filter '*.csv' -File -Force `
    | Select-Object -ExpandProperty 'FullName' `
    | Import-Csv -Delimiter "`t" `
    | ForEach-Object -Process {
        $outputFilePath = "out\{0}-{1}.csv" -f $_.Ticker, $_.Date;
        $pendingRecords = $pendingRecordsByFilePath[$outputFilePath];

        if ($pendingRecords -eq $null)
        {
            # This is the first time we're encountering this output file; create a new array
            $pendingRecords = @();
        }
        elseif ($pendingRecords.Length -ge $maxPendingRecordsPerFilePath)
        {
            # Flush all pending records for this output file
            $pendingRecords `
                | Export-Csv -Path $outputFilePath -Append -NoTypeInformation;
            $pendingRecords = @();
        }

        $pendingRecords += $_;
        $pendingRecordsByFilePath[$outputFilePath] = $pendingRecords;
    };

# No more input records to be read; flush all pending records for each output file
foreach ($outputFilePath in $pendingRecordsByFilePath.Keys)
{
    $pendingRecordsByFilePath[$outputFilePath] `
        | Export-Csv -Path $outputFilePath -Append -NoTypeInformation;
}

方法3

我们可以通过使用formatting而不是数组来存储待写记录来进一步改进。通过在创建时将列表的容量设置为$maxPendingRecordsPerFileName,这将消除每次添加另一条记录时扩展这些数组的开销。

$pendingRecordsByFilePath = @{};
$maxPendingRecordsPerFilePath = 1000;

Get-ChildItem -Filter '*.csv' -File -Force `
    | Select-Object -ExpandProperty 'FullName' `
    | Import-Csv -Delimiter "`t" `
    | ForEach-Object -Process {
        $outputFilePath = "out\{0}-{1}.csv" -f $_.Ticker, $_.Date;
        $pendingRecords = $pendingRecordsByFilePath[$outputFilePath];

        if ($pendingRecords -eq $null)
        {
            # This is the first time we're encountering this output file; create a new list
            $pendingRecords = New-Object `
                -TypeName 'System.Collections.Generic.List[Object]' `
                -ArgumentList (,$maxPendingRecordsPerFilePath);
            $pendingRecordsByFilePath[$outputFilePath] = $pendingRecords;
        }
        elseif ($pendingRecords.Count -ge $maxPendingRecordsPerFilePath)
        {
            # Flush all pending records for this output file
            $pendingRecords `
                | Export-Csv -Path $outputFilePath -Append -NoTypeInformation;
            $pendingRecords.Clear();
        }
        $pendingRecords.Add($_);
    };

# No more input records to be read; flush all pending records for each output file
foreach ($outputFilePath in $pendingRecordsByFilePath.Keys)
{
    $pendingRecordsByFilePath[$outputFilePath] `
        | Export-Csv -Path $outputFilePath -Append -NoTypeInformation;
}

方法4a

如果我们使用List<object>,我们可以消除缓冲输出记录/行的需要,并不断打开/附加输出文件。我们将为每个输出文件创建一个StreamWriter,并将其保持打开状态,直到我们完成为止。必须使用try / finally块才能确保它们正常关闭。我使用ConvertTo-Csv生成输出,无论我们是否需要它,它总是包含一个标题行,因此有逻辑确保我们只在首次打开文件时写入标题。

$truncateExistingOutputFiles = $true;
$outputFileWritersByPath = @{};

try
{
    Get-ChildItem -Filter '*.csv' -File -Force `
        | Select-Object -ExpandProperty 'FullName' `
        | Import-Csv -Delimiter "`t" `
        | ForEach-Object -Process {
            $outputFilePath = Join-Path -Path (Get-Location) -ChildPath ('out\{0}-{1}.csv' -f $_.Ticker, $_.Date);
            $outputFileWriter = $outputFileWritersByPath[$outputFilePath];
            $outputLines = $_ | ConvertTo-Csv -NoTypeInformation;

            if ($outputFileWriter -eq $null)
            {
                # This is the first time we're encountering this output file; create a new StreamWriter
                $outputFileWriter = New-Object `
                    -TypeName 'System.IO.StreamWriter' `
                    -ArgumentList ($outputFilePath, -not $truncateExistingOutputFiles, [System.Text.Encoding]::ASCII);

                $outputFileWritersByPath[$outputFilePath] = $outputFileWriter;

                # Write the header line
                $outputFileWriter.WriteLine($outputLines[0]);
            }

            # Write the data line
            $outputFileWriter.WriteLine($outputLines[1]);
        };
}
finally
{
    foreach ($writer in $outputFileWritersByPath.Values)
    {
        $writer.Close();
    }
}

令人惊讶的是,这导致性能变化为175%...... 较慢。我会在进一步修改此代码时确定原因。

方法4b

我首先想到的是性能下降是为了重新引入输出缓冲;基本上,结合方法3和4a。同样令人惊讶的是,这只会进一步损害性能。我唯一的猜测是因为StreamWriter自己的角色缓冲它使得我们自己的缓冲不必要。事实上,我测试了{10}的幂{10}的值{10}到100,000,这两个极端的整体性能差异只有5秒。因此,我们自己的缓冲并没有真正帮助任何事情,管理maxPendingRecordsPerFilePath的微小开销在一百万次迭代中增加了30秒的运行时间。所以,让我们废弃缓冲。

方法4c

不要使用List来排除ConvertTo-Csv s(标题行和数据行)的2元素数组,而是让我们自己使用{{1}构建这两行格式化。

方法4d

string的每次迭代中,我们都需要构建输出文件路径,因为它基于输入对象stringForEach-Object属性。我们在构造Ticker时传递一个绝对路径,因为PowerShell有一个不同的&#34;当前目录&#34; (相对路径将基于),而不是典型的.NET应用程序。我们一直在调用Date来构建每次迭代的绝对路径,这是不必要的,因为该路径不会发生变化。因此,我们将调用移至StreamWriter之外的Get-Location

方法4e

不要使用Get-Location来构建输出文件路径,而是尝试使用.NET StreamWriter class

方法4f

不要使用ForEach-Object来构建输出文件路径,而是尝试使用与平台无关的Join-Path插值(Join-Path)。

结合方法 4a 4c 4d 4e 的变化,我们得到了最终的代码:

string

以下是针对一百万行CSV的三次运行平均每种方法的基准测试。这是在Core i7 860 @ 2.8 GHz上执行的,在Windows 10 Pro v1703上禁用TurboBoost运行64位PowerShell v5.1:

+--------+----------------------+----------------------+--------------+---------------------+-----------------+
| Method |     Path handling    |     Line building    | File writing |   Output buffering  |  Execution time |
+--------+----------------------+----------------------+--------------+---------------------+-----------------+
|    1   |       Relative       |      Export-Csv      |  Export-Csv  |          No         | 2,178.5 seconds |
+--------+----------------------+----------------------+--------------+---------------------+-----------------+
|    2   |       Relative       |      Export-Csv      |  Export-Csv  | 1,000-element array |   222.9 seconds |
+--------+----------------------+----------------------+--------------+---------------------+-----------------+
|    3   |       Relative       |      Export-Csv      |  Export-Csv  |  1,000-element List |   154.2 seconds |
+--------+----------------------+----------------------+--------------+---------------------+-----------------+
|   4a   |       Join-Path      |     ConvertTo-Csv    | StreamWriter |          No         |   425.0 seconds |
+--------+----------------------+----------------------+--------------+---------------------+-----------------+
|   4b   |       Join-Path      |     ConvertTo-Csv    | StreamWriter |  1,000-element List |   456.1 seconds |
+--------+----------------------+----------------------+--------------+---------------------+-----------------+
|   4c   |       Join-Path      | String interpolation | StreamWriter |          No         |   302.5 seconds |
+--------+----------------------+----------------------+--------------+---------------------+-----------------+
|   4d   |       Join-Path      | String interpolation | StreamWriter |          No         |   225.1 seconds |
+--------+----------------------+----------------------+--------------+---------------------+-----------------+
|   4e   | [IO.Path]::Combine() | String interpolation | StreamWriter |          No         |    78.0 seconds |
+--------+----------------------+----------------------+--------------+---------------------+-----------------+
|   4f   | String interpolation | String interpolation | StreamWriter |          No         |    77.7 seconds |
+--------+----------------------+----------------------+--------------+---------------------+-----------------+

关键要点:

  • $outputFilePath = "$outputDirectoryPath\$outputFileName";一起使用时,输出缓冲(1→2和1→3)可以显着提升性能。
  • $truncateExistingOutputFiles = $true; $outputDirectoryPath = Join-Path -Path (Get-Location) -ChildPath 'out'; $outputFileWritersByPath = @{}; try { Get-ChildItem -Filter '*.csv' -File -Force ` | Select-Object -ExpandProperty 'FullName' ` | Import-Csv -Delimiter "`t" ` | ForEach-Object -Process { $outputFileName = '{0}-{1}.csv' -f $_.Ticker, $_.Date; $outputFilePath = [System.IO.Path]::Combine($outputDirectoryPath, $outputFileName); $outputFileWriter = $outputFileWritersByPath[$outputFilePath]; if ($outputFileWriter -eq $null) { # This is the first time we're encountering this output file; create a new StreamWriter $outputFileWriter = New-Object ` -TypeName 'System.IO.StreamWriter' ` -ArgumentList ($outputFilePath, -not $truncateExistingOutputFiles, [System.Text.Encoding]::ASCII); $outputFileWritersByPath[$outputFilePath] = $outputFileWriter; # Write the header line $outputFileWriter.WriteLine('"Ticker","Price","Date"'); } # Write the data line $outputFileWriter.WriteLine("""$($_.Ticker)"",""$($_.Price)"",""$($_.Date)"""); }; } finally { foreach ($writer in $outputFileWritersByPath.Values) { $writer.Close(); } } 一起使用时,输出缓冲(4a→4b)没有帮助,实际上会导致性能下降。
  • 消除Export-Csv(4a→4c)会将执行时间缩短三分之一(153.6秒)。
  • 方法4a比缓冲的StreamWriter方法慢得多,因为它引入了ConvertTo-CsvExport-Csv的使用。这些cmdlet在幕后进行的处理比在眼前更多,或者调用cmdlet一般都很慢(当然,当做了一百万次时)。
    • Get-Location移到Join-Path之外(4c→4d)会将执行时间缩短四分之一(77.4秒)。
    • 使用Get-Location代替ForEach-Object(4d→4e)将执行时间减少了三分之二(147.1秒)。
  • 脚本优化很有趣,也很有教育意义!