Windows PowerShellでフォルダ内の重複するファイルを調べる
前の記事では(私の想像で) 人間が手作業している日常業務を自動化する例としてWindows PowerShellでExcelファイルを編集すると言う内容を書きましたが、余りPowerShellっぽさがないスクリプトになってしまったので、今度はPowerShellっぽいスクリプトになるようにフォルダ内の重複するファイルを調べてみます
とりあえず今回のサンプルコードとしてはピクチャフォルダ配下のファイルを調べることにします
まずはファイルの一覧を取ってみましょう
Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force
はい、取れました
ピクチャフォルダを[Environment]::GetFolderPath('MyPictures')
で取得してGet-ChildItem
の-Path
に設定し、ファイルだけ欲しいので-File
付けて、子アイテムがフォルダの場合その中身も再帰的に取得するよう-Recurse
付けて、ついでに隠しファイルも取れるよう-Force
も付けました
いままでDOSでバッチを書いていた身からすると、PowerShellは本当に強力です
次に同一ファイルを特定する情報の取得ですが、今回はHashを使う事にします
Hashが何かについてはこの記事では説明しないので、各自ハッシュ関数で検索して下さい
Windows PowerShellではversion 4から標準コマンドレットにGet-FileHash
と言う、ファイルハッシュを取得するその名の通りのコマンドレットが追加されていますので、先ほど取得したファイルリストをパイプ (|
) を使ってGet-FileHash
に流し込んでみます
Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force | Get-FileHash -Algorithm SHA256;
はい、できました
Get-FileHash
はパイプラインから流れてくるSystem.IO.FileSystemInfo
を直接受け取れるので上記のように書けます
ちなみに、-Algorithm
パラメータにSHA256
を与えていますがここはパフォーマンスと相談して好きなアルゴリズムを採用してください (今回のサンプルコードでは悪意のある第三者が改竄したファイルでhashを衝突させてくる心配はほぼないですし)
そして、ユニークではないhashを持つObjectだけ出力したいので、とりあえず今回はSort-Object -Property Hash
でHashでソートした上で直前のObjectのhashと同じかどうかを判定することにします
[String] $prevHash = ''; [PSCustomObject] $prevObject = $null; Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force | Get-FileHash -Algorithm SHA256 | Sort-Object -Property Hash | Foreach-Object {If($_.Hash -eq $prevHash){$prevObject; $_;} $PrevObject=$_;$PrevHash=$_.Hash;}
しかし、これだと一致するファイル A1,A2,A3 があった場合、A1,A2,A2,A3 と、A2が2回出力されてしまいます
そこで、その後Select-Object -Property * -Unique
として重複したObjectを取り除くことにします
[String] $prevHash = ''; [PSCustomObject] $prevObject = $null; Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force | Get-FileHash -Algorithm SHA256 | Sort-Object -Property Hash | Foreach-Object {If($_.Hash -eq $prevHash){$prevObject; $_;} $PrevObject=$_;$PrevHash=$_.Hash;} | Select-Object -Property * -Unique;
以上でピクチャフォルダ配下の重複したファイルが分かるようになりましたが、重複はしているがそれぞれ必要なファイルの場合もありますので、削除や移動はせず、CSVファイルに出力することにします
CSVに出力する場合、Export-CSV
と言うそのままの名前のコマンドレットを使います
[String] $prevHash = ''; [PSCustomObject] $prevObject = $null; [String] $CsvFullName = Join-Path -Path ([Environment]::GetFolderPath('Desktop')) -CHildPath '重複ファイルリスト.csv' Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force | Get-FileHash -Algorithm SHA256 | Sort-Object -Property Hash | Foreach-Object {If($_.Hash -eq $prevHash){$prevObject; $_;} $PrevObject=$_;$PrevHash=$_.Hash;} | Select-Object -Property * -Unique | Export-CSV -Path $CsvFullName -Encoding Default -Force -NoTypeInformation;
ほぼ完成です
ですが、出力されたCSVを開いてみると先頭の列はAlgorithmという、今回は全行同じ値が必ず入っているので要らない情報が有ったり、逆に (同じファイルとはいえ) 最終更新日等があった方がどのファイルを消すかの参考情報になって便利などの微妙に使い勝手が悪いことが気になります
そこで、Get-FileHash
の行を書き換え、素のGet-FileHash
の出力結果ではなく、Select-Object
を使って必要な情報を持ったPSCustomObjectを次のパイプに渡すように変更してみます
[String] $prevHash = ''; [PSCustomObject] $prevObject = $null; [String] $CsvFullName = Join-Path -Path ([Environment]::GetFolderPath('Desktop')) -CHildPath '重複ファイルリスト.csv' Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force | Select-Object -Property Name,Directory,FullName,Length,Extension,LastWriteTime,CreationTime,@{Name='Hash';Expression={(Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash}} | Sort-Object -Property Hash | Foreach-Object {If($_.Hash -eq $prevHash){$prevObject; $_;} $PrevObject=$_;$PrevHash=$_.Hash;} | Select-Object -Property * -Unique | Export-CSV -Path $CsvFullName -Encoding Default -Force -NoTypeInformation;
最後に、このコードをps1ファイルに保存するときにはファイルの先頭に#Requires -Version 4.0
と実行時の最低バージョンの指定を書いておくと良いでしょう
今回のスクリプトではversion 4.0から追加されたGet-FileHash
を使っていますが、2017年12月現在、Version 4.0が入っていない環境は一応残っています (参考: Windows PowerShell のシステム要件 | Microsoft Docs)
そんな訳で最終的にはこんなバッチファイルになりました
#Requires -Version 4.0 [String] $prevHash = ''; [PSCustomObject] $prevObject = $null; [String] $CsvFullName = Join-Path -Path ([Environment]::GetFolderPath('Desktop')) -CHildPath '重複ファイルリスト.csv' Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force | Select-Object -Property Name,Directory,FullName,Length,Extension,LastWriteTime,CreationTime,@{Name='Hash';Expression={(Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash}} | Sort-Object -Property Hash | Foreach-Object {If($_.Hash -eq $prevHash){$prevObject; $_;} $PrevObject=$_;$PrevHash=$_.Hash;} | Select-Object -Property * -Unique | Export-CSV -Path $CsvFullName -Encoding Default -Force -NoTypeInformation;
パイプをどんどん繋げてObjectを流していると、俺、シェル書いてるって気分になって楽しいです