【PowerShell】2つのフォルダを比較して変更されたファイルを見つける|ファイル名・更新日時ではなくハッシュ値で

スポンサーリンク
powershell
スポンサーリンク

「変更前のフォルダ」と「変更後のフォルダ」を比べて、追加・削除・変更されたファイルを洗い出したい。PowerShell なら、各ファイルのハッシュ値を計算して比べるのが確実である。ファイル名や更新日時で比べるやり方もよく紹介されているが、それでは中身の変更を見落とす(理由は後半で実際に試した結果を載せる)。

私は仕事で、事前に用意した資材(設定ファイルなど)が知らない間に変わっていないか、ハッシュ値で確かめている(1つのフォルダのハッシュ値を一覧にする方法はGet-FileHash でフォルダ内のハッシュ値を一括で CSV 出力する記事で紹介した)。この記事では、その一覧同士を比べて差分を出すところまでをまとめる。

2つのフォルダを比較するスクリプト

先頭の2行を、比べたいフォルダのパスに書き換える。$before が変更前のフォルダ、$after が変更後のフォルダである。あとは PowerShell に貼り付けて実行するだけで、管理者権限は不要である。

スクリプトは3つの部分に分かれている。Get-HashList でフォルダ配下の全ファイルの「相対パス」と「ハッシュ値」の一覧を作り、Compare-Object で2つの一覧を比べ、最後に結果を「追加・削除・変更」に読み替えて表示している。

$before = "C:\work\before"
$after  = "C:\work\after"

