【bat】バッチファイルでCSVを結合する方法完全ガイド|ヘッダースキップ・文字コード・サブフォルダ再帰・PowerShell・実践パターンまで

【bat】バッチファイルでCSVを結合する方法完全ガイド|ヘッダースキップ・文字コード・サブフォルダ再帰・PowerShell・実践パターンまで bat

「月次レポートのCSVが担当者ごとに分かれていて、毎月手作業で結合している」「サーバー上でCSVを自動集計したいけどExcelは使えない」——そんな場面でバッチファイル(bat)は強力な武器になります。

この記事では、typeコマンドによる最速結合からヘッダースキップ・再帰検索・文字コード対応まで、実務で使える5つの方法と3本の実践例を徹底解説します。

この記事でできること

  • 同フォルダ内の全CSVを1コマンドで結合する
  • 2ファイル目以降のヘッダー行を自動スキップする
  • サブフォルダを再帰的にたどってCSVを収集・結合する
  • PowerShellでShift-JIS/UTF-8を統一しながら結合する
  • 月次・日次の自動結合バッチを実装する
  • 出力ファイルが入力に混入するバグなど落とし穴5つを回避する
スポンサーリンク

方法の比較と選び方

まず全方法の特徴を把握してから、用途に合ったものを選んでください。

方法 ヘッダースキップ 再帰検索 文字コード対応 向き不向き
type コマンド ×(全コピー) × ヘッダーなし or 確認済みの1回限り
for + more +1 × 同フォルダの定期結合・軽量スクリプト
for /f + skip ○(厳密) × 空行ありCSVを除く通常データ
for /r 再帰 複数フォルダからCSVを収集
PowerShell ○(完全) エンコーディング混在・大容量ファイル

方法1: typeコマンド(最速・ヘッダー重複あり)

ヘッダーが不要な場合、または全ファイルのヘッダー重複を後処理で除去できる場合の最速手段です。

@echo off
REM 同フォルダ内の全CSVをそのまま結合(ヘッダー行も全コピー)
type *.csv > merged_all.csv
echo 結合完了: merged_all.csv

typeコマンドの特徴

  • コマンド1行で結合できる最速の方法
  • ヘッダー行も全ファイル分コピーされる(データ結合用途では後で除去が必要)
  • 出力ファイル名を merged_all.csv のように既存CSVと重複しない名前にすること

方法2: for + more +1(ヘッダースキップ・基本形)

最もよく使われる方法です。最初のファイルのヘッダーだけを残し、2ファイル目以降は1行目をスキップします。

@echo off
setlocal enabledelayedexpansion

set "OUTPUT=merged.csv"

REM 出力ファイルを事前削除(前回の残骸を防ぐ)
if exist "%OUTPUT%" del "%OUTPUT%"

set "FIRST=1"

for %%f in (*.csv) do (
    REM 出力ファイル自身を入力に含めない
    if /i "%%f" NEQ "%OUTPUT%" (
        if "!FIRST!"=="1" (
            REM 最初のファイル: ヘッダーごとコピー
            copy "%%f" "%OUTPUT%" > nul
            set "FIRST=0"
            echo [1st] %%f
        ) else (
            REM 2ファイル目以降: 1行目(ヘッダー)をスキップ
            more +1 "%%f" >> "%OUTPUT%"
            echo [add] %%f
        )
    )
)

echo.
echo 結合完了: %OUTPUT%

more +1 の注意点

more +1 は末尾に余分な改行を付加することがあります。データ行数が少ないファイルや末尾が空行で終わるCSVでは問題になる場合があります。厳密な制御が必要な場合は方法3またはPowerShellを使ってください。

方法3: for /f + skip=1(ヘッダーを厳密にスキップ)

more +1 の代わりに for /f skip=1 でヘッダー行番号を管理する方法です。注意点:for /f は空行を自動で読み飛ばします。空のセルだけの行(,, など)は残りますが、完全な空行は消えます。

