【问题标题】:Merging multiple CSV files into one using PowerShell使用 PowerShell 将多个 CSV 文件合并为一个
【发布时间】:2015-03-09 16:28:09
【问题描述】:

您好,我正在寻找将目录中的所有 csv 文件合并到一个文本文件 (.txt) 的 powershell 脚本。所有 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
 )

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

谢谢。

约翰

【问题讨论】:

    标签: powershell csv batch-file


    【解决方案1】:

    如果您追求单线,您可以将每个 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 选项
    • 这绝对是最简单的解决方案——只要所有源 CSV 文件具有相同顺序的相同列集。如果源文件具有不同的列(或顺序)并且您需要一个超集文件,您需要将 Import-Csv 输出通过管道传输到 System.Data.DataTable,随时添加列,并将最终的 DataTable 输出到 Export -CSV。
    • 这是“真正的”PowerShell 答案;其他答案没有利用关键的 PowerShell 功能
    • 有没有办法提高这个性能?例如多线程?刚刚尝试合并 100 个总计 2.6 GB 的 CSV,耗时 > 30 分钟,CPU/磁盘使用率从未达到最大容量的 10%,因此它既不受 CPU 限制,也不受磁盘限制,这意味着它只是在一个线程中完成所有事情。
    • @AdityaAnand - 我认为多线程会引入更多问题 - 所有线程都会尝试附加到 merged\merged.csv。也许分批运行上述内容?即尝试合并 10 个而不是 100 个 csvs。我已经尝试将约 300 个文件合并到一个约 500MB 的文件中,总共约 100MB,它在大约 10 秒内完成。另外,请确保您也没有尝试合并您的合并文件,这是不可取的。
    【解决方案2】:

    这会将所有文件附加在一起,一次一个地读取它们:

    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
        }
    

    【讨论】:

    • 这段代码几乎完全符合我的需要。它非常快,但我只需要从第一个文件中读取标题(第一行)。在所有其他文件中,应跳过第一行。获取子项。 *.csv | foreach {[System.IO.File]::AppendAllText(".\summary.txt", [System.IO.File]::ReadAllText($_.FullName))}
    • 这很棒。正是我想要的。
    【解决方案3】:

    试试这个,它对我有用

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

    【讨论】:

    • 此方法不会跳过标题行。它将把每个文件的标题放在合并的 csv 中。
    • 迄今为止最简单的答案。谢谢!!
    【解决方案4】:

    这在 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 个文件,每个文件 100 MB,那么您显然会遇到问题。

    使用System.IO.FileSystem.IO.StreamWriter 可能会获得更好的性能。

    【讨论】:

    • 感谢您的回答。您能否建议如何在您的代码中实现 System.IO.File 和 System.IO.StreamWriter,因为加入 4000 个文件并从 3999 个文件中跳过第一行需要很长时间。
    • 数组是固定长度的。如果要添加到集合中,请使用 List 之类的东西。 theposhwolf.com/howtos/PS-Plus-Equals-Dangers
    • @Zachafer 谢谢。我很清楚这个问题,但这是一个古老的答案。我已经用更好的模式替换了代码。
    【解决方案5】:

    您的批处理文件效率很低!试试这个(你会感到惊讶:)

    @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. &gt;&gt; 重定向打开文件,在末尾附加数据并关闭文件,这是通过每个输出 *line* 完成的。 &gt; 重定向使文件始终保持打开状态。

    【讨论】:

    • 你认为值得解释一下你和 OP 之间的区别吗?
    • @Matt - Aacini's 消除了对计数器变量和逻辑检查的需要,使脚本在循环中执行的操作更少,从而更快。
    • 感谢您的帮助,但由于某种原因它不起作用。错误是:“删除不被识别为内部或外部命令,可运行程序或批处理文件。我猜应该有ECHO “删除现有的组合文件”之前的命令。但即使我修复它也不起作用。摘要文件中只有几个字符。
    • @Matt:两个最重要的区别是:1.for /f ... in ('type "%%i"')需要加载并执行cmd.exe才能执行type命令,捕获它的在一个临时文件中输出,然后从中读取数据,这是每个输入文件完成的。 for /f ... in ("%%i") 直接从文件中读取数据。 2.&gt;&gt; 重定向打开文件,在末尾附加数据并关闭文件,这是每个输出*line* 完成的。 &gt; 重定向使文件始终保持打开状态。
    【解决方案6】:

    这是一个同样使用 System.IO.File 的版本,

    $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 时,一个文件的最后一行与新文件的第一行合并在一起。
    • 刚刚编辑了代码以在每个 csv 行之后插入一个“NewLine”。
    • 非常感谢。现在它工作正常,但它比 Kevin 的代码慢 2 倍以上。除非有人在一个目录中有超过几百个文件,否则这无关紧要。再次感谢您。
    • 我明白了,我明白为什么,我分别写了每一行。如果你有时间,试试这个代码......(再次编辑)
    • 我的直觉是直接调用 .NET 应该比“Get-content”/“Add-Content”更快,但我想不是。在使用 500 个 CSV 文件的样本测试两个版本之后,“Get-content”/“Add-Content”胜出。此 [System.IO.File] 版本: 经过时间:2.254 秒 Kevin 的(“获取内容”/“添加内容”)版本经过时间:1.741 秒
    【解决方案7】:

    如果您需要递归扫描文件夹,则可以使用以下方法

    Get-ChildItem -Recurse -Path .\data\*.csv  | Get-Content | Add-Content output.csv
    

    这基本上是:

    • Get-ChildItem -Recurse -Path .\data\*.csv递归查找请求的文件
    • Get-Content 获取每个内容
    • Add-Content output.csv 将其附加到 output.csv

    【讨论】:

      【解决方案8】:

      我发现以前的解决方案在性能方面对于大型 csv 文件效率很低,所以这里有一个高性能替代方案

      这是一个简单地附加文件的替代方法:

      cmd /c copy  ((gci "YOUR_DIRECTORY\*.csv" -Name) -join '+') "YOUR_OUTPUT_FILE.csv" 
      

      此后,您可能希望摆脱多个 csv-headers。

      【讨论】:

        【解决方案9】:
        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
        

        【讨论】:

        • 虽然这可能是答案,但对提出问题的人来说,一些上下文/解释会有所帮助。
        【解决方案10】:

        以下批处理脚本非常快。只要您的 CSV 文件不包含制表符,并且所有源 CSV 文件的行数少于 64k,它就应该可以正常工作。

        @echo off
        set "skip="
        >summary.txt (
          for %%F in (*.csv) do if defined skip (
            more +1 "%%F"
          ) else (
            more "%%F"
            set skip=1
          )
        )
        

        限制的原因是MORE将制表符转换为一系列空格,重定向的MORE在64k行处挂起。

        【讨论】:

        • 将类型更改为更多,以防第一个文件不以新行结尾
        【解决方案11】:

        stinkyfriend's helpful answer 展示了一个基于Import-CsvExport-Csv 的优雅、PowerShell 惯用的解决方案。

        很遗憾,

        • 它非常缓慢,因为它最终涉及到对象的不必要的往返转换。

        • 此外,即使对于 CSV 解析器而言,文件的特定格式可能会在此过程中改变,因为Export-Csv 双引号 > 所有列值,始终Windows PowerShell 中,默认情况下PowerShell (Core) 7+ 中,其中现在通过-UseQuotes-QuoteFields 提供选择加入控制。

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

        以下PSv5+解决方案:

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

        字符编码警告

        • PowerShell 从不保留文件的输入字符编码,因此您可能必须使用-Encoding 参数来覆盖Set-Content 的默认编码(同样适用于Export-Csv 和任何其他文件写入cmdlet;在PowerShell (Core) 7+ 所有 cmdlet 现在始终默认为无 BOM 的 UTF-8;但不仅 Windows PowerShell cmdlet 不默认为 UTF-8,它们还使用 不同的编码 - 请参阅this answer 的底部)。
        # 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.
        

        【讨论】:

          【解决方案12】:
          #Input path
          $InputFolder = "W:\My Documents\... input folder"
          $FileType    = "*.csv"
          
          #Output path
          $OutputFile  = "W:\My Documents\... some folder\merged.csv"
          
          #Read list of files
          $AllFilesFullName = @(Get-ChildItem -LiteralPath $InputFolder -Filter $FileType | Select-Object -ExpandProperty FullName)
          
          #Loop and write 
          Write-Host "Merging" $AllFilesFullName.Count $FileType "files."
          foreach ($FileFullName in $AllFilesFullName) {
              Import-Csv $FileFullName | Export-Csv $OutputFile -NoTypeInformation -Append
              Write-Host "." -NoNewline
          }
          
          Write-Host
          Write-Host "Merge Complete"
          

          【讨论】:

            【解决方案13】:
            $pathin = 'c:\Folder\With\CSVs'
            $pathout = 'c:\exported.txt'
            $list = Get-ChildItem -Path $pathin | select FullName
            foreach($file in $list){
                Import-Csv -Path $file.FullName | Export-Csv -Path $pathout -Append -NoTypeInformation
            }
            

            【讨论】:

              【解决方案14】:

              输入 *.csv >> 文件夹\combined.csv

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2015-06-19
                • 2019-10-11
                • 2021-11-21
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多