我有以下格式的文件夹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
}
有什么想法吗?
答案 0 :(得分:3)
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记录。我们使用Ticker
和Date
属性构建适合该记录的输出路径(后者是string
而不是DateTime
,因此没有{{3}必要的)。然后,我们将记录传递给Export-Csv
,并将新行追加到$outputPath
的文件中。虽然这段代码很简单,但每个输入记录打开并附加到每个输出文件一次非常慢,特别是对于一百万行,尽管内存使用量很小,因为在任何给定时间内只有一条记录在内存中。 / p>
我们可以通过仅在每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;
}
我们可以通过使用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;
}
如果我们使用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%...... 较慢。我会在进一步修改此代码时确定原因。
我首先想到的是性能下降是为了重新引入输出缓冲;基本上,结合方法3和4a。同样令人惊讶的是,这只会进一步损害性能。我唯一的猜测是因为StreamWriter
自己的角色缓冲它使得我们自己的缓冲不必要。事实上,我测试了{10}的幂{10}的值{10}到100,000,这两个极端的整体性能差异只有5秒。因此,我们自己的缓冲并没有真正帮助任何事情,管理maxPendingRecordsPerFilePath
的微小开销在一百万次迭代中增加了30秒的运行时间。所以,让我们废弃缓冲。
不要使用List
来排除ConvertTo-Csv
s(标题行和数据行)的2元素数组,而是让我们自己使用{{1}构建这两行格式化。
在string
的每次迭代中,我们都需要构建输出文件路径,因为它基于输入对象string
和ForEach-Object
属性。我们在构造Ticker
时传递一个绝对路径,因为PowerShell有一个不同的&#34;当前目录&#34; (相对路径将基于),而不是典型的.NET应用程序。我们一直在调用Date
来构建每次迭代的绝对路径,这是不必要的,因为该路径不会发生变化。因此,我们将调用移至StreamWriter
之外的Get-Location
。
不要使用Get-Location
来构建输出文件路径,而是尝试使用.NET StreamWriter
class。
不要使用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秒)。StreamWriter
方法慢得多,因为它引入了ConvertTo-Csv
和Export-Csv
的使用。这些cmdlet在幕后进行的处理比在眼前更多,或者调用cmdlet一般都很慢(当然,当做了一百万次时)。
Get-Location
移到Join-Path
之外(4c→4d)会将执行时间缩短四分之一(77.4秒)。Get-Location
代替ForEach-Object
(4d→4e)将执行时间减少了三分之二(147.1秒)。