@echo off
setlocal enabledelayedexpansion

set "OUTPUT=merged.csv"
if exist "%OUTPUT%" del "%OUTPUT%"
set "FIRST=1"

for %%f in (*.csv) do (
    if /i "%%f" NEQ "%OUTPUT%" (
        if "!FIRST!"=="1" (
            copy "%%f" "%OUTPUT%" > nul
            set "FIRST=0"
            echo [1st] %%f
        ) else (
            REM skip=1 で最初の1行(ヘッダー)を飛ばす
            for /f "usebackq skip=1 delims=" %%L in ("%%f") do (
                echo %%L >> "%OUTPUT%"
            )
            echo [add] %%f
        )
    )
)

echo 結合完了: %OUTPUT%

usebackq とは

for /f "usebackq" を付けると、ファイル名をダブルクォートで囲めます(スペース含むパスに対応)。省略するとスペース入りパスでエラーになります。

方法4: for /r でサブフォルダを再帰検索して結合

複数のサブフォルダに散らばったCSVを一気に収集して結合します。

@echo off
setlocal enabledelayedexpansion

REM 結合先は別フォルダに置いて入力に混入しないようにする
set "OUTPUT_DIR=C:\work\output"
set "OUTPUT=%OUTPUT_DIR%\merged.csv"

if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
if exist "%OUTPUT%" del "%OUTPUT%"

set "FIRST=1"

REM カレントフォルダ以下を再帰的に検索
for /r . %%f in (*.csv) do (
    REM %%~ff = %%f のフルパス展開(~f 修飾子)。出力ファイルと完全一致で除外
    if /i "%%~ff" NEQ "%OUTPUT%" (
        if "!FIRST!"=="1" (
            copy "%%f" "%OUTPUT%" > nul
            set "FIRST=0"
            echo [1st] %%f
        ) else (
            more +1 "%%f" >> "%OUTPUT%"
            echo [add] %%f
        )
    )
)

if "!FIRST!"=="1" (
    echo エラー: CSVファイルが見つかりませんでした
    exit /b 1
)
echo 結合完了: %OUTPUT%

出力先を別フォルダに分けることで「出力ファイルが入力に混入する」バグを確実に防げます。

方法5: PowerShell(文字コード・BOM・大容量対応)

Shift-JIS と UTF-8 が混在している場合や、大容量CSVには PowerShell が最適です。バッチファイルから呼び出せます。

どのくらいのサイズからPowerShellを使うべき?

  • type / more +1: 数十MB・数万行まで安定。日常的な定期結合に最適
  • for /f: 数万行超になると echo %%L >> file の繰り返しでI/Oが遅くなる
  • PowerShell: 100万行超・複数ファイル合計100MB超・エンコーディング変換が必要な場合に推奨

UTF-8統一で結合(BOMなし)

@echo off
powershell -NoProfile -ExecutionPolicy Bypass -Command "^
  $out = New-Object System.IO.StreamWriter('merged.csv', $false,
    [System.Text.Encoding]::UTF8); ^
  $first = $true; ^
  Get-ChildItem -Filter *.csv ^
    | Where-Object { $_.Name -ne 'merged.csv' } ^
    | ForEach-Object { ^
        $lines = Get-Content $_.FullName -Encoding UTF8; ^
        if ($first) { ^
          $lines | ForEach-Object { $out.WriteLine($_) }; ^
          $first = $false ^
        } else { ^
          $lines | Select-Object -Skip 1 ^
            | ForEach-Object { $out.WriteLine($_) } ^
        } ^
      }; ^
  $out.Close() ^
"
echo 結合完了: merged.csv

Shift-JISファイルをUTF-8に変換しながら結合

