使用PowerShell将多个CSV文件合并为一个

35

你好,我正在寻找一个PowerShell脚本,可以将一个目录中的所有csv文件合并成一个文本文件(.txt)。所有的csv文件都有相同的标题,该标题始终存储在每个文件的第一行。因此,我需要从第一个文件中取出标题,并跳过其余文件的第一行。

我已经找到了一个批处理文件,可以完全满足我的需求,但是我有超过4000个csv文件在单个目录中,这需要超过45分钟来完成任务。

@echo off
ECHO Set working directory
cd /d %~dp0
Deleting existing combined file
del summary.txt
setlocal ENABLEDELAYEDEXPANSION
set cnt=1
for %%i in (*.csv) do (
 if !cnt!==1 (
 for /f "delims=" %%j in ('type "%%i"') do echo %%j >> summary.txt
) else (
 for /f "skip=1 delims=" %%j in ('type "%%i"') do echo %%j >> summary.txt
 )
 set /a cnt+=1
 )

有什么建议可以创建比这个批处理代码更有效的PowerShell脚本吗?

谢谢。

约翰

15个回答

75

如果你只需要一行代码,你可以将每个csv文件导入Import-Csv,然后立即将其传输到Export-Csv。这将保留初始标题行并排除剩余文件的标题行。它还将逐个处理每个csv文件,而不是将所有文件加载到内存中,然后将它们转储到合并的csv文件中。

Get-ChildItem -Filter *.csv | Select-Object -ExpandProperty FullName | Import-Csv | Export-Csv .\merged\merged.csv -NoTypeInformation -Append

这个能在 PowerShell 2 版本上运行的方法吗?这是我唯一拥有的版本,但它不包括 Export-Csv 中的 -Append 选项。 - rw2
这绝对是最简单的解决方案——只要所有源CSV文件在相同顺序的列中都有相同的集合。如果源文件具有不同的列(或顺序),并且您需要一个超集文件,则需要将Import-Csv输出管道传输到System.Data.DataTable中,按需添加列,并将最终DataTable管道传输到Export-Csv。 - AlwaysLearning
1
这是“真正的”PowerShell答案;其他答案没有充分利用关键的PowerShell功能。 - Zacharious
1
有没有办法提高这个程序的性能?例如多线程?我刚试图合并一百个 CSV 文件,总计 2.6 GB,花费了超过 30 分钟,但 CPU 和磁盘使用率从未达到最大容量的 ~10%,因此既不是 CPU 限制也不是磁盘限制,这意味着它只是在单个线程中执行所有操作。 - Aditya Anand
@AdityaAnand - 我认为多线程会引入更多问题 - 所有线程都将尝试附加到 merged\merged.csv。也许分批运行上述操作?即尝试合并10个而不是100个csv文件。我已经尝试过将约300个文件合并成总计约100MB的文件,最终生成了一个大约500MB的文件,并且只需要大约10秒钟。此外,请确保您不要尝试合并您的合并文件,这是不可取的。 - stinkyfriend

57
这将把所有的文件都逐个读取然后追加到一起:
get-childItem "YOUR_DIRECTORY\*.txt" 
| foreach {[System.IO.File]::AppendAllText
 ("YOUR_DESTINATION_FILE", [System.IO.File]::ReadAllText($_.FullName))}

# Placed on seperate lines for readability

这个选项将在每个文件条目末尾放置一个新行,如果您需要的话:
get-childItem "YOUR_DIRECTORY\*.txt" | foreach
{[System.IO.File]::AppendAllText("YOUR_DESTINATION_FILE", 
[System.IO.File]::ReadAllText($_.FullName) + [System.Environment]::NewLine)}

跳过第一行:

$getFirstLine = $true

get-childItem "YOUR_DIRECTORY\*.txt" | foreach {
    $filePath = $_

    $lines =  $lines = Get-Content $filePath  
    $linesToWrite = switch($getFirstLine) {
           $true  {$lines}
           $false {$lines | Select -Skip 1}

    }

    $getFirstLine = $false
    Add-Content "YOUR_DESTINATION_FILE" $linesToWrite
    }

这段代码几乎做到了我需要的一切。并且它非常快,但是我需要仅从第一个文件中读取标题(第一行)。在所有其他文件中,第一行应该被跳过。get-childItem . *.csv | foreach {[System.IO.File]::AppendAllText(".\summary.txt", [System.IO.File]::ReadAllText($_.FullName))} - john50
这太棒了。正是我想要的。 - Amitabh Ghosh

12

