TECHNOLOGY

Windowsで階層フォルダに保存されたファイルを指定した場所に集める方法

カメラの写真

カメラで写真や動画を撮影した場合、日付ごとにフォルダが作成され、その中に写真ファイルや動画ファイルが作成されることがあります。

この作成された写真や動画などを整理して1つのフォルダの中にまとめたい場合に、マウスでフォルダを開いて、中のファイルをコピーして次のフォルダへ進むという作業をするのは、ファイルが多いと大変な作業になります。

この記事では、WindowsのPowershellまたはコマンドプロンプトを用いて、階層状のフォルダの中にある指定した拡張子をもつファイルを、ユーザーが指定したフォルダの中に集めるバッチファイルを作成しましたので紹介します。

目次

  1. 方法
    1. バッチファイル
    2. 仕様
  2. 使い方
  3. コードの説明
  4. まとめ

方法

Windowsのバッチファイルを使用します。

バッチファイルは、複数のコマンドをまとめて実行するためのスプリクトファイルで、任意のテキストエディタで記述した後に拡張子を.batに変えることでバッチファイルになります。

コマンドプロンプトやPowershellで使われるコマンドを記述することができ、バッチファイルを実行するには、通常のファイルと同じようにエクスプローラーでバッチファイルをダブルクリックしたり、コマンドプロンプトやPowershellで実行することができます。

バッチファイル

作成したバッチファイルを以下に示します。

このバッチファイルをメモ帳などにコピーし、保存する際に拡張子を.batとすればバッチファイルが完成します。

@echo off

set "run_cp_or_mv=0"

rem コマンドライン引数の第1引数は移動かコピーかを選択する
if "%~1"=="-m" (
    set "moveflag=1"
) else if "%~1"=="-c" (
    set "moveflag=0"
) else (
    echo Enter -c to copy a file or -m to move a file.
    goto :eof
)
rem コマンドライン引数の第2引数にコピー元のフォルダ名、第3引数にコピー先のフォルダ名を記入する
if "%~2"=="" (
    echo Enter the name of the folder from which you are copying.
    goto :eof
) else (
    set "source_folder=%~2"
)
if "%~3"=="" (
    echo Enter the name of the destination folder.
    goto :eof
) else (
    set "destination_folder=%~3"
)
rem コマンドライン引数の第4引数はコピーするファイルの拡張子をオプションで指定する
if "%~4"=="" (
    set "file_extension=*.*"
) else (
    set "file_extension=%~4"
)

setlocal enabledelayedexpansion

for /R "%source_folder%" %%F in (%file_extension%) do (
rem コピー元のフォルダを含めた階層の浅いフォルダ内のファイルを順に処理する
    set "base_name=%%~nF"
    set "extension=%%~xF"
    set "file_name=%%~nxF"
    set "source_file_size=%%~zF"
    set "source_update_date=%%~tF"
    set "counter=1"
    set "run_cp_or_mv=0"

    if exist "%destination_folder%\%%~nxF" (
    rem 送り先のフォルダに既に同名のファイルがある場合
        call :check_file_name
        if !run_cp_or_mv! equ 1 (
            if  !moveflag! equ 0 (
                copy "%%F" "%%~dpF!new_file_name!" > nul
                echo Copied and renamed %%F to !new_file_name!
            ) else (
                move "%%F" "%%~dpF!new_file_name!" > nul
                echo Moved %%F to !new_file_name!
            )
            move "%%~dpF!new_file_name!" "%destination_folder%\!new_file_name!" > nul
        ) else (
            echo Skipped %%F
            echo [destination: !destination_update_date!  source: !source_update_date!]
        )
    ) else (
        if  !moveflag! equ 0 (
            robocopy "%%~dpF" "%destination_folder%" "%%~nxF" > nul
            echo Copied and renamed %%F to %destination_folder%
        ) else (
            move "%%F" "%destination_folder%\%%~nxF" > nul
            echo Moved %%F to %destination_folder%
        )
    )
)
echo File collection completed.

