Corredor

ウェブ、プログラミングの勉強メモ。

PowerShell の GetDetailsOf を使ってファイルの詳細プロパティを取得する

久々に PowerShell を書いてみた。

「メディアの作成日時」というファイルプロパティを取得したい

画像ファイルや動画ファイルをリネームしたり、撮影日別のディレクトリに移動したりしたいと思った。

画像や動画の撮影日というと、ファイルの中に「撮影日時」というプロパティや「メディアの作成日時」というプロパティで日付情報が埋め込まれている。コレまでは、エクスプローラでこのプロパティのカラムを表示して、それを見ながら手入力でリネームしたりしていた。

今回は、コレをスクリプトで取得して、リネームやファイル移動をバッチ化しようと思う。

先にコードを載せておく

先に成果物を載せておく。

  • ディレクトリ配下のファイルについて、「メディアの作成日時」か「撮影日時」の値を取得しリネームする
    • ex. HOGE.jpg2019-01-31 HOGE.jpg とリネームする

github.com

  • ディレクトリ配下のファイルについて、「メディアの作成日時」か「撮影日時」の値が取得できたらその値で、両方取得できなかったら「作成日時」か「更新日時」のより古い方の値で、「YYYY-MM-DD」ディレクトリを作り、そこにファイルを移動する
    • ex. HOGE.jpg2019-01-31\HOGE.jpg と移動する

github.com

これらのスクリプトの実装に至るメモ、使い方など順に説明していく。

詳細プロパティを拾うには GetDetailsOf を使う

まずは、「撮影日時」や「メディアの作成日時」というプロパティの値を拾う方法を調べてみる。