试一试,这对我有用

Get-Content *.csv| Add-Content output.csv

3
此方法不会跳过标题行。它会将每个文件的标题放入合并后的CSV中。 - Mike Deluca
迄今为止最简单的答案。谢谢! - jim

6
这在PowerShell中相当简单。
$CSVFolder = 'C:\Path\to\your\files';
$OutputFile = 'C:\Path\to\output\file.txt';

$CSV = Get-ChildItem -Path $CSVFolder -Filter *.csv | ForEach-Object { 
    Import-Csv -Path $_
}

$CSV | Export-Csv -Path $OutputFile -NoTypeInformation -Force;

这种方法的唯一缺点是它会解析每个文件。同时,它也会将所有文件加载到内存中,因此如果有4000个大小为100MB的文件,你显然会遇到问题。 你可以使用System.IO.File和System.IO.StreamWriter来获得更好的性能。

谢谢您的回答。能否请您建议如何将 System.IO.File 和 System.IO.StreamWriter 实现到您的代码中?因为合并 4000 个文件并跳过前 3999 个文件的第一行需要很长时间。 - john50
数组长度是固定的。如果你想要添加到一个集合中,可以使用类似于List的东西。https://theposhwolf.com/howtos/PS-Plus-Equals-Dangers/ - Zacharious
2
@Zachafer 谢谢。我很清楚这个问题,但是这个答案已经过时了。我已经用更好的模式替换了代码。 - Bacon Bits
如果您的文件位于网络共享上,请将“Import-Csv -Path $_”更改为“Import-Csv -Path $ _.FullName”,否则脚本会认为您正在使用C:这种情况曾经发生在我身上。 - shadow2020

2
现代 Powershell 7 的解决方案:
(假设所有的 csv 文件都在同一个目录下,并且有相同数量的字段。)
@(Get-ChildItem -Filter *.csv).fullname | Import-Csv |Export-Csv ./merged.csv -NoTypeInformation

流水线的第一部分获取所有 .csv 文件并解析全名(路径 + 文件名 + 扩展名),然后导入 CSV 获取每个文件并创建一个对象,然后将每个对象合并到一个仅有一个标题的单个 CSV 文件中。


2
如果您需要递归扫描文件夹,则可以使用以下方法。
Get-ChildItem -Recurse -Path .\data\*.csv  | Get-Content | Add-Content output.csv

这段代码的作用是:

  • Get-ChildItem -Recurse -Path .\data\*.csv 递归查找指定的CSV文件
  • Get-Content 获取每个文件的内容
  • Add-Content output.csv 将获取到的内容追加到output.csv中

2

您的批处理文件效率不太高!试试这个(您会感到惊讶的:)

@echo off
ECHO Set working directory
cd /d %~dp0
ECHO Deleting existing combined file
del summary.txt
setlocal
for %%i in (*.csv) do set /P "header=" < "%%i" & goto continue
:continue

(
   echo %header%
   for %%i in (*.csv) do (
      for /f "usebackq skip=1 delims=" %%j in ("%%i") do echo %%j
   )
) > summary.txt

这样做的改进之处

  1. for /f ... in ('type "%%i"')需要加载和执行cmd.exe以执行type命令,将其输出捕获到临时文件中,然后从中读取数据,并且对于每个输入文件都要这样做。而for /f ... in ("%%i")直接从文件中读取数据。
  2. >>重定向打开文件,将数据附加到末尾并关闭文件,对于每个输出*行*都要这样做。而>重定向则一直保持文件处于打开状态。

你认为解释你和原帖作者之间的区别是否值得吗? - Matt
@Matt - Aacini的方法消除了计数器变量和逻辑检查的需要,使脚本在循环内执行的任务更少,从而提高了速度。 - SomethingDark
谢谢你的帮助,但出于某种原因它不起作用。错误是:“删除”不被识别为内部或外部命令、可操作的程序或批处理文件。我猜在“删除现有合并文件”之前应该有ECHO命令。但即使我修复了它,它仍然不起作用。总结文件中只有几个字符。 - john50
@Matt:最重要的两个区别是:1. for /f ... in ('type "%%i"') 需要加载和执行 cmd.exe 以执行 type 命令,将其输出捕获到临时文件中,然后从中读取数据,并且这是针对每个输入文件都要做的。而 for /f ... in ("%%i") 直接从文件中读取数据。 2. >> 重定向打开文件,在末尾追加数据并关闭文件,并且这是针对每个输出都要做的。而 > 重定向则一直保持文件处于打开状态。 - Aacini