goto :eof


rem 送り先にすでに同じファイルがある場合は連番を後ろにつけたファイル名を作成
:check_file_name
    set "chk_name=%destination_folder%\!file_name!"

    :loop

    set "next_name=%destination_folder%\!base_name!_!counter!!extension!"
    set "new_file_name=!base_name!_!counter!!extension!"

    rem chk_nameには最初のループはコピーしたいファイルの名前、その後は連番をつけたファイルの名前が設定される
    if exist !chk_name! (
        for %%A in (!chk_name!) do (
            set "destination_file_size=%%~zA"
            set "destination_update_date=%%~tA"
        )
    ) else (
        set "destination_file_size=-1"
    )

    if !source_file_size! equ !destination_file_size! (
    rem ファイルサイズが同じ場合
        if "%source_update_date%" gtr "%destination_update_date%" (
        rem 更新日が新しい場合
            call :counter_increment
        ) 
    ) else (
    rem  ファイルサイズが異なる場合
        call :counter_increment
    )
goto :eof


rem 連番をつけたファイル名がすでに存在する場合はその連番を1つインクリメントする
:counter_increment
    if exist !next_name! (
        set /A counter+=1
        set "chk_name=!next_name!"
        set run_cp_or_mv=0
        goto loop
    ) else (
        set run_cp_or_mv=1
    )
goto :eof

endlocal

仕様

コマンドライン引数で指定したフォルダの階層構造以上にある全てのファイルを、指定した別のフォルダにコピーまたは移動させるバッチファイルです。

オプションとして、対象とするファイルの拡張子を指定できます。

ファイル名が同じファイルがある場合には、ファイルサイズを比較します。ファイルサイズが異なる場合には連番をつけて別ファイルとしてコピーまたは移動させます。

ファイル名とファイルサイズが同じ場合には更新日時を確認します。更新日時が同じか古いファイルならばスキップし、新しいファイルの場合には連番をつけて別ファイルとしてコピーまたは移動させます。

使い方

コマンドラインまたはPowershellで次のコマンドを実行します。バッチファイル名を「fileCollect.bat」とすると、

fileCollect.bat [option] 送信元フォルダ 送信先フォルダ 拡張子

として実行します。オプションには次のいずれかを必ず指定します。

オプション意味
-cファイルを指定フォルダにコピーする
-mファイルを指定フォルダに移動する

大切なファイルを処理する場合はコピーを選択する方が安心です。

「送信元フォルダ」にはコピー(または移動)元のフォルダ名を指定し、「送信先フォルダ」にはコピー(または移動)先のフォルダ名を指定します。

「送信元フォルダ」と「送信先フォルダ」は完全に独立なフォルダにしてください。例えば、「送信元フォルダ」の中に「送信先フォルダ」がある場合などは想定しておりません。

「拡張子」ではファイルの種類を指定できます。拡張子はデフォルトではすべてのファイルを対象とする「*.*」が設定されています。

例えば、テキストファイルのみを移動させたい場合は「*.txt」などとします。デフォルトから変更しない場合には拡張子の指定は省略できます。

以下は、カレントディレクトリにあるフォルダ「Folder1」以上の階層構造に存在する拡張子が.movであるファイルをすべて「Folder2」にコピーする場合の例です。

fileCollect.bat -c ./Folder1 ./Folder2 *.mov

コードの説明

簡単なコードの説明を行います。

for /R "%source_folder%" %%F in (%file_extension%) do (...)

この行は、指定されたフォルダ内のファイルに対してループを実行します。/Rは、forコマンドのオプションの1つで、再帰的にフォルダ内のファイルを探索することを指定します。ここでは%source_folder%以下の階層構造を探索しています。%%Fはループ変数でそれぞれのファイルを表しており、%file_extension%で指定されたファイルの拡張子に合致するファイルを取り出しています(例えば、*.*はすべてのファイルを対象にします。)

    set "base_name=%%~nF"
    set "extension=%%~xF"
    set "file_name=%%~nxF"
    set "source_file_size=%%~zF"
    set "source_update_date=%%~tF"

