source

PowerShell을 사용하여 여러 CSV 파일을 하나로 병합

goodcode 2023. 8. 24. 22:43
반응형

PowerShell을 사용하여 여러 CSV 파일을 하나로 병합

안녕하세요. 디렉터리에 있는 모든 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
 )

이 배치 코드보다 더 효율적인 파워셸 스크립트를 만드는 방법에 대한 제안이 있습니까?

감사해요.

존.

원라이너를 , 를 원라를경각우 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-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-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 { 
    Import-Csv -Path $_
}

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

이 접근 방식의 유일한 단점은 모든 파일을 구문 분석한다는 것입니다.또한 모든 파일을 메모리에 로드하기 때문에 각각 100MB씩 4000개의 파일을 사용하면 문제가 발생할 수 있습니다.

를 사용하면 더 나은 성능을 얻을 수 있습니다.System.IO.File그리고.System.IO.StreamWriter.

배치 파일이 상당히 비효율적입니다!이것을 사용해 보세요 (놀라실 겁니다 :)

@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"')에서는 type 명령을 실행하고 출력을 임시 파일에 캡처한 다음 데이터를 읽으려면 cmd.exe를 로드하고 실행해야 합니다. 그러면입력 파일에서 이 작업이 수행됩니다.for /f ... in ("%%i")파일에서 직접 데이터를 읽습니다.
  2. >>redirection은 파일을 열고, 끝에 데이터를 추가하고, 파일을 닫습니다. 그러면 이것은 각 출력 *line*으로 수행됩니다.>리디렉션은 파일을 항상 열어 둡니다.

폴더를 재귀적으로 검색해야 하는 경우 아래 접근 방식을 사용할 수 있습니다.

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

이것이 기본적으로 하는 일은 다음과 같습니다.

  • Get-ChildItem -Recurse -Path .\data\*.csv한 파일을 .
  • Get-Content각 항목에 대한 내용 가져오기
  • Add-Content output.csv 파일 이름.csv

여기에도 시스템을 사용하는 버전이 있습니다.IO.파일,

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

악취가 나는 친구의 유용한 답변은 및 을 기반으로 한 우아하고 PowerShell-diomatic 솔루션을 보여줍니다.

불행하게도,

  • 궁극적으로 불필요한 왕복 변환이 수반되기 때문에 상당히 느립니다.

  • 또한 CSV 파서에 문제가 되지 않더라도 파일의 특정 형식이 프로세스에서 변경될 수 있습니다.Export-Csv PowerShell(Core) 7+에서는 기본적으로 모든 열 값을 두 로 따옴표로 묶습니다. PowerShell(Core) 7+에서는 다음을 통해 최적화된 제어 기능을 제공합니다.-UseQuotes그리고.-QuoteFields).

성능이 중요한 경우 일반 텍스트 솔루션이 필요합니다. 이 솔루션을 사용하면 실수로 형식이 변경되는 것을 방지할 수 있습니다(링크된 답변과 마찬가지로 모든 입력 CSV 파일의 열 구조가 동일하다고 가정).

다음 PSv5+ 솔루션:

  • 각 입력 파일의 내용을 메모리에 전체적으로 읽어 들입니다. 단일 다중 행 문자열로, -Raw 읽기 (줄별 읽기 속도),
  • 한 모든 을 건너뜁니다.-replace '^.+\r?\n'정규식 기반 연산자를 사용합니다.
  • 를 사용하여 결과를 대상 파일에 저장합니다. -NoNewLine.

문자 인코딩 주의:

  • 파일의 PowerShell 은파일입인을보딩존지않하사다다있습니할을 해야 할 수도 .-Encoding to override Set-Content입니다(「 」 「 」 「 」 「 」도 마찬가지입니다Export-Csv기타 파일 쓰기 cmdlet, PowerShell(Core) 7+에서는 모든 cmdlet이 BOM-less UTF-8로 일관되게 기본 설정됩니다. Windows PowerShell cmdlet은 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.

파워셸 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-header를 제거할 수 있습니다.

다음 배치 스크립트는 매우 빠릅니다.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 줄에 매달려 있기 때문입니다.

#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"
$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 >> 폴더\csv.csv를 입력합니다.

언급URL : https://stackoverflow.com/questions/27892957/merging-multiple-csv-files-into-one-using-powershell

반응형