@echo off
powershell -NoProfile -ExecutionPolicy Bypass -Command "^
  $sjis = [System.Text.Encoding]::GetEncoding('shift_jis'); ^
  $utf8 = New-Object System.Text.UTF8Encoding($false); ^
  $out = New-Object System.IO.StreamWriter('merged_utf8.csv', $false, $utf8); ^
  $first = $true; ^
  Get-ChildItem -Filter *.csv ^
    | Where-Object { $_.Name -ne 'merged_utf8.csv' } ^
    | ForEach-Object { ^
        $lines = [System.IO.File]::ReadAllLines($_.FullName, $sjis); ^
        if ($first) { ^
          $lines | ForEach-Object { $out.WriteLine($_) }; ^
          $first = $false ^
        } else { ^
          $lines | Select-Object -Skip 1 ^
            | ForEach-Object { $out.WriteLine($_) } ^
        } ^
      }; ^
  $out.Close() ^
"
echo 変換・結合完了: merged_utf8.csv

StreamWriter を使う理由

Out-FileAdd-Content はBOM付きUTF-8を出力することがあります。StreamWriterUTF8Encoding($false)(BOMなし)を渡すことで、ExcelやDBインポートで文字化けしないUTF-8ファイルを生成できます。

実践例A: 月次売上CSVの自動結合(年月フォルダ対応)

年月ごとのフォルダ(例: 2025\01\)に保存された売上CSVを、年単位で1ファイルに結合する例です。

@echo off
setlocal enabledelayedexpansion

REM 対象年を引数で受け取る(例: merge_sales.bat 2025)
set "YEAR=%~1"
if "%YEAR%"=="" set "YEAR=2025"

set "BASE_DIR=C:\sales\%YEAR%"
set "OUTPUT=C:\sales\%YEAR%_all.csv"

if not exist "%BASE_DIR%" (
    echo エラー: フォルダが見つかりません: %BASE_DIR%
    exit /b 1
)
if exist "%OUTPUT%" del "%OUTPUT%"

set "FIRST=1"
set "FILE_COUNT=0"

REM 月フォルダを順に処理(01~12の順序保証)
for /l %%m in (1,1,12) do (
    REM 月を2桁ゼロ埋めに変換
    set "MM=0%%m"
    set "MM=!MM:~-2!"

    set "MONTH_DIR=%BASE_DIR%\!MM!"
    if exist "!MONTH_DIR!" (
        for %%f in ("!MONTH_DIR!\*.csv") do (
            if "!FIRST!"=="1" (
                copy "%%f" "%OUTPUT%" > nul
                set "FIRST=0"
                echo [1st] %%f
            ) else (
                more +1 "%%f" >> "%OUTPUT%"
                echo [add] %%f
            )
            set /a "FILE_COUNT+=1"
        )
    )
)

if "%FILE_COUNT%"=="0" (
    echo CSVファイルが見つかりませんでした
    exit /b 1
)
echo.
echo 結合完了: %FILE_COUNT% ファイル -> %OUTPUT%

月番号を for /l でループすることで、ファイルシステムの列挙順に依存せず01→02→…→12 の時系列順に結合できます。

なぜ for %%f in (*.csv) でまとめて処理しないのか?

for %%f in (2025\01\*.csv 2025\02\*.csv) と書いても、フォルダをまたぐ列挙順はファイルシステム(NTFS)の実装依存で保証されません。また一部フォルダが存在しない月(欠損)でもループが止まらないよう、for /lif exist の組み合わせで堅牢に処理しています。

実践例B: 複数の入力フォルダからCSVを収集して結合

拠点ごと・担当者ごとに分かれたフォルダから、特定パターンのCSVだけを収集して結合します。

@echo off
setlocal enabledelayedexpansion

REM 入力フォルダリストをスペース区切りで定義
set "DIRS=C:\data\tokyo C:\data\osaka C:\data\nagoya"

set "OUTPUT=C:\data\combined\all_report.csv"
set "OUTPUT_DIR=C:\data\combined"