# シェルオブジェクトを作成する
$objShell = New-Object -ComObject Shell.Application
# ディレクトリを指定する
$objFolder = $objShell.namespace("C:\")

# プロパティ名の一覧を表示する : とりあえず330項目分拾ってみる
0..330 | foreach { "{0,3}:{1}" -f $_, $objFolder.getDetailsOf($Null, $_) }

この中に出てきた、getDetailsOf というメソッドがキモ。第1引数に $Null を入れておくと、「プロパティ名」が返ってくる。手元で実行した感じではこんな結果になった。少し長いのでご注意。

  0:名前
  1:サイズ
  2:項目の種類
  3:更新日時
  4:作成日時
  5:アクセス日時
  6:属性
  7:オフラインの状態
  8:利用可能性
  9:認識された種類
 10:所有者
 11:分類
 12:撮影日時
 13:参加アーティスト
 14:アルバム
 15:年
 16:ジャンル
 17:指揮者
 18:タグ
 19:評価
 20:作成者
 21:タイトル
 22:件名
 23:分類項目
 24:コメント
 25:著作権
 26:トラック番号
 27:長さ
 28:ビット レート
 29:保護
 30:カメラのモデル
 31:大きさ
 32:カメラの製造元
 33:会社
 34:ファイルの説明
 35:マスター キーワード
 36:マスター キーワード
 37:
 38:
 39:
 40:
 41:
 42:プログラム名
 43:継続時間
 44:オンライン
 45:再帰
 46:場所
 47:任意出席者アドレス
 48:任意出席者
 49:開催者住所
 50:開催者名
 51:アラーム時刻
 52:必須出席者アドレス
 53:必須出席者
 54:リソース
 55:会議の状態
 56:空き時間情報
 57:合計サイズ
 58:アカウント名
 59:
 60:進捗状況
 61:コンピューター
 62:記念日
 63:秘書の名前
 64:秘書の電話
 65:誕生日
 66:住所 (会社)
 67:市 (会社)
 68:国/地域 (会社)
 69:私書箱 (会社)
 70:郵便番号 (会社)
 71:都道府県 (会社)
 72:番地 (会社)
 73:FAX 番号 (会社)
 74:会社のホーム ページ
 75:会社電話
 76:コールバック番号
 77:自動車電話
 78:子供
 79:会社代表電話
 80:部署
 81:電子メール アドレス
 82:電子メール2
 83:電子メール3
 84:電子メールの一覧
 85:電子メール表示名
 86:表題2
 87:名
 88:氏名
 89:性別
 90:名2
 91:趣味
 92:住所 (自宅)
 93:市 (自宅)
 94:国/地域 (自宅)
 95:私書箱 (自宅)
 96:郵便番号 (自宅)
 97:都道府県 (自宅)
 98:番地 (自宅)
 99:自宅 FAX
100:自宅電話
101:IM アドレス
102:イニシャル
103:役職
104:ラベル
105:姓
106:住所 (郵送先)
107:ミドル ネーム
108:携帯電話
109:ニックネーム
110:勤務先所在地
111:住所 (その他)
112:ほかの市区町村
113:他の国/地域
114:他の私書箱
115:他の郵便番号
116:他の都道府県
117:他の番地
118:ポケットベル
119:肩書き
120:市区町村 (郵送先)
121:国/地域
122:私書箱 (郵送先)
123:郵便番号 (郵送先)
124:都道府県
125:番地 (郵送先)
126:プライマリ電子メール
127:通常の電話
128:職業
129:配偶者
130:サフィックス
131:TTY/TTD 電話
132:テレックス
133:Web ページ
134:内容の状態
135:内容の種類
136:取得日時
137:アーカイブ日時
138:完了日
139:デバイス カテゴリ
140:接続済み
141:探索方法
142:フレンドリ名
143:ローカル コンピューター
144:製造元
145:モデル
146:ペアリング済み
147:クラス
148:状態
149:状態
150:クライアント ID
151:共同作成者
152:コンテンツの作成日時
153:前回印刷日
154:前回保存日時
155:事業部
156:ドキュメント ID
157:ページ数
158:スライド
159:総編集時間
160:単語数
161:期限
162:終了日
163:ファイル数
164:ファイル拡張子
165:ファイル名
166:ファイル バージョン
167:フラグの色
168:フラグの状態
169:空き領域
170:
171:
172:グループ
173:共有の種類
174:ビットの深さ
175:水平方向の解像度
176:幅
177:垂直方向の解像度
178:高さ
179:重要度
180:添付
181:削除
182:暗号化の状態
183:フラグの有無
184:終了済
185:未完了
186:開封の状態
187:共有
188:製作者
189:日付時刻
190:フォルダー名
191:フォルダーのパス
192:フォルダー
193:参加者
194:パス
195:場所ごと
196:種類
197:連絡先の名
198:履歴の種類
199:言語
200:最終表示日
201:説明
202:リンクの状態
203:リンク先
204:URL
205:
206:
207:
208:メディアの作成日時
209:リリース日
210:エンコード方式
211:エピソード番号
212:プロデューサー
213:発行元
214:シーズン番号
215:サブタイトル
216:ユーザー Web URL
217:作者
218:
219:添付ファイル
220:BCC アドレス
221:BCC
222:CC アドレス
223:CC
224:会話 ID
225:受信日時
226:送信日時
227:送信元アドレス
228:差出人
229:添付ファイルの有無
230:送信者アドレス
231:送信者名
232:ストア
233:送信先アドレス
234:To do タイトル
235:宛先
236:経費情報
237:アルバムのアーティスト
238:アルバム アーティストで並べ替え
239:アルバム ID
240:アルバムで並べ替え
241:参加アーティストで並べ替え
242:ビート数/分
243:作曲者
244:作曲者で並べ替え
245:ディスク
246:イニシャル キー
247:コンパイルの一部
248:雰囲気
249:セットのパート
250:期間
251:色
252:保護者による制限
253:保護者による制限の理由
254:使用領域
255:EXIF バージョン
256:イベント
257:露出補正
258:露出プログラム
259:露出時間
260:絞り値
261:フラッシュ モード
262:焦点距離
263:35mm 焦点距離
264:ISO 速度
265:レンズ メーカー
266:レンズ モデル
267:光源
268:最大絞り
269:測光モード
270:向き
271:人物
272:プログラムのモード
273:彩度
274:対象の距離
275:ホワイト バランス
276:優先度
277:プロジェクト
278:チャンネル番号
279:この回のタイトル
280:字幕
281:再放映
282:SAP
283:放送日
284:プログラムの説明
285:記録時間
286:ステーション コール サイン
287:局名
288:概要
289:抜粋
290:自動要約
291:関連度
292:ファイルの所有権
293:秘密度
294:共有ユーザー
295:共有状態
296:
297:製品名
298:製品バージョン
299:サポートのリンク
300:ソース
301:開始日
302:共有中
303:可用性の状態
304:状態
305:課金情報
306:完了
307:仕事の所有者
308:タイトルで並べ替え
309:総ファイル サイズ
310:商標
311:ビデオ圧縮
312:ディレクター
313:データ速度
314:フレーム高
315:フレーム率
316:フレーム幅
317:球形
318:ステレオ
319:ビデオの向き
320:総ビット レート
321:Audio tracks
322:Bit depth
323:Contains chapters
324:Content compression
325:Subtitles
326:Subtitle tracks
327:Video tracks
328:
329:
330:

0:名前 など先頭の方はよくあるプロパティなので、どの PC 環境でも大体同じ順番で出てくるのだが、今回お目当ての「撮影日時」や「メディアの作成日時」といった項目は、PC 環境によって登場する番号が異なるようだ。

今回は「プロパティ名」を取得したが、$Null を指定した第1引数部分にファイル名を入れることで、そのファイルの「撮影日時」や「メディアの作成日時」の値が拾える。

一括リネームするスクリプトを書いてみる

さて、getDetailsOf メソッドを使えば撮影日時などの値が拾えることが分かったので、順にスクリプトを書いていってみる。スクリプトファイル名は適当に rename-all.ps1 みたいな名前にしようと思う。

PowerShell を書くのが久々だったので、引っかかったところを順にメモしていく。

カレントディレクトリを取得する

まずは、このスクリプトを呼び出した時に、カレントディレクトリを対象に操作をしたいので、スクリプトの中でカレントディレクトリを取得したい。

[String]$targetDirectory = Get-Location

このように String 型にキャストして Get-Location とすれば良い。

もし任意のディレクトリを固定で指定したいなら、

$targetDirectory = "C:\Path\To\Target\Directory"

なんていう風にパスを書いてしまえば良い。

指定ディレクトリ配下のファイルをフルパスで取得する

先程の $targetDirectory で、ディレクトリのパスを取得できた。次は、その配下にあるファイル名をフルパスで取得してみる。

$targetFiles = Get-ChildItem $targetDirectory | Where-Object { ! $_.PSIsContainer } | % { $_.FullName }

Get-ChildItem で一覧を取得し、Where-Object を使ってディレクトリを除外する。Where-Object?where というエイリアスでも書ける。

ディレクトリを除外したら、ForEach-Object (= エイリアス %) でフルパスを取得する。コレで変数 $targetFiles に、指定ディレクトリ配下のファイルがフルパスで格納された。

ファイルを順に操作する

ファイルを順に操作するには、先程も登場した ForEach-Object (%) を使ったりできるのだが、ForEach-Objectcontinuebreak ができないようなので、代わりに foreach を使う。

foreach($targetFile in $targetFiles) {
  # ディレクトリパスとファイル名に分割する
  $folderPath = Split-Path $targetFile
  $fileName   = Split-Path $targetFile -Leaf
  
  Write-Host "$folderPath : $fileName"
  
  if((Get-Item $targetFile).extension.toLower() -eq ".ps1") {
    Write-Host "PowerShell スクリプトは処理しない"
    continue
  }
}

こんな感じで、変数 $targetFilesforeach でループしている。拡張子で判定して、処理したくないファイルなら continue で飛ばしたり、ということができる。

「メディアの作成日時」や「撮影日時」プロパティの情報を取得する

以降は foreach 内にコードを書いていく。最初に調べた getDetailsOf メソッドを使っていく。

foreach($targetFile in $targetFiles) {
  # ディレクトリパスとファイル名に分割する
  $folderPath = Split-Path $targetFile
  $fileName   = Split-Path $targetFile -Leaf
  
  # …中略…
  
  # シェルオブジェクトを作成する
  $shell       = New-Object -ComObject Shell.Application
  $shellFolder = $shell.namespace($folderPath)
  $shellFile   = $shellFolder.parseName($fileName)
  
  # 詳細プロパティからリネームに使用するプロパティ名と値を取得する
  $selectedPropertyNo    = ""
  $selectedPropertyName  = ""
  $selectedPropertyValue = ""
  
  # 詳細プロパティを列挙する : 適当に310項目ほど取得する
  for($i = 0; $i -lt 310; $i++) {
    # プロパティ名を取得する
    $propertyName  = $shellFolder.getDetailsOf($Null, $i)
    
    # プロパティ名が目的のモノなら、そのプロパティの値を取得する
    if($propertyName -eq "メディアの作成日時" -or $propertyName -eq "撮影日時") {
      # ファイルからそのプロパティの値を取得する
      $propertyValue = $shellFolder.getDetailsOf($shellFile, $i)
      
      # そのプロパティに値があれば変数に控えて終わりにする
      if($propertyValue) {
        $selectedPropertyNo    = $i
        $selectedPropertyName  = $propertyName
        $selectedPropertyValue = $propertyValue
        break
      }
    }
  }
}

getDetailsOf メソッドが取得できるプロパティの総数はパッとは分からないので、ループ中では適当に 310 回ループしている。上に貼ったプロパティ一覧を見ても、「撮影日時」は 12、「メディアの作成日時」は 208 という位置に登場しているので、300 ぐらいまでループすれば多分取得できる。

そして、プロパティ名を見て目的のプロパティかどうか判定している。直接 -eq "メディアの作成日時" なんて比較をしているので、英語環境の Windows なんかだと上手く拾えないと思う。ココらへんはキニシナイ。w

ifelseIf (スペースなし) における And 条件、Or 条件は -and-or で書ける。

「メディアの作成日時」プロパティの番号が分かったところで、ファイルから情報を取得する。それが $propertyValue = $shellFolder.getDetailsOf($shellFile, $i) 部分。

そのプロパティに値が入っているかどうか、空文字かどうかを判定するには、JavaScript ライクに if に変数を突っ込むだけで大丈夫。Bool にキャストしたりしても良い。

値が拾えたら、for ループの外に用意しておいた変数に値を退避し、for ループを break で抜ける。

日付文字列を整形してリネームする

プロパティを特定して値を取得し、$selectedPropertyValue などの変数に値を控えておいた。次はその値を整形して、リネームしていく。

foreach($targetFile in $targetFiles) {
  # …中略…
  for($i = 0; $i -lt 310; $i++) {
    # …中略…
  }
  
  if(! $selectedPropertyNo) {
    Write-Host "プロパティ・もしくはプロパティの値がなかった・リネーム処理できない"
    continue
  }
  
  # YYYY-MM-DD 形式で日付を取得する
  $dateTimeStr = $selectedPropertyValue.substring(1, 4) `
                 + "-" + $selectedPropertyValue.substring(7, 2) `
                 + "-" + $selectedPropertyValue.substring(11, 2)
  # 新しいファイル名を作る (日付をオリジナルのファイル名の行頭に付与しスペースを付ける)
  $newFileName = $dateTimeStr + " " + $fileName
  
  # リネームする
  Rename-Item $targetFile -newName $newFileName
}

空文字かどうかの判定は先程も書いたとおり if 文に突っ込むだけ。if(! $selectedPropertyNo)! で条件を反転させれば、「空文字だった場合は」と処理できる。リネームに使用したい値が取得できていなかった場合は、continue を使って foreach ループを中断している。

さて、「メディアの作成日時」や「撮影日時」の値は、先頭に1文字スペースがあり、YYYY- MM- DD HH:mm みたいな謎の書式になっている。この情報は String 型で取得されていて、Date 型への変換が面倒くさかった。

色々面倒くさいので、substring メソッドで位置指定して YYYYMMDD 部分を取得し、自前でハイフン - と結合し、YYYY-MM-DD 形式にする。

そしてそれをオリジナルのファイル名の先頭にくっつけて、変数 $newFileName を用意する。

実際のリネームは Rename-Item というメソッドを使う。

ファイルから日付情報を取得し、そのディレクトリを作成、そこにファイルを移動する

次は、リネームではなく、ファイル移動。画像ファイルなどを撮影日時別のディレクトリに分類するための、別のスクリプトを作る。

「メディアの作成日時」や「撮影日時」というプロパティは、ファイルによって情報が付与されていたりいなかったりするので、どちらのプロパティもなかった場合は、「作成日時」や「更新日時」あたりの必ずあるプロパティを使いたい。

ただ、「作成日時」や「更新日時」は、たまに更新日時の方がより古い値になっていて、そちらが実際の撮影時間に近い場合があったりする。

そこで、作成日時と更新日時を取得したら日時を比較して、より古い値の方を撮影時間とみなして利用することにする。

# ファイルフルパスを元に1ファイルずつ処理する
foreach($targetFile in $targetFiles) {
  # ディレクトリパスとファイル名に分割する
  $folderPath = Split-Path $targetFile
  $fileName   = Split-Path $targetFile -Leaf
  
  # ファイルのオブジェクトを取得しておく
  $targetFileObject = Get-Item $targetFile
  
  # …中略…
  
  # 詳細プロパティの取得を試みる
  for($i = 0; $i -lt 310; $i++) {
    # …中略…
  }
  
  # 日付文字列を控える変数
  $dateTimeStr = "0000-00-00"
  
  if(! $selectedPropertyNo) {
    # プロパティが取れなかったら、作成日時か更新日時からより古い方の値を取得する
    
    if($targetFileObject.creationTime -lt $targetFileObject.lastWriteTime) {
      # 作成日時の方が古い
      $dateTimeStr = $targetFileObject.creationTime.toString("yyyy-MM-dd")
    }
    else {
      # 更新日時の方が古い
      $dateTimeStr = $targetFileObject.lastWriteTime.toString("yyyy-MM-dd")
    }
  }
  else {
    # 「メディアの作成日時」や「撮影日時」が取得できた場合は、そのプロパティの値から日付を取得する
    
    $dateTimeStr = $selectedPropertyValue.substring(1, 4) `
                 + "-" + $selectedPropertyValue.substring(7, 2) `
                 + "-" + $selectedPropertyValue.substring(11, 2)
  }
  
  # 日付のディレクトリを作る
  $newDirectoryPath = $targetDirectory + "\" + $dateTimeStr
  # Out-Null にパイプすることで結果を非表示にする
  New-Item $newDirectoryPath -ItemType Directory -Force | Out-Null
  
  # ファイルを移動する
  Move-Item $targetFile $newDirectoryPath
}

if(! $selectedPropertyNo)if 文の中が、作成日時 (creationTime) や更新日時 (lastWriteTime) を取得・比較しているところ。単純に -lt と比較演算子を使って比較できた。toString メソッドを使うと日付のフォーマットを指定して文字列として取得できる。

else の方は、リネームスクリプトに出てきたコードと同様だ。

変数 $newDirectoryPath として、これから作ろうとしている日付のディレクトリのフルパスを用意する。【カレントディレクトリ】\YYYY-MM-DD という内容だ。

コレを使ってディレクトリを作成するには、New-Item を使う。-ItemType Directory を指定しないと空ファイルを作ってしまう (touch 的な挙動) ので注意。-Force は、そのディレクトリが既に存在する場合もエラーを発生させないようにするためのモノ。

New-Item メソッドを使うと、作成したディレクトリの情報が出力されるので、その出力を非表示にするために | Out-Null にパイプしている。Bash でいうと > /dev/null みたいなモノだ。

ファイルの移動は Move-Item。簡単だ。

スクリプトの実行方法

さて、こうして作ったスクリプトを簡単に実行する方法を紹介する。

PATH のとおったディレクトリを用意すると楽

まずは、ユーザホームディレクトリ直下に bin というディレクトリを作る。フルパスでいうと C:\Users\【ユーザ名】\bin\ という具合だ。

その bin ディレクトリの下に、今回作成した rename-all.ps1move-all.ps1 ファイルを置いておく。

そして「環境変数」の設定を開き、Path に先程の bin ディレクトリまでのパス C:\Users\【ユーザ名】\bin\ を追加しておく。

こうすると、コマンドプロンプトや PowerShell で、この bin ディレクトリ配下に置いたスクリプトを、フルパスを書くことなく呼び出せるようになる。

ユーザホームディレクトリの直下に bin ディレクトリを作る、という構成は、Linux における ~/bin/ と同じ構成になり、実際に GitBash でも ~/bin/ でアクセスできるようになり、扱いやすい。

実行方法1

ということで、ココまでできたら、Win + X キーでコンテキストメニューを出し、A キーで「Windows PowerShell (管理者)」を選択したりして、PowerShell を起動したら、

PS1> cd C:\Path\To\Target\Directory\
PS1> rename-all

こんな風に一括リネームを実行したいディレクトリに移動 (cd) して、rename-allmove-all のように、スクリプトファイル名を入力すれば実行できる。拡張子は書いても書かなくても良い。

実行方法2

次はエクスプローラから起動する方法。

エクスプローラで目的のディレクトリに移動したら、アドレスバーに %comspec% もしくは cmd と入力する。するとコマンドプロンプトが起動する。その時のカレントディレクトリは、エクスプローラで開いていたディレクトリになっているので、そのまま

> powershell rename-all

みたいにすると実行できる。

実行方法3

エクスプローラのアドレスバーに powershell と入力すると PowerShell のウィンドウが起動する。その時のカレントディレクトリは、エクスプローラで開いていたディレクトリになっているので、そのまま

PS1> rename-all

と実行すれば、スクリプトが実行できる。

実行方法4

もう少し直接的に、エクスプローラのアドレスバーに

powershell rename-all

と打つと、PowerShell が起動してスクリプトが実行される。ただしこの場合、スクリプトが終わると PowerShell のウィンドウも閉じてしまうので、スクリプトの末尾に

Read-Host "続行するには Enter キーを押してください。"

みたいな1行を入れて、コマンドプロンプトの Pause のように処理を止めてあげるか、アドレスバーに入れる時に

powershell -noexit rename-all

と、-NoExit (大文字小文字は問わない) を入れてあげると、PowerShell のウィンドウが閉じずに残ってくれる。

まとめ

ということで起動方法まとめ。

  1. Win + XA → (PowerShell 起動) → cd 【ディレクトリ】rename-all (実行)
  2. エクスプローラのアドレスバーで %comspec% or cmd → (コマンドプロンプト起動) → powershell rename-all (実行)
  3. エクスプローラのアドレスバーで powershell → (PowerShell 起動) → rename-all (実行)
  4. エクスプローラのアドレスバーで powershell -noexit rename-all (実行)

4つ目が楽かなぁ。powershell ってタイプ数が多いが面倒くさいので、ココも短くしたいなぁ。

以上

久々に PowerShell を書いたので、文法も何も完全に忘れていた。

中途半端にシェルスクリプトっぽく書けたり、.NET っぽさも出てきたり、なかなかややこしいので、あんまり積極的には書きたくないな…。それでも WSH より色んなことがやりやすいので、今回は PowerShell を使ってみた次第。

PowerShell実践ガイドブック ~クロスプラットフォーム対応の次世代シェルを徹底解説~

PowerShell実践ガイドブック ~クロスプラットフォーム対応の次世代シェルを徹底解説~

Windows PowerShellクックブック

Windows PowerShellクックブック

  • 作者: Lee Holmes,マイクロソフト株式会社ITプロエバンジェリストチーム(監訳),菅野良二
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2008/10/23
  • メディア: 大型本
  • 購入: 4人 クリック: 72回
  • この商品を含むブログ (15件) を見る

Windows PowerShell実践システム管理ガイド 第3版 (TechNet ITプロシリーズ)

Windows PowerShell実践システム管理ガイド 第3版 (TechNet ITプロシリーズ)

【改訂新版】 Windows PowerShell ポケットリファレンス

【改訂新版】 Windows PowerShell ポケットリファレンス