株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
本記事は 「PowerShell 業務自動化レシピ」 シリーズの第3回です。このシリーズは、文法解説ではなく 「1記事=1つの業務課題」を、コピペで動く検証済みスクリプトで解決することを目的とした実務レシピ集です(全12回想定)。SES・受託・社内運用の現場で、いまだに手作業でやっている地味な作業を片っ端から自動化していきます。
第1回 では「大量ファイルの一括リネーム」と「拡張子別の自動フォルダ振り分け」を、第2回 では「古いログファイルの抽出・自動削除」を扱いました。どちらも Get-ChildItem でファイルを集め、-WhatIf で安全確認してから処理する、という流れが共通の軸でした。
第3回のテーマは 「フォルダ構成・ファイル一覧をExcel台帳化」。納品物・共有フォルダの中身を一覧化して「ファイル名・パス・サイズ・更新日時・拡張子」の台帳を作る——資産棚卸し、納品物チェック、容量調査でよくある作業を、Get-ChildItem -Recurse + Export-Csv で自動化します。
本記事は PowerShell 7(pwsh、クロスプラットフォーム) を基準にしています。Windows標準の Windows PowerShell 5.1 での差異は、各所と最後の章で明記します。
今回は 読み取り+CSV出力のみ で、ファイルを書き換えたり消したりはしません(破壊的操作なし)。安心して試せますが、出力先CSVの上書きと、巨大フォルダでの実行時間にだけ軽く注意します(後述)。
Before:手作業のつらいシーン
シーン1:納品物フォルダの「ファイル一覧表」を手で作る
「納品フォルダの中身を一覧表にして提出してください」。エクスプローラーでフォルダを開き、ファイルを1個ずつ見て、Excelに ファイル名・サイズ・更新日時 を転記。サブフォルダがあれば中に潜って同じことを繰り返す。
- 1ファイルあたり「クリック→プロパティ確認→Excelに打ち込み」で十数秒
- 100ファイル・数階層あったら…体感 40分〜1時間、しかも転記ミスつき
シーン2:容量を食っているファイルを探す
「共有ドライブが満杯。何がデカいのか調べて」。エクスプローラーでサイズ列ソートはできても、サブフォルダをまたいだ横断ソートができない。フォルダを開いては戻り、開いては戻り…で大物を探す。
- どのフォルダのどのファイルが効いているのか、全体像が見えない
- 「拡張子別だと何が一番容量を食っているか」も手作業では集計しづらい
これ、1コマンドでフォルダを再帰的に舐めてCSV台帳化すれば、Excelで開いて自由にソート・集計できます。
After:スクリプト一本で完了
進め方は ①ファイル一覧をCSV台帳に出力 → ②拡張子別・フォルダ別に集計 → ③容量の大きいファイルTOP N抽出 → ④(任意)xlsx直接出力 の順です。①だけでも実務は十分回ります。
レシピ1:ファイル一覧をCSV台帳に出力する
C:\delivery(macOS/Linuxなら ~/delivery)以下を再帰的に探索し、名前・相対パス・サイズ(KB,MB)・更新日時・拡張子 を1行ずつ並べたCSV台帳を作ります。
# ===== 設定 =====
$targetDir = "C:\delivery" # 台帳化したいルートフォルダ
$outCsv = "C:\delivery\_inventory.csv" # 出力先CSV
# ===== 本体 =====
# ルートの末尾を区切りで正規化(相対パス計算用)
$root = (Resolve-Path -LiteralPath $targetDir).Path.TrimEnd('\','/')
Get-ChildItem -LiteralPath $targetDir -File -Recurse |
Select-Object `
@{Name='Name'; Expression={ $_.Name }},
@{Name='RelativePath'; Expression={ $_.FullName.Substring($root.Length + 1) }},
@{Name='SizeKB'; Expression={ [math]::Round($_.Length / 1KB, 2) }},
@{Name='SizeMB'; Expression={ [math]::Round($_.Length / 1MB, 2) }},
@{Name='LastWrite'; Expression={ $_.LastWriteTime.ToString('yyyy/MM/dd HH:mm:ss') }},
@{Name='Extension'; Expression={ $_.Extension.TrimStart('.').ToLower() }} |
Export-Csv -LiteralPath $outCsv -Encoding UTF8BOM -NoTypeInformation
Write-Host "台帳を出力しました: $outCsv"
出力された _inventory.csv をExcelで開くと、こんな台帳になります(サンプル)。
Name RelativePath SizeKB SizeMB LastWrite Extension
---- ------------ ------ ------ --------- ---------
仕様書.xlsx docs\仕様書.xlsx 240.0 0.23 2026/06/20 10:11:00 xlsx
設計書.docx docs\設計書.docx 1536.0 1.5 2026/06/21 09:02:13 docx
画面遷移図.png docs\img\画面遷移図.png 3072.0 3.0 2026/06/22 14:48:50 png
ロゴ.png assets\ロゴ.png 500.0 0.49 2026/06/18 18:30:00 png
build.zip release\build.zip 71680.0 70.0 2026/06/25 02:00:00 zip
manual.pdf release\manual.pdf 5120.0 5.0 2026/06/24 16:20:00 pdf
readme readme 2.0 0.0 2026/06/17 11:00:00
上の表は見やすさのためカラムを整形して載せていますが、実際の
_inventory.csvは"Name","RelativePath",...というカンマ区切りのCSVで、ダブルクリックでExcelが開きます。SizeKB/SizeMBは数値なので、Excel側でそのままソート・合計できます。
これだけで「ファイル一覧表を手で作る」作業は終わりです。あとはExcelでフィルタやピボットを使えば自由自在です。とはいえ、よく使う集計はPowerShell側でも一発で出せます。
レシピ2:拡張子別・フォルダ別に集計する(件数・合計サイズ)
「どの種類のファイルが何個あって、合計何MBか」を一覧にします。容量調査・棚卸しの定番です。
# ===== 設定 =====
$targetDir = "C:\delivery"
# ===== 本体 =====
$files = Get-ChildItem -LiteralPath $targetDir -File -Recurse
# --- 拡張子別の集計(合計サイズの大きい順)---
Write-Host "===== 拡張子別 ====="
$files |
Group-Object { $_.Extension.TrimStart('.').ToLower() } |
Select-Object `
@{Name='Extension'; Expression={ if ($_.Name) { $_.Name } else { '(なし)' } }},
@{Name='Count'; Expression={ $_.Count }},
@{Name='TotalMB'; Expression={ [math]::Round((($_.Group | Measure-Object Length -Sum).Sum) / 1MB, 2) }} |
Sort-Object TotalMB -Descending |
Format-Table -AutoSize
出力イメージ:
===== 拡張子別 =====
Extension Count TotalMB
--------- ----- -------
zip 1 70
pdf 1 5
png 2 3.49
docx 1 1.5
xlsx 1 0.23
(なし) 1 0
フォルダ別(直下のトップレベルフォルダごと)に合計したいなら、相対パスの先頭要素でグルーピングします。
# ===== 設定 =====
$targetDir = "C:\delivery"
# ===== 本体 =====
$root = (Resolve-Path -LiteralPath $targetDir).Path.TrimEnd('\','/')
$files = Get-ChildItem -LiteralPath $targetDir -File -Recurse
Write-Host "===== トップレベルフォルダ別 ====="
$files |
Group-Object { ($_.FullName.Substring($root.Length + 1) -split '[\\/]')[0] } |
Select-Object `
@{Name='TopFolder'; Expression={ $_.Name }},
@{Name='Count'; Expression={ $_.Count }},
@{Name='TotalMB'; Expression={ [math]::Round((($_.Group | Measure-Object Length -Sum).Sum) / 1MB, 2) }} |
Sort-Object TotalMB -Descending |
Format-Table -AutoSize
===== トップレベルフォルダ別 =====
TopFolder Count TotalMB
--------- ----- -------
release 2 75
docs 3 4.73
assets 1 0.49
readme 1 0
ルート直下に直接置かれたファイル(例の
readme)は、相対パスにフォルダ区切りが無いためTopFolderがファイル名そのものになります。気になる場合は後述の応用で除外・グルーピングを調整できます。
レシピ3:容量の大きいファイル TOP N を抽出する
「とにかくデカいやつを上から知りたい」用のレシピです。再帰探索した全ファイルをサイズ降順で並べ、上位N件だけ表示します。
# ===== 設定 =====
$targetDir = "C:\delivery"
$topN = 3 # 上位何件を見るか
# ===== 本体 =====
$root = (Resolve-Path -LiteralPath $targetDir).Path.TrimEnd('\','/')
Get-ChildItem -LiteralPath $targetDir -File -Recurse |
Sort-Object Length -Descending |
Select-Object -First $topN |
Select-Object `
@{Name='SizeMB'; Expression={ [math]::Round($_.Length / 1MB, 2) }},
@{Name='Name'; Expression={ $_.Name }},
@{Name='RelativePath'; Expression={ $_.FullName.Substring($root.Length + 1) }} |
Format-Table -AutoSize
SizeMB Name RelativePath
------ ---- ------------
70 build.zip release\build.zip
5 manual.pdf release\manual.pdf
3 画面遷移図.png docs\img\画面遷移図.png
「サイズの大きい順に並べた台帳CSV」が欲しいなら、Format-Table の代わりに Export-Csv -Encoding UTF8BOM -NoTypeInformation に差し替えるだけです。
レシピ4(任意):xlsx を直接出力する(ImportExcel)
CSVでも実務は十分回りますが、「最初から .xlsx で、色やオートフィルタ付きで欲しい」場合は ImportExcel モジュールが便利です。Excel本体が無くてもxlsxを生成できます。
# 初回だけ:モジュールをインストール(現在のユーザー範囲)
Install-Module ImportExcel -Scope CurrentUser
# ===== 設定 =====
$targetDir = "C:\delivery"
$outXlsx = "C:\delivery\_inventory.xlsx"
# ===== 本体 =====
$root = (Resolve-Path -LiteralPath $targetDir).Path.TrimEnd('\','/')
Get-ChildItem -LiteralPath $targetDir -File -Recurse |
Select-Object `
@{Name='Name'; Expression={ $_.Name }},
@{Name='RelativePath'; Expression={ $_.FullName.Substring($root.Length + 1) }},
@{Name='SizeKB'; Expression={ [math]::Round($_.Length / 1KB, 2) }},
@{Name='SizeMB'; Expression={ [math]::Round($_.Length / 1MB, 2) }},
@{Name='LastWrite'; Expression={ $_.LastWriteTime.ToString('yyyy/MM/dd HH:mm:ss') }},
@{Name='Extension'; Expression={ $_.Extension.TrimStart('.').ToLower() }} |
Export-Excel -Path $outXlsx -WorksheetName 'Inventory' -AutoSize -FreezeTopRow -TableName 'Inventory'
-FreezeTopRow(見出し行固定)、-AutoSize(列幅自動)、-TableName(テーブル化=オートフィルタ付与)まで一発で付きます。ImportExcelはサードパーティ製(PowerShell標準ではない)ので、配布先で使えない環境もあります。迷ったら、まずは標準の Export-Csv で完結させるのが無難です。
コードの要点解説(なぜこの書き方か)
文法そのものではなく、現場でハマらないための選択を中心に解説します。
1. Export-Csv -Encoding UTF8BOM でExcel文字化けを防ぐ
日本語を含むCSVをExcelでダブルクリックして開くと、BOM(Byte Order Mark)が無いUTF-8だと文字化けすることがあります(Excelがファイルの文字コードを誤判定するため)。-Encoding UTF8BOM で先頭にBOMを付けると、ExcelがUTF-8と正しく認識し、日本語ファイル名も化けません。
UTF8BOMという指定値は PowerShell 7 以降 で使えます。Windows PowerShell 5.1 では別の書き方が必要です(後述)。Excelで開く前提のCSVでは、このエンコード指定が事故防止の肝です。
2. 計算プロパティ @{Name=...; Expression={...}} で台帳の列を作る
Select-Object に @{Name='列名'; Expression={ 計算式 }} を渡すと、好きな名前・好きな計算結果の列を作れます。$_ はその行のファイルオブジェクトです。
-
SizeMB…$_.Length(バイト)を/ 1MBしてMB換算 -
LastWrite…$_.LastWriteTimeをToString('yyyy/MM/dd HH:mm:ss')で文字列化(書式を固定) -
Extension…$_.Extension.TrimStart('.').ToLower()でドット除去+小文字統一
Name= は N=、Expression= は E= と省略もできますが、台帳のように列が多いときは略さず書くほうが後で読みやすいです。
3. 相対パスは FullName.Substring($root.Length + 1) で作る
Get-ChildItem -Recurse が返す FullName は C:\delivery\docs\仕様書.xlsx のようなフルパスです。台帳には「ルートからの相対パス」(docs\仕様書.xlsx)が欲しいので、ルートの文字数+1(末尾の区切り文字ぶん)を先頭から切り落とします。
$root は Resolve-Path で実体パスに直したうえで TrimEnd('\','/') し、末尾の区切りを正規化しています。これをしておかないと + 1 の計算が1文字ずれます。
4. 1KB / 1MB はPowerShell組み込みの定数
PowerShellは 1KB(=1024)、1MB(=1048576)、1GB … をそのまま数値リテラルとして理解します。$_.Length / 1MB で素直にMB換算でき、[math]::Round(値, 2) で小数2桁に丸めています。バイト÷1000ではなく÷1024系である点に注意(Windowsのプロパティ表示と揃います)。
5. Group-Object + Measure-Object -Sum で集計する
拡張子別・フォルダ別の集計は、Group-Object でまとめ、各グループの $_.Group(そのグループに属するファイル群)に対して Measure-Object Length -Sum で合計バイトを出しています。.Sum を / 1MB して丸めれば「合計MB」になります。Excelのピボットテーブルと同じことを、スクリプト側で完結できます。
応用・注意点
① 出力先CSVの「上書き」に注意
Export-Csv は、出力先に同名ファイルがあると 確認なしで上書きします(破壊的ではありませんが、前回の台帳は消えます)。実行ごとに履歴を残したいなら、ファイル名に日時を入れます。
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
$outCsv = "C:\delivery\_inventory_$stamp.csv" # 例: _inventory_20260628_103000.csv
なお、出力先CSVを対象フォルダの「中」に置くと、次回の -Recurse 探索が自分が出力した台帳CSV自身を拾ってしまうことがあります。気になる場合は出力先をフォルダ外にするか、探索結果から除外します。
# 台帳CSV自身(_inventory*.csv)を集計対象から外す
Get-ChildItem -LiteralPath $targetDir -File -Recurse |
Where-Object { $_.Name -notlike '_inventory*.csv' }
② Excelで開くときの文字化け対策(エンコーディング)
前述のとおり、Excelで開く前提のCSVは -Encoding UTF8BOM(PowerShell 7) を付けるのが基本です。それでも文字化けする・数値が勝手に日付変換されるなどExcelの自動解釈が邪魔な場合は、CSVをダブルクリックで開かず、Excelの 「データ」→「テキスト/CSVから」 で文字コードと列の型を指定して読み込むと確実です。
③ -NoTypeInformation は何のため?
Export-Csv は既定で、CSVの1行目に #TYPE Selected.System.IO.FileInfo のような型情報のコメント行を付けます。台帳としては邪魔なので -NoTypeInformation で抑制しています。
PowerShell 7 では
-NoTypeInformationは既定の挙動(型情報行は元々付きません)なので、付けても付けなくても結果は同じです。ただし Windows PowerShell 5.1 では既定で型情報行が付くため、5.1 と両対応にするなら 明示的に付けておくのが安全です。本記事では揃えて付けています。
④ 巨大フォルダでの実行時間
Get-ChildItem -Recurse は、ファイル数が数万〜数十万になるとそれなりに時間がかかります(数十秒〜数分)。
- まず件数の見当をつける:
(Get-ChildItem -LiteralPath $targetDir -File -Recurse).Count - ネットワークドライブや権限の無いフォルダがあると、アクセスエラーで止まったり遅くなったりします。エラーを無視して走り切りたいときは
-ErrorAction SilentlyContinueを付けます。
Get-ChildItem -LiteralPath $targetDir -File -Recurse -ErrorAction SilentlyContinue
巨大フォルダで何度も試行錯誤するなら、一度 $files = Get-ChildItem ... で変数に取り込み、その後の集計(レシピ2・3)は変数を使い回すと、毎回フォルダを舐め直さずに済みます。
⑤ シンボリックリンク・隠しファイルの扱い
Get-ChildItem は既定で隠しファイル・システムファイルを表示しません。台帳に含めたいなら -Force を付けます。また、フォルダのジャンクション/シンボリックリンクをたどると無限ループや二重カウントの恐れがあるため、リンクが多い環境では注意してください(既定ではリンク先の再帰追跡は抑制されますが、構成によっては想定外の挙動になります)。
Get-ChildItem -LiteralPath $targetDir -File -Recurse -Force # 隠し・システムも含める
⑥ Windows PowerShell 5.1 との差異
本記事のスクリプトは、コアな部分(探索・計算プロパティ・集計)は 5.1 でもそのまま動作します。最大の違いは CSVのエンコードです。
| 項目 | PowerShell 7(pwsh) | Windows PowerShell 5.1 |
|---|---|---|
Export-Csv の既定エンコード |
UTF-8(BOMなし) | 環境の既定(多くはShift_JIS/ANSI) |
-Encoding UTF8BOM の指定 |
利用可(BOM付きUTF-8で出力) | 指定不可(値が存在しない) |
-NoTypeInformation |
既定で型情報行は付かない(付けても無害) |
付けないと #TYPE ... 行が入る → 付けるのが必須 |
| Excelでの文字化け |
UTF8BOM 指定で回避しやすい |
既定のShift_JISでExcelは概ね素直に開けることが多い |
5.1 でUTF-8(BOM付き)にしたい場合は、いったん既定で出力してから書き直すか、Out-File を併用します。最も手堅いのは、5.1ではShift_JIS既定のまま出してExcelで開くか、可能なら PowerShell 7 を入れて UTF8BOM を使うことです。
# 【5.1向け】UTF-8(BOM付き)で出したいときの例:CSV文字列を作って書き出す
$csv = $data | ConvertTo-Csv -NoTypeInformation
$utf8Bom = New-Object System.Text.UTF8Encoding($true) # $true = BOM付き
[System.IO.File]::WriteAllLines("C:\delivery\_inventory.csv", $csv, $utf8Bom)
迷ったら PowerShell 7(pwsh)+ -Encoding UTF8BOM に統一するのが、文字化けで悩まない一番の近道です。
まとめ
- 台帳化は
Get-ChildItem -Recurse+ 計算プロパティ +Export-Csv - Excelで開く前提のCSVは
-Encoding UTF8BOMで文字化け回避(PowerShell 7) - サイズは
$_.Length / 1MB+[math]::Round(..., 2)、相対パスはFullName.Substring($root.Length + 1) - 集計は
Group-Object+Measure-Object -Sum、TOP N はSort-Object Length -Descending | Select-Object -First N - 5.1 は
Export-Csvの既定エンコードが違う・-NoTypeInformationが必要・UTF8BOMが使えない の3点に注意
これで、毎回30分〜1時間かけていた「ファイル一覧表づくり」が、数秒のコマンド一本になります。
次回 第4回は「複数CSVを1枚に結合して集計」。各拠点・各月から集まったバラバラのCSVを Import-Csv で読み込み、Group-Object で横断集計して1枚のサマリにまとめるレシピを扱います。お楽しみに。
参考
- Get-ChildItem (Microsoft.PowerShell.Management) - Microsoft Learn
- Export-Csv (Microsoft.PowerShell.Utility) - Microsoft Learn
- Select-Object (Microsoft.PowerShell.Utility) - Microsoft Learn
- Group-Object (Microsoft.PowerShell.Utility) - Microsoft Learn
- Measure-Object (Microsoft.PowerShell.Utility) - Microsoft Learn
- about_Calculated_Properties (PowerShell) - Microsoft Learn
- ImportExcel (PowerShell Gallery)
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!