PowerShellでフォルダー差分をハッシュ比較する方法 実用スクリプトと失敗しない手順

PowerShellでフォルダー差分をハッシュ比較したいなら、Get-ChildItem で比較対象のファイルを再帰的に集め、Get-FileHash で内容のハッシュを取り、Compare-Object または相対パス基準の突合で差分を見るのが基本です。迷いやすいのは「何をキーに比べるか」ですが、実務では 絶対パスではなく相対パス + ハッシュ で揃えるのが正解です。これで、追加・削除・内容変更を迷わず切り分けられます。(Microsoft Learn)

Get-FileHash はファイル内容に対してハッシュを計算し、既定値は SHA256 です。ファイル名や拡張子が変わっても内容が同じならハッシュは同じなので、バックアップや移行後の整合性確認に向いています。アルゴリズムに迷ったらまず SHA256 のままで問題なく、MD5 や SHA1 は簡易な変更確認に限るほうが安全です。(Microsoft Learn)

目次

PowerShellでフォルダー差分をハッシュ比較すると何が分かるか

フォルダー差分の実務では、「片方にしかないファイル」と「同じ場所にあるが中身が違うファイル」を分けて見られることが重要です。Compare-Object はオブジェクト全体ではなく、指定したプロパティだけで比較できるため、RelativePath と Hash を揃えて比べると判断しやすくなります。(Microsoft Learn)

差分の種類判定方法実務での意味
追加片方にしか相対パスがない新規作成、同期漏れ
削除逆側にしか相対パスがない消し忘れ、整理済みファイル
内容変更同じ相対パスでハッシュが違う上書き漏れ、更新差分
一致同じ相対パスでハッシュも同じ内容は同一

まずは最短で差分を見る