%%は、forループ変数を参照するためのプレフィックスです。%%Fはループ変数Fを参照することを意味します。

~は変数の修飾子で、変数の値を操作するために使用します。例えば、

C:\Path\to\Source\Folder\file_name.txt

が%%Fに読み込まれたとします。nはファイル名を表す修飾子で、%%~nFはループ変数からファイル名(file_name)を取り出します。xはファイルの拡張子を表す修飾子です。%%~nxFはループ変数からファイル名と拡張子(file_name.txt)を取り出します。同様に、zはファイルサイズを、tは更新日時を取り出しています。

if exist "%destination_folder%\%%~nxF" (
   ...
) else (
   ...
)

この条件文は、移動先またはコピー先のフォルダ%destination_folder%に%%~nxFというファイルがすでに存在しているかどうかを調べています。存在しない場合には、

if  !moveflag! equ 0 (
     robocopy "%%~dpF" "%destination_folder%" "%%~nxF" > nul
     echo Copied and renamed %%F to %destination_folder%
) else (
     move "%%F" "%destination_folder%\%%~nxF" > nul
     echo Moved %%F to %destination_folder%
)

を実行し、移動フラグmoveflagが0の場合はコピーを、それ以外の場合は移動を行います。コピーにはrobocopyコマンド、移動にはmoveコマンドを使用しています。 > nulは結果を標準出力に表示しないことを意味します。なお、%%~dpFはループ変数Fに設定されているファイルのドライブ名とパスを意味します。先ほどの例でいえば、

C:\Path\to\Source\Folder\

の部分を指しています。移動先またはコピー先のフォルダ%destination_folder%に%%~nxFというファイルがすでに存在している場合には、

call :check_file_name
if !run_cp_or_mv! equ 1 (
     if  !moveflag! equ 0 (
          copy "%%F" "%%~dpF!new_file_name!" > nul
          echo Copied and renamed %%F to !new_file_name!
     ) else (
          move "%%F" "%%~dpF!new_file_name!" > nul
          echo Moved %%F to !new_file_name!
     )
     move "%%~dpF!new_file_name!" "%destination_folder%\!new_file_name!" > nul
) else (
     echo Skipped %%F
     echo [destination: !destination_update_date!  source: !source_update_date!]
)

を実行します。関数check_file_nameの中でファイル名につける連番号を調べ、新しいファイルの名前を「元のファイル名_連番号.拡張子」という形とします。この関数では、

if !source_file_size! equ !destination_file_size! (
  rem ファイルサイズが同じ場合
     if "%source_update_date%" gtr "%destination_update_date%" (
     rem 更新日が新しい場合
         call :counter_increment
     ) 
 ) else (
    rem  ファイルサイズが異なる場合
     call :counter_increment
 )

の部分で、コピー元のファイルとコピー先にある同名のファイルのファイルサイズと更新日を比較しています。ファイルサイズが異なる場合やファイルサイズは同じだけど更新日が新しい場合は、連番号をファイル名につけます。そして、コピーまたは移動を行うことを示すフラグrun_cp_or_mvを1に設定します。

関数check_file_nameの処理が終わった後、設定されたそのフラグに基づき、ファイルを移動またはコピーするか、スキップをするかを条件文で判別します。

if !run_cp_or_mv! equ 1 (
  ...
) else (
  ...
)

まとめ

私が旅行などでカメラで撮影した写真や動画ファイルを整理するために作成したバッチファイルと使い方を紹介しました。

階層状のフォルダ内にある全てのファイルを、指定したフォルダにコピーまたは移動させることができます。

また、拡張子の指定やファイル名の重複にも対応しました。

これはバッチファイルを用いた自動化の一例ですが、その他にもいろいろなことを自動化することができますので、よければご活用ください。

-TECHNOLOGY
-