if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
if exist "%OUTPUT%" del "%OUTPUT%"

set "FIRST=1"
set "TOTAL=0"

for %%d in (%DIRS%) do (
    if exist "%%d" (
        for %%f in ("%%d\report_*.csv") do (
            if "!FIRST!"=="1" (
                copy "%%f" "%OUTPUT%" > nul
                set "FIRST=0"
            ) else (
                more +1 "%%f" >> "%OUTPUT%"
            )
            set /a "TOTAL+=1"
            echo  処理: %%f
        )
    ) else (
        echo [警告] フォルダなし: %%d
    )
)

echo.
echo 結合完了: %TOTAL% ファイル -> %OUTPUT%

実践例C: 日次バッチ自動実行(タスクスケジューラ連携)

毎朝前日分のCSVを自動結合してログを残す、運用向けスクリプトです。

@echo off
setlocal enabledelayedexpansion

set "LOG_DIR=C:\logs\csv_merge"
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"

REM PowerShellでロケール非依存のタイムスタンプ取得
for /f %%t in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd_HHmmss"') do set "TS=%%t"

set "LOG=%LOG_DIR%\merge_%TS%.log"
echo [%TS%] CSV結合開始 >> "%LOG%"

REM 前日の日付フォルダを特定
for /f %%d in ('powershell -NoProfile -Command "(Get-Date).AddDays(-1).ToString(''yyyyMMdd'')"') do set "YESTERDAY=%%d"

set "SRC_DIR=C:\data\daily\%YESTERDAY%"
set "OUTPUT=C:\data\merged\%YESTERDAY%_merged.csv"

if not exist "%SRC_DIR%" (
    echo [ERROR] フォルダなし: %SRC_DIR% >> "%LOG%"
    exit /b 1
)

if exist "%OUTPUT%" del "%OUTPUT%"
set "FIRST=1"

for %%f in ("%SRC_DIR%\*.csv") do (
    if "!FIRST!"=="1" (
        copy "%%f" "%OUTPUT%" > nul
        set "FIRST=0"
    ) else (
        more +1 "%%f" >> "%OUTPUT%"
    )
    echo  [OK] %%f >> "%LOG%"
)

echo [%TS%] 完了: %OUTPUT% >> "%LOG%"
echo 結合完了

タスクスケジューラ連携や実行ログの詳細はバッチファイルで実行ログを出力する方法も参照してください。

落とし穴と対策

落とし穴1: 出力ファイルが入力の *.csv に混入する

CMDの for %%f in (*.csv) はループ開始時にファイルリストを確定します。しかし 出力ファイルがループ開始前に既に存在していた場合、そのファイルも入力リストに含まれ、自分自身の古い内容が末尾に追記されてファイルが肥大化します。

REM BAD: merged.csv がループ開始前に存在していると自分自身の内容も追記される
for %%f in (*.csv) do type "%%f" >> merged.csv

REM GOOD: 出力ファイルを明示的に除外
for %%f in (*.csv) do (
    if /i "%%f" NEQ "merged.csv" type "%%f" >> merged.csv
)

REM BEST: 出力先を別フォルダに分ける
for %%f in (*.csv) do more +1 "%%f" >> "C:\output\merged.csv"

落とし穴2: 文字コードが混在するとデータが壊れる

Shift-JIS(cp932)のファイルとUTF-8のファイルを typemore で結合すると文字化けが発生します。文字コードが統一されているか事前に確認するか、方法5(PowerShell)でエンコーディングを指定して結合してください。

落とし穴3: UTF-8 with BOM ファイルをバイナリ結合するとBOMが中間に入る

UTF-8 BOM付きファイル(先頭3バイト EF BB BF)を type で結合すると、2ファイル目以降のBOMがデータの途中に混入します。ExcelやDBインポートが誤動作します。PowerShellの StreamWriter でBOMなしUTF-8に統一して結合してください。

落とし穴4: for /f は完全な空行を読み飛ばす