function Get-HashList($root) {
    $root = (Resolve-Path $root).Path
    Get-ChildItem -Path $root -Recurse -File | ForEach-Object {
        [PSCustomObject]@{
            RelativePath = $_.FullName.Remove(0, $root.Length).TrimStart('\')
            Hash         = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash
        }
    }
}

$a = Get-HashList $before
$b = Get-HashList $after
$diff = Compare-Object -ReferenceObject $a -DifferenceObject $b -Property RelativePath, Hash

$result = $diff | Group-Object RelativePath | ForEach-Object {
    $sides = $_.Group.SideIndicator
    if ($sides -contains '<=' -and $sides -contains '=>') { $state = '変更' }
    elseif ($sides -contains '<=') { $state = '削除' }
    else { $state = '追加' }
    [PSCustomObject]@{ 状態 = $state; ファイル = $_.Name }
}
$result | Sort-Object ファイル | Format-Table -AutoSize

実行すると、違いのあったファイルだけが次のように表示される(後半のテスト用フォルダで実行した結果)。

状態 ファイル
---- --------
追加 new.txt
削除 old.txt
変更 settings.ini

何も表示されなければ、2つのフォルダの中身は完全に同じである。

結果の見方

状態 意味
変更 両方のフォルダにあるが、中身が違う
削除 変更前($before)にだけある
追加 変更後($after)にだけある

サブフォルダの中のファイルは、sub\data.csv のようにフォルダからの相対パスで表示される。相対パスで比べているので、2つのフォルダが別の場所にあっても、同じ位置にあるファイル同士が比べられる。

結果を CSV に保存する

記録に残したい場合は、続けて次を実行する。出力先のパスは好きな場所に書き換えてよい。

$result | Sort-Object ファイル | Export-Csv "C:\work\diff.csv" -NoTypeInformation -Encoding Default
"状態","ファイル"
"追加","new.txt"
"削除","old.txt"
"変更","settings.ini"

-Encoding Default は、Windows PowerShell 5.1 では Shift-JIS になり、Excel で開いても文字化けしない。PowerShell 7 では意味が変わる(UTF-8 になる)ので、PowerShell 7 で Excel 向けに出すなら -Encoding utf8BOM を指定する。

ファイル名や更新日時で比べてはいけない理由(実際に試した)

フォルダの比較を調べると、Compare-Object でファイル名や更新日時を比べる方法がよく出てくる。手軽だが、中身が同じかどうかの判定には使えない。テスト用のフォルダを作って、実際に確かめた。

テスト用のフォルダ

「変更前(before)」と「変更後(after)」に、わざと次の違いを入れた。

ファイル before と after の違い
config.txt 中身は同じ。更新日時だけ違う
settings.ini 中身だけ違う(timeout=30 → timeout=60)。サイズと更新日時は同じ
old.txt before にだけある(削除された)
new.txt after にだけある(追加された)
sub\data.csv まったく同じ

同じ状態は次のコマンドで作れる(%TEMP% の下に作る)。自分の手元で試したい場合に使ってほしい。

$root = "$env:TEMP\hashtest"
New-Item "$root\before\sub", "$root\after\sub" -ItemType Directory -Force | Out-Null
Set-Content "$root\before\config.txt"   "server=app01"
Set-Content "$root\after\config.txt"    "server=app01"
Set-Content "$root\before\settings.ini" "timeout=30"
Set-Content "$root\after\settings.ini"  "timeout=60"
Set-Content "$root\before\old.txt"      "old file"
Set-Content "$root\after\new.txt"       "new file"
Set-Content "$root\before\sub\data.csv" "id,name"
Set-Content "$root\after\sub\data.csv"  "id,name"
Get-ChildItem $root -Recurse -File | ForEach-Object { $_.LastWriteTime = [datetime]'2026-11-01 10:00' }
(Get-Item "$root\after\config.txt").LastWriteTime = [datetime]'2026-11-02 10:00'
$before = "$root\before"
$after  = "$root\after"

できたファイルは次のとおり。settings.ini は before も after もサイズ 12、更新日時も同じで、一覧を見ただけでは区別がつかない。

FullName                                                        Length LastWriteTime
--------                                                        ------ -------------
C:\Users\<User>\AppData\Local\Temp\hashtest\after\config.txt        14 2026/11/02 10:00:00
C:\Users\<User>\AppData\Local\Temp\hashtest\after\new.txt           10 2026/11/01 10:00:00
C:\Users\<User>\AppData\Local\Temp\hashtest\after\settings.ini      12 2026/11/01 10:00:00
C:\Users\<User>\AppData\Local\Temp\hashtest\after\sub\data.csv       9 2026/11/01 10:00:00
C:\Users\<User>\AppData\Local\Temp\hashtest\before\config.txt       14 2026/11/01 10:00:00
C:\Users\<User>\AppData\Local\Temp\hashtest\before\old.txt          10 2026/11/01 10:00:00
C:\Users\<User>\AppData\Local\Temp\hashtest\before\settings.ini     12 2026/11/01 10:00:00
C:\Users\<User>\AppData\Local\Temp\hashtest\before\sub\data.csv      9 2026/11/01 10:00:00

ファイル名で比べた場合: 中身の変更を見落とす

Compare-Object (Get-ChildItem $before -Recurse -File) (Get-ChildItem $after -Recurse -File) -Property Name
Name    SideIndicator
----    -------------
new.txt =>
old.txt <=

追加と削除は分かるが、中身が変わった settings.ini は出てこない。名前が同じだからである。

名前・サイズ・更新日時で比べた場合: 誤判定と見落としが出る

Compare-Object (Get-ChildItem $before -Recurse -File) (Get-ChildItem $after -Recurse -File) -Property Name, Length, LastWriteTime
Name       Length LastWriteTime       SideIndicator
----       ------ -------------       -------------
config.txt     14 2026/11/02 10:00:00 =>
new.txt        10 2026/11/01 10:00:00 =>
config.txt     14 2026/11/01 10:00:00 <=
old.txt        10 2026/11/01 10:00:00 <=

中身が同じ config.txt が「違う」と出て、本当に変わった settings.ini はやはり出てこない。更新日時は、開いて保存し直しただけでも、コピーの仕方によっても変わる。中身を書き換えてもサイズが同じなら、サイズでは気づけない。

ハッシュ値で比べた場合: 正しく判定できる

冒頭のスクリプトのうち、2つの一覧を比べたところまで($diff)の中身はこうなっていた。

RelativePath Hash                                                             SideIndicator
------------ ----                                                             -------------
new.txt      CDBA66F93B029372B55EF8112536AED0829A14D70B294C1A9E33A49ED4AD757A =>
settings.ini 8644FF8EFA542A1470E8CBFBF39AE90F6025E542F65D8A61FC1396630E635F26 =>
old.txt      9849BF57C781A24E5CDF242DFDE29370C69C0A5E3C83AB03A1E43FF4ABC55B4D <=
settings.ini B75039373646B8C569AFFEFE71AA79F6E824CFC4D581F2BABA62C4FC037A624B <=

SideIndicator の <= は before にだけ、=> は after にだけある、という意味である。settings.ini はハッシュ値が違うので両側に1行ずつ出ている。更新日時だけ違う config.txt と、まったく同じ sub\data.csv は出てこない。冒頭のスクリプトは、これをファイルごとにまとめて「変更・削除・追加」に読み替えている。

試し終わったら、テスト用のフォルダは消しておく。

Remove-Item "$env:TEMP\hashtest" -Recurse -Force

使うときの注意

  • ファイル数やサイズが大きいと時間がかかる。ハッシュ値は全ファイルを最後まで読んで計算するためだ。追加・削除だけ先に知りたいなら、ファイル名での比較で当たりを付けてから、中身の確認を必要なフォルダに絞るのも手である
  • 変更前の一覧を残しておけば、後から比べられる。資材を配る前に Get-HashList の結果を CSV に保存しておけば、後で今の状態と比べて、誰かが中身を書き換えていないか確かめられる
  • NAS へのバックアップが正しくコピーされたかの確認にも使える(バックアップの方法はRobocopy で NAS にバックアップする記事を参照)
  • 今回試したのはローカルのフォルダである
  • スクリプトを .ps1 ファイルに保存して実行するときに「このシステムではスクリプトの実行が無効になっているため」と出たら、PowerShell の実行ポリシーの記事を見てほしい

この記事のまとめ

この記事のまとめ
  • 2つのフォルダの中身を確実に比べるなら、相対パスとハッシュ値の一覧を作って Compare-Object で比べる
  • 結果は、両側に出たら「変更」、変更前だけなら「削除」、変更後だけなら「追加」
  • ファイル名の比較は中身の変更を見落とし、更新日時やサイズの比較は誤判定と見落としが出る

参考

コメント

タイトルとURLをコピーしました