最短で確認するなら、存在チェックを Test-Path で行い、Get-ChildItem -File -Recurse でファイルだけを拾い、Get-FileHash -LiteralPath でハッシュを取り、Compare-Object -Property RelativePath, Hash で比較します。パスに [ や * などが含まれるとワイルドカード解釈で事故りやすいので、比較元・比較先とも -LiteralPath を使うほうが安全です。Test-Path は -PathType Container でフォルダー実在確認ができ、LiteralPath は入力された文字列をそのまま扱います。(Microsoft Learn)

下の例は、隠しファイルやシステムファイルも含めて比較漏れを減らすために -Force を付けています。不要なら外してください。-Force は hidden / system を拾えますが、権限不足そのものを突破するわけではありません。(Microsoft Learn)

$source = 'C:\Data\Source'
$destination = 'D:\Data\Backup'

function Get-FolderHashList {
    param(
        [Parameter(Mandatory)]
        [string]$Root,

        [string]$Algorithm = 'SHA256'
    )

    $rootPath = Convert-Path -LiteralPath $Root

    Get-ChildItem -LiteralPath $rootPath -File -Recurse -Force | ForEach-Object {
        [pscustomobject]@{
            RelativePath = $_.FullName.Substring($rootPath.Length).TrimStart('\')
            Hash         = (Get-FileHash -LiteralPath $_.FullName -Algorithm $Algorithm).Hash
        }
    }
}

if (-not (Test-Path -LiteralPath $source -PathType Container)) {
    throw "比較元フォルダーが見つかりません: $source"
}

if (-not (Test-Path -LiteralPath $destination -PathType Container)) {
    throw "比較先フォルダーが見つかりません: $destination"
}

$left  = @(Get-FolderHashList -Root $source)
$right = @(Get-FolderHashList -Root $destination)

Compare-Object -ReferenceObject $left -DifferenceObject $right -Property RelativePath, Hash |
    Sort-Object RelativePath, SideIndicator |
    Format-Table -AutoSize

Compare-Object の <= は比較元だけにある値、=> は比較先だけにある値です。何も出なければ差分なしです。同じ RelativePath が <= と => で1回ずつ出る場合は、「同じ場所にあるがハッシュが違う」つまり内容変更と読めます。なお、片方のフォルダーが空だと $null が混ざりやすく、Compare-Object は $null を渡されるとエラーになるため、@(...) で必ず配列化している点も実務では重要です。(Microsoft Learn)

実務では1行1レコードの結果に整える

Compare-Object は最短確認に便利ですが、変更ファイルが2行に分かれるので、コピーや削除の判断には少し読みづらくなります。運用では SourceOnly / DestinationOnly / Modified に整形し、CSV に落としてレビューできる形にしておくと失敗しにくくなります。

$source = 'C:\Data\Source'
$destination = 'D:\Data\Backup'
$algorithm = 'SHA256'
$reportPath = '.\folder-diff.csv'

function Get-FolderHashList {
    param(
        [Parameter(Mandatory)]
        [string]$Root,

        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5')]
        [string]$Algorithm = 'SHA256'
    )

    if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
        throw "フォルダーが見つかりません: $Root"
    }

    $rootPath = Convert-Path -LiteralPath $Root

    foreach ($file in Get-ChildItem -LiteralPath $rootPath -File -Recurse -Force) {
        $relativePath = $file.FullName.Substring($rootPath.Length).TrimStart('\')

        try {
            $hash = (Get-FileHash -LiteralPath $file.FullName -Algorithm $Algorithm -ErrorAction Stop).Hash

            [pscustomobject]@{
                RelativePath = $relativePath
                Length       = $file.Length
                Hash         = $hash
                FullName     = $file.FullName
            }
        }
        catch {
            Write-Warning "ハッシュ計算に失敗しました: $($file.FullName) / $($_.Exception.Message)"
        }
    }
}

$left  = @(Get-FolderHashList -Root $source -Algorithm $algorithm)
$right = @(Get-FolderHashList -Root $destination -Algorithm $algorithm)

$leftIndex = @{}
$rightIndex = @{}

foreach ($item in $left) {
    $leftIndex[$item.RelativePath] = $item
}

foreach ($item in $right) {
    $rightIndex[$item.RelativePath] = $item
}

$allPaths = ($leftIndex.Keys + $rightIndex.Keys) | Sort-Object -Unique

$result = foreach ($path in $allPaths) {
    $l = $leftIndex[$path]
    $r = $rightIndex[$path]

    if ($null -eq $l) {
        [pscustomobject]@{
            Status            = 'DestinationOnly'
            RelativePath      = $path
            SourceLength      = $null
            DestinationLength = $r.Length
            SourceHash        = $null
            DestinationHash   = $r.Hash
        }
        continue
    }

    if ($null -eq $r) {
        [pscustomobject]@{
            Status            = 'SourceOnly'
            RelativePath      = $path
            SourceLength      = $l.Length
            DestinationLength = $null
            SourceHash        = $l.Hash
            DestinationHash   = $null
        }
        continue
    }

    if ($l.Hash -ne $r.Hash) {
        [pscustomobject]@{
            Status            = 'Modified'
            RelativePath      = $path
            SourceLength      = $l.Length
            DestinationLength = $r.Length
            SourceHash        = $l.Hash
            DestinationHash   = $r.Hash
        }
    }
}

if ($result) {
    $result | Sort-Object Status, RelativePath |
        Format-Table Status, RelativePath, SourceLength, DestinationLength -AutoSize

    $result | Export-Csv -LiteralPath $reportPath -NoTypeInformation -Encoding UTF8

    "`n件数サマリー"
    $result | Group-Object Status | Sort-Object Name |
        Select-Object Name, Count |
        Format-Table -AutoSize

    "`nCSVを出力しました: $reportPath"
}
else {
    "差分はありません。"
}

この形なら、コンソールでは「何をどう直すべきか」をすぐ読め、CSV では詳細なハッシュも追えます。バックアップの検証なら SourceOnly は未コピー、DestinationOnly は余剰ファイル、Modified は再コピー候補として見ればまず迷いません。警告が出た場合は、そのファイルは未比較と考えてください。

Status意味まずやること
SourceOnly比較元だけにある比較先にコピーすべきか確認する
DestinationOnly比較先だけにある削除対象か、保管対象かを確認する
Modified同じ相対パスだが内容が違う再コピー、上書き、調査の対象にする

失敗しやすいポイント

FullName のまま比較する

比較元が C:\Data\Source、比較先が D:\Backup\Data のようにベースパスが違うと、内容が同じでも絶対パス文字列は必ず違います。Compare-Object は指定したプロパティだけを比べるので、ここで FullName を使うと「全部差分」に見えやすいです。フォルダー差分のハッシュ比較では、必ず相対パスにそろえてから比べてください。(Microsoft Learn)

-Path と -LiteralPath を混同する

PowerShell の多くの Path パラメーターはワイルドカードを受け付けます。-LiteralPath は入力された文字列をそのまま使い、ワイルドカードとして解釈しません。共有フォルダー名やエクスポートファイル名に [] や ? が含まれている環境では、ここを曖昧にすると別物を比較してしまいます。(Microsoft Learn)

隠しファイル・システムファイル・シンボリックリンクの扱いを決めていない

Get-ChildItem は既定では hidden を表示せず、-Force で取得できます。また、再帰時はディレクトリへのシンボリックリンクを表示しても既定ではその先まで辿りません。リンク先まで比較したいなら -FollowSymlink を使いますが、このパラメーターは FileSystem 用で PowerShell 6.0 以降です。結果が思ったより少ないときは、まずここを疑うべきです。(Microsoft Learn)

対象拡張子を絞るのに Where-Object から始める

.xlsx だけ、.csv だけのように対象が決まっているなら、Get-ChildItem の -Filter を使うほうが効率的です。フィルターはプロバイダー側で適用され、PowerShell が取得後に絞り込むより無駄が少なく、利用できるワイルドカードは * と ? です。(Microsoft Learn)

リネームや移動を「内容変更」と誤解する

この記事のやり方は相対パス基準なので、同じ内容のファイルを別フォルダーへ移動した場合は 削除 + 追加 として見えます。リネーム検出まで必要なら、別途 Hash 単位で突き合わせて「同一ハッシュの移動候補」を洗い出してください。

警告を見落として「差分なし」と判断する

Get-FileHash が読めなかったファイルは比較対象から落ちます。アクセス拒否や使用中ファイルがあると、差分ゼロでも完全一致とは言い切れません。-Force は hidden / system の取得には有効ですが、セキュリティ制限は超えられないため、警告が出たら先に権限やロック状態を確認してください。(Microsoft Learn)

大容量フォルダーを速く回すコツ

ハッシュ比較はファイル内容そのものを読みにいくので、ファイル数や総容量が大きいほど時間がかかります。精度を落とさずに少しでも速くしたいなら、対象を最初から減らす設計が効きます。(Microsoft Learn)

  • 比較対象が決まっているなら、最初から -Filter で拡張子を絞る
  • まず相対パスとサイズだけで候補を出し、最後にハッシュで確定する
  • 毎回コンソールだけで終わらせず、CSV に出して再実行時の比較材料を残す

特に「数十万ファイルあるが、本当に重要なのは Office 文書だけ」のような現場では、-Filter の有無で体感差が出やすいです。逆に、サイズが同じでも内容が違うケースは普通にあるので、最終判定だけはハッシュで行うほうが安全です。

戻し方と代替策

この記事のスクリプトは読み取り専用なので、比較だけなら戻し方は不要です。危ないのは、差分結果を見てそのまま削除や上書きまで自動化するケースです。最初は CSV を保存し、人の目で DestinationOnly を確認してから削除し、Modified は再コピー後にもう一度同じスクリプトで再検証する流れが安全です。

作業前の下見だけなら robocopy の /L も便利です。robocopy はファイルコピー用コマンドで、/L を付けると実際にはコピー・削除・タイムスタンプ変更をせず一覧だけを出せます。たとえば次のコマンドなら、どのファイルが対象になりそうかをフルパスとバイト数で先に確認できます。(Microsoft Learn)

robocopy C:\Data\Source D:\Data\Backup /E /L /FP /BYTES

ただし、最終的に「内容まで同じ」と言い切りたいなら、一覧確認だけで終わらせず、最後は Get-FileHash ベースの比較に戻してください。

迷ったらこの順で進める

  1. まず最短スクリプトで、差分がどの程度あるか全体像をつかむ
  2. 次に実務版で CSV を出し、SourceOnly / DestinationOnly / Modified に分類する
  3. 修正後に再実行し、差分ゼロかつ警告なしを確認して完了にする

PowerShellでフォルダー差分をハッシュ比較するときは、相対パスで揃える、-LiteralPath を使う、hidden / symlink / filter の扱いを決めてから回す、この3点で失敗はかなり減ります。バックアップ検証でも移行後チェックでも、まずはこの形をテンプレートにしておけば十分実務投入できます。(Microsoft Learn)

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次