使用 PowerShell 将多个 CSV 文件合并为一个文件
Merging multiple CSV files into one using PowerShell
您好,我正在寻找 powershell 脚本,它将目录中的所有 csv 文件合并到一个文本文件 (.txt) 中。所有 csv 文件都具有相同的 header,它始终存储在每个文件的第一行中。所以我需要从第一个文件中获取 header,但在其余文件中,第一行应该被跳过。
我能够找到完全符合我需要的批处理文件,但我在一个目录中有超过 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 脚本的任何建议?
谢谢。
约翰
这在 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.File
和 System.IO.StreamWriter
可能会获得更好的性能。
这会将所有文件附加在一起,一次读取一个文件:
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
}
您的批处理文件效率很低!试试这个(你会惊讶的:)
@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
这是一个怎样的进步
for /f ... in ('type "%%i"')
需要加载并执行 cmd.exe 才能执行 type 命令,将其输出捕获到一个临时文件中,然后从中读取数据,这是通过 [=19 完成的=]每个输入文件。 for /f ... in ("%%i")
直接从文件中读取数据。
>>
重定向打开文件,在末尾附加数据并关闭文件,这是通过 每个输出 *line* 完成的。 >
重定向使文件始终保持打开状态。
这是一个同样使用 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))
}
下面的批处理脚本非常快。只要您的 none 个 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行。
$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
}
如果您在 one-liner 之后,您可以将每个 csv 传送到 Import-Csv
,然后立即将其传送到 Export-Csv
。这将保留初始 header 行并排除剩余的文件 header 行。它还将一次处理每个 csv,而不是将所有 csv 加载到内存中,然后将它们转储到合并的 csv 中。
Get-ChildItem -Filter *.csv | Select-Object -ExpandProperty FullName | Import-Csv | Export-Csv .\merged\merged.csv -NoTypeInformation -Append
我发现以前的解决方案对于大型 csv-files 在性能方面效率很低,所以这里有一个 高性能的替代方案。
这是一个简单地附加文件的替代方法:
cmd /c copy ((gci "YOUR_DIRECTORY\*.csv" -Name) -join '+') "YOUR_OUTPUT_FILE.csv"
此后,您可能想要摆脱多个 csv-headers。
输入 *.csv >> folder\combined.csv
试试这个,对我有用
Get-Content *.csv| Add-Content output.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
stinkyfriend's helpful answer shows an elegant, PowerShell-idiomatic solution based on Import-Csv
and Export-Csv
.
不幸的是,
相当慢,因为它涉及最终不必要的 round-trip 与 objects[= 之间的转换68=].
此外,即使 CSV 解析器不重要,文件的特定格式也可能会在此过程中被更改,因为Export-Csv
double-quotes 所有列值,总是 所以在 Windows PowerShell、在 PowerShell (Core) 7+ 中默认 ,现在通过 -UseQuotes
和 [= 提供 opt-in 控制15=]).
当性能很重要时,需要 plain-text 解决方案,这也避免了任何无意的格式更改(就像链接的答案假定所有输入 CSV 文件都具有相同的列结构)。
以下 PSv5+ 解决方案:
- 将每个输入文件的内容完整读入内存,作为单个multi-line字符串,使用
Get-Content
-Raw
(这比比默认 line-by-line 读数更快),
- 使用 regex-based
-replace
operator, 跳过 header 行,除了第一个带有 -replace '^.+\r?\n'
的文件
- 并用
Set-Content
-NoNewLine
. 将结果保存到目标文件
Character-encoding 警告:
- PowerShell 从不保留文件的输入字符编码,因此您可能必须使用
-Encoding
参数来覆盖 Set-Content
的默认编码(同样适用于 Export-Csv
和任何其他 file-writing cmdlet;在 PowerShell (Core) 7+ 中,所有 cmdlet 现在始终默认为 BOM-less UTF-8;但不仅 Windows PowerShell cmdlet 不默认为 UTF-8,它们使用 不同的 编码 - 请参阅 this answer).[=70= 的底部部分]
# 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"
如果您需要递归扫描文件夹,那么您可以使用下面的方法
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
现代 Powershell 7 答案:
(假设所有 csv 文件都在同一个目录中并且具有相同数量的字段。)
@(Get-ChildItem -Filter *.csv).fullname | Import-Csv |Export-Csv ./merged.csv -NoTypeInformation
管道的第一部分获取所有 .csv 文件并解析全名(路径 + 文件名 + 扩展名),然后导入 CSV 获取每个文件并创建一个 object 然后每个 object 得到合并成一个只有一个 header.
的 CSV 文件
您好,我正在寻找 powershell 脚本,它将目录中的所有 csv 文件合并到一个文本文件 (.txt) 中。所有 csv 文件都具有相同的 header,它始终存储在每个文件的第一行中。所以我需要从第一个文件中获取 header,但在其余文件中,第一行应该被跳过。 我能够找到完全符合我需要的批处理文件,但我在一个目录中有超过 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 脚本的任何建议?
谢谢。
约翰
这在 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.File
和 System.IO.StreamWriter
可能会获得更好的性能。
这会将所有文件附加在一起,一次读取一个文件:
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
}
您的批处理文件效率很低!试试这个(你会惊讶的:)
@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
这是一个怎样的进步
for /f ... in ('type "%%i"')
需要加载并执行 cmd.exe 才能执行 type 命令,将其输出捕获到一个临时文件中,然后从中读取数据,这是通过 [=19 完成的=]每个输入文件。for /f ... in ("%%i")
直接从文件中读取数据。>>
重定向打开文件,在末尾附加数据并关闭文件,这是通过 每个输出 *line* 完成的。>
重定向使文件始终保持打开状态。
这是一个同样使用 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))
}
下面的批处理脚本非常快。只要您的 none 个 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行。
$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
}
如果您在 one-liner 之后,您可以将每个 csv 传送到 Import-Csv
,然后立即将其传送到 Export-Csv
。这将保留初始 header 行并排除剩余的文件 header 行。它还将一次处理每个 csv,而不是将所有 csv 加载到内存中,然后将它们转储到合并的 csv 中。
Get-ChildItem -Filter *.csv | Select-Object -ExpandProperty FullName | Import-Csv | Export-Csv .\merged\merged.csv -NoTypeInformation -Append
我发现以前的解决方案对于大型 csv-files 在性能方面效率很低,所以这里有一个 高性能的替代方案。
这是一个简单地附加文件的替代方法:
cmd /c copy ((gci "YOUR_DIRECTORY\*.csv" -Name) -join '+') "YOUR_OUTPUT_FILE.csv"
此后,您可能想要摆脱多个 csv-headers。
输入 *.csv >> folder\combined.csv
试试这个,对我有用
Get-Content *.csv| Add-Content output.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
stinkyfriend's helpful answer shows an elegant, PowerShell-idiomatic solution based on Import-Csv
and Export-Csv
.
不幸的是,
相当慢,因为它涉及最终不必要的 round-trip 与 objects[= 之间的转换68=].
此外,即使 CSV 解析器不重要,文件的特定格式也可能会在此过程中被更改,因为
Export-Csv
double-quotes 所有列值,总是 所以在 Windows PowerShell、在 PowerShell (Core) 7+ 中默认 ,现在通过-UseQuotes
和 [= 提供 opt-in 控制15=]).
当性能很重要时,需要 plain-text 解决方案,这也避免了任何无意的格式更改(就像链接的答案假定所有输入 CSV 文件都具有相同的列结构)。
以下 PSv5+ 解决方案:
- 将每个输入文件的内容完整读入内存,作为单个multi-line字符串,使用
Get-Content
-Raw
(这比比默认 line-by-line 读数更快), - 使用 regex-based
-replace
operator, 跳过 header 行,除了第一个带有 - 并用
Set-Content
-NoNewLine
. 将结果保存到目标文件
-replace '^.+\r?\n'
的文件
Character-encoding 警告:
- PowerShell 从不保留文件的输入字符编码,因此您可能必须使用
-Encoding
参数来覆盖Set-Content
的默认编码(同样适用于Export-Csv
和任何其他 file-writing cmdlet;在 PowerShell (Core) 7+ 中,所有 cmdlet 现在始终默认为 BOM-less UTF-8;但不仅 Windows PowerShell cmdlet 不默认为 UTF-8,它们使用 不同的 编码 - 请参阅 this answer).[=70= 的底部部分]
# 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"
如果您需要递归扫描文件夹,那么您可以使用下面的方法
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
现代 Powershell 7 答案:
(假设所有 csv 文件都在同一个目录中并且具有相同数量的字段。)
@(Get-ChildItem -Filter *.csv).fullname | Import-Csv |Export-Csv ./merged.csv -NoTypeInformation
管道的第一部分获取所有 .csv 文件并解析全名(路径 + 文件名 + 扩展名),然后导入 CSV 获取每个文件并创建一个 object 然后每个 object 得到合并成一个只有一个 header.
的 CSV 文件