CSVに完全な空行が含まれる場合、for /f はその行をスキップします。空行が重要なデータ区切りとして使われているCSVには more +1 またはPowerShellを使ってください。

REM for /f は空行をスキップする(↓空行が消える)
for /f "usebackq skip=1 delims=" %%L in ("data.csv") do echo %%L

REM more +1 は空行を保持する(↓空行もそのまま出力)
more +1 "data.csv" >> merged.csv

落とし穴5: ヘッダー行が複数行あるCSV

一部のCSVは1行目にタイトル・2行目に列ヘッダー・3行目からデータという構造になっています。この場合 more +1 では不十分で、2行分スキップする必要があります。

REM more +2 で最初の2行をスキップ
more +2 "data.csv" >> merged.csv

REM for /f でも skip=2 で2行スキップ可能
for /f "usebackq skip=2 delims=" %%L in ("data.csv") do echo %%L >> merged.csv

よくある質問(FAQ)

結合後のファイルをExcelで開くと文字化けします
Excelは日本語環境でCSVをShift-JISと判断します。UTF-8で保存されたCSVをExcelで正しく開くには、①BOM付きUTF-8で保存する(UTF8Encoding($true))か、②ExcelのデータタブからCSVをインポート時にエンコーディングを指定してください。
CSVの列数が異なるファイルを結合できますか?
バッチファイル自体は列数チェックをしないので結合はできますが、列がずれたまま結合されます。列を揃えてから結合したい場合はPowerShellで各行のフィールド数を検証するか、Pythonなどの高水準言語を使うことをお勧めします。
結合結果に重複行がある場合、除去できますか?

sort /unique オプションで重複行を除去できます。ただしソートも同時に行われます。ヘッダーを除いた行だけ重複除去したい場合は以下の手順を使ってください。

@echo off
REM 1. ヘッダーを別ファイルに保存
for /f "usebackq delims=" %%L in ("merged.csv") do (
    echo %%L > header.tmp
    goto :next
)
:next

REM 2. ヘッダーをスキップしてデータ行をソート・重複除去
more +1 "merged.csv" | sort /unique > data_sorted.tmp

REM 3. ヘッダー + 重複除去済みデータを結合
copy /b header.tmp + data_sorted.tmp merged_unique.csv
del header.tmp data_sorted.tmp
echo 重複除去完了: merged_unique.csv

詳細は テキストファイルをソートする方法完全ガイドを参照してください。

結合前にCSVファイルの存在チェックをしたい
以下のように dir /b *.csv 2>nul で事前確認できます。

dir /b *.csv > nul 2>&1
if errorlevel 1 (
    echo エラー: CSVファイルが見つかりません
    exit /b 1
)
結合するファイルの順番を制御できますか?
for %%f in (*.csv) の列挙順はファイルシステム依存で保証されません。順番を保証するには、①ファイル名に 001_ のような連番プレフィックスを付ける、②実践例Aのように for /l で月番号をループして各フォルダを順に処理する、③ファイルリストをテキストで管理して for /f で順番に処理する、のいずれかを使ってください。
CSVを読み込んで特定列だけを結合したい
for /f "tokens=1,3 delims=," で列を選択してから結合できます。詳細な列操作は バッチファイルでCSVファイルを読み込む方法を参照してください。

まとめ

バッチファイルによるCSV結合の方法を5つ紹介しました。

用途 推奨方法
最速・1回限りの結合 方法1: typeコマンド
定期結合・ヘッダースキップ 方法2: for + more +1
ヘッダー厳密スキップ 方法3: for /f + skip=1
複数フォルダから収集 方法4: for /r 再帰
文字コード変換・大容量 方法5: PowerShell

CSVのマージ(bat csv マージ)にはこの記事の方法が最適です。逆にCSVを分割したい場合はCSVファイルを分割する方法完全ガイドもあわせてご覧ください。