1
这是另一个使用System.IO.File的版本,保留了HTML标签。
$result = "c:\temp\result.txt"
$csvs = get-childItem "c:\temp\*.csv" 
#read and write CSV header
[System.IO.File]::WriteAllLines($result,[System.IO.File]::ReadAllLines($csvs[0])[0])
#read and append file contents minus header
foreach ($csv in $csvs)  {
    $lines = [System.IO.File]::ReadAllLines($csv)
    [System.IO.File]::AppendAllText($result, ($lines[1..$lines.Length] | Out-String))
}

谢谢您的回答,但 result.txt 文件因某些原因不是正确的格式。当我按 F4 键时,所有内容都会被放在一起。同时,当我按 F3 键时,一个文件的最后一行与另一个文件的第一行合并在了一起。 - john50
刚刚编辑了代码,在每个 CSV 行后插入了一个“NewLine”。 - Jan Chrbolka
非常感谢。现在它可以正常工作了,但是它比Kevin的代码慢了两倍以上。除非有人在一个目录中有超过几百个文件,否则这不应该成为问题。再次感谢您。 - john50
我明白了,我能理解为什么了,我之前是逐行编写的。如果你有时间,可以试试这段代码...(再次编辑) - Jan Chrbolka
我的直觉是直接调用.NET应该比“Get-content”/“Add-Content”更快,但我想这并不是事实。在使用500个CSV文件的样本测试了两个版本之后,“Get-content”/“Add-Content”毫无疑问获胜。 [System.IO.File]版本:耗时2.254秒 Kevin的(“Get-content”/“Add-Content”)版本:耗时1.741秒 - Jan Chrbolka

1

stinkyfriend的有用答案展示了一种优雅、PowerShell惯用的解决方案,基于Import-CsvExport-Csv

然而,它相当缓慢,因为它涉及到不必要的往返转换为和从对象

  • 此外,尽管这对CSV解析器应该没有影响,文件的特定格式可能会在过程中被更改,因为Export-Csv会双引号所有列值无论如何都是这样,在Windows PowerShell中,默认情况下PowerShell (Core) 7+中提供了通过-UseQuotes-QuoteFields进行选择性控制的选项)。

当性能很重要时,需要使用纯文本解决方案,这也避免了任何意外的格式更改(就像链接的答案一样,它假设所有输入CSV文件具有相同的列结构)。

以下是PSv5+解决方案:

  • 使用Get-Content-Raw将每个输入文件的内容完整地读入内存中,作为单个多行字符串(比默认逐行读取要快得多)
  • 使用基于正则表达式的-replace操作符跳过除第一个文件之外的所有文件的标题行-replace '^.+\r?\n'
  • 并使用Set-Content-NoNewLine将结果保存到目标文件中。

字符编码注意事项

  • PowerShell不会保留文件的输入字符编码,因此您可能需要使用 -Encoding 参数来覆盖 Set-Content 的默认编码(对于 Export-Csv 和任何其他写入文件的命令,也适用此规则;在 PowerShell (Core) 7+ 中,所有命令现在都默认为不带BOM的UTF-8编码;但是,Windows PowerShell 命令未默认使用UTF-8,它们使用 不同的 编码 - 请参见 此答案 底部的部分)。
# Determine the output file and remove a preexisting one, if any.
$outFile = 'summary.csv'
if (Test-Path $outFile) { Remove-Item -ErrorAction Stop $outFile }

# Process all *.csv files in the current folder and merge their contents,
# skipping the header line for all but the first file.
$first = $true
Get-ChildItem -Filter *.csv | 
  Get-Content -Raw | 
    ForEach-Object {
      $content = 
        if ($first) { # first file: output content as-is
          $_; $first = $false
        } else { # subsequent file: skip the header line.
          $_ -replace '^.+\r?\n'
        }
      # Make sure that each file content ends in a newline
      if (-not $content.EndsWith("`n")) { $content += [Environment]::NewLine }
      $content # Output
    } | 
      Set-Content -NoNewLine $outFile # add -Encoding as needed.

1
Get-ChildItem *.csv|select -First 1|Get-Content|select -First 1|Out-File -FilePath .\input.csv -Force #Get the header from one of the CSV Files, write it to input.csv
Get-ChildItem *.csv|foreach {Get-Content $_|select -Skip 1|Out-File -FilePath .\Input.csv -Append} #Get the content of each file, excluding the first line and append it to input.csv

虽然这可能是答案,但给提问者一些背景/解释会更有帮助。 - static_cast

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