Объединение нескольких файлов CSV в один с помощью PowerShell

Здравствуйте, я ищу скрипт 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 ответов

Решение

Это добавит все файлы вместе, читая их по одному:

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 к Import-Csv а затем сразу же трубку, чтобы Export-Csv, Это сохранит начальную строку заголовка и исключит оставшиеся строки заголовка файла. Он также будет обрабатывать каждый CSV по одному, а не загружать все в память, а затем выгружать их в объединенный CSV.

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

Попробуйте это, у меня получилось

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

Это довольно тривиально в PowerShell.

$CSVFolder = 'C:\Path\to\your\files';
$OutputFile = 'C:\Path\to\output\file.txt';

$CSV= @();

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

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

Единственным недостатком этого подхода является то, что он анализирует каждый файл. Он также загружает все файлы в память, поэтому, если мы говорим о 4000 файлах по 100 МБ каждый, вы, очевидно, столкнетесь с проблемами.

Вы можете получить лучшую производительность с System.IO.File а также System.IO.StreamWriter,

Если вам нужно рекурсивно сканировать папку, вы можете использовать подход ниже

      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

Ваш пакетный файл довольно неэффективен! Попробуйте это (вы будете удивлены:)

@echo off
ECHO Set working directory
cd /d %~dp0
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, чтобы выполнить команду типа, записать ее вывод во временный файл, а затем прочитать данные из него, и это делается с каждым входным файлом. for /f ... in ("%%i") напрямую читает данные из файла.
  2. >> перенаправление открывает файл, добавляет данные в конце и закрывает файл, и это делается с каждым выводом * line *. > перенаправление сохраняет файл открытым все время.

Современный ответ Powershell 7:
(при условии, что все файлы csv находятся в одном каталоге и имеют одинаковое количество полей.)

      @(Get-ChildItem -Filter *.csv).fullname | Import-Csv |Export-Csv ./merged.csv -NoTypeInformation

Первая часть конвейера получает все файлы .csv и анализирует полное имя (путь + имя файла + расширение), затем импорт CSV берет каждый и создает объект, а затем каждый объект объединяется в один файл CSV только с одним заголовком.

Я обнаружил, что предыдущие решения довольно неэффективны для больших csv-файлов с точки зрения производительности, поэтому здесь есть эффективная альтернатива.

Вот альтернатива, которая просто добавляет файлы:

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

После этого вы, вероятно, захотите избавиться от нескольких заголовков csv.

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

Вот версия, также использующая 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))
}

Полезный ответ stinkyfriend показывает элегантное идиоматическое решение PowerShell, основанное на Import-Csv а также .

К несчастью,

  • это довольно медленно, потому что включает в себя, в конечном счете, ненужное двустороннее преобразование в объекты и обратно .

  • Кроме того, даже если это не имеет значения для парсера CSV, конкретный формат файлов может быть изменен в процессе, потому что все значения столбцов заключаются в двойные кавычки , что неизменно в Windows PowerShell, по умолчанию в PowerShell (Core) 7+, который теперь предлагает управление подпиской через -UseQuotes а также -QuoteFields).

Когда производительность имеет значение, требуется текстовое решение, которое также позволяет избежать непреднамеренного изменения формата (как и связанный ответ, он предполагает, что все входные файлы CSV имеют одинаковую структуру столбцов).

Следующее решение для PSv5+:

  • считывает содержимое каждого входного файла в память полностью как одну многострочную строку, используя Get-Content -Raw (что намного быстрее, чем построчное чтение по умолчанию),
  • пропускает строку заголовка для всех файлов, кроме первого с -replace '^.+\r?\n', используя основанное на регулярном выражении -replaceоператор ,
  • и сохраняет результаты в целевой файл с помощью -NoNewLine.

Предупреждение о кодировке символов :

  • PowerShell никогда не сохраняет кодировку входных символов файлов, поэтому вам, возможно, придется использовать -Encoding параметр для переопределения Set-Contentкодировка по умолчанию (то же самое относится и к Export-Csvи любые другие командлеты для записи файлов; в PowerShell (Core) 7+ для всех командлетов теперь по умолчанию используется 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.
      #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"

Следующий пакетный скрипт очень быстрый. Это должно работать хорошо, если ни один из ваших CSV-файлов не содержит символов табуляции, а все исходные CSV-файлы содержат менее 64 тыс. Строк.

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

Причина ограничений заключается в том, что MORE преобразует вкладки в ряд пробелов, а перенаправленный MORE зависает на 64 тыс. Строк.

$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
}

Введите *.csv >> folder\ комбинированный.csv

Другие вопросы по тегам