タイダログ

もっと怠けますか? (y/n)

PowerShell でファイル名の先頭の文字列でフォルダ分けする

スキャナで連続取り込みした画像や PDF ファイルでフォルダが溢れ返ってしまうので、ファイル名の接頭辞ごとにフォルダ分けします。

before

after

apple

作業環境

完成形

# 正規表現
# 先頭から、ハイフン以外の文字の連続部分を接頭辞として使用
$pattern = '^([^-]+)-.+\.pdf$'

# 接頭辞でフォルダ分けする
Get-ChildItem -File | Where-Object {$_.Name -match $pattern} | ForEach-Object {if ((Test-Path $Matches[1]) -eq $false) {New-Item -Name $Matches[1] -ItemType Directory}; $_ | Move-Item -Destination $Matches[1]}

正規表現はファイル名やフォルダ名に合わせて適宜変えてください。フォルダ名にしたい部分を () で囲みます。エスケープ文字はバックスラッシュ (\) です。円記号 (¥) ではだめです。

^([^-]+)-.+\.pdf$ で、「ハイフン以外の文字が一回以上連続した後、ハイフンが一回出現し、その後任意の文字が任意の回数出現し、'.pdf' で終わる」というファイル名を表します。拡張子が '.jpg' なら ^([^-]+)-.+\.jpg$ 、指定しない場合は ^([^-]+)-.+$ あるいは ^([^-]+) とします。

2021/10/03 追記
もっとシンプルに「ハイフンより左」と考えれば、^(.+)- でいいですね。あるいは「ハイフン以外の文字の連続部分」なら ^[^-]+ でいいです。後者はグループ化をしていないので $Matches だけでいいです。
2021/10/03 追記ここまで

説明

以下の 2 ステップです。

  1. 正規表現で接頭辞をグループ化してキャプチャ
  2. 接頭辞ごとにフォルダに移動

正規表現で接頭辞をグループ化してキャプチャ

正規表現のパターンの一部を () でくくると、その部分をグループ(ひとつのまとまり)として扱い、マッチした文字列からその部分だけをキャプチャする(抜き出す)ことができます。^([^-]+)-.+\.pdf$ とすると [^-]+ にマッチした部分を取得できます。この場合、「ファイル名の先頭から、ハイフン以外の文字が一回以上連続する部分」を取得します。

-match でマッチした内容は $Matches 自動変数に入ります。グループ化した部分は $Matches[1] のようにすると取得できます。

正規表現やグループについての詳しい説明はこちらの方々にお任せします。また、後の「正規表現のグループ化のサンプル」でサンプルをご覧ください。

docs.microsoft.com

mtgpowershell.blogspot.com

bgt-48.blogspot.com

接頭辞ごとにフォルダに移動

$Matches[1] に入っている接頭辞を移動先のフォルダ名にします。

正規表現のグループ化のサンプル

$Matches 自動変数

グループ化してキャプチャした内容がどのように $Matches 自動変数に入るか見てみましょう。

次のファイルがあるディレクトリで以下のコードを試してください。

apple-001.pdf
apple-002.pdf
apple-003.pdf
caramel-001.pdf
caramel-002.pdf
caramel-003.pdf
caramel-004.pdf
potato-001.pdf
potato-002.pdf
potato-003.pdf
pumpkin-001.pdf
pumpkin-002.pdf
pumpkin-003.pdf
pumpkin-004.pdf
pumpkin-005.pdf
# 正規表現
# 先頭から、ハイフン以外の文字の連続部分を接頭辞として使用
$pattern = '^([^-]+)-.+\.pdf$'

# 接頭辞をグループ化してキャプチャ
Get-ChildItem .\apple-001.pdf | Where-Object {$_.Name -match $pattern} | ForEach-Object {$Matches}

このような結果になるはずです。

Name                           Value
----                           -----
1                              apple
0                              apple-001.pdf

$Matches 自動変数のインデックス 0 にはマッチした文字列全体が、1 には () でグループ化した部分のみが入っています。

お次はこちら。

# 正規表現
# 先頭から、ハイフン以外の文字の連続部分を接頭辞として使用
$pattern = '^([^-]+)-.+\.pdf$'

# 接頭辞をグループ化してキャプチャ
Get-ChildItem -File | Where-Object {$_.Name -match $pattern} | ForEach-Object {Write-Output "ファイル名: $($Matches[0])`t接頭辞: $($Matches[1])"}

このような結果になるはずです。

ファイル名: apple-001.pdf       接頭辞: apple
ファイル名: apple-002.pdf       接頭辞: apple
ファイル名: apple-003.pdf       接頭辞: apple
ファイル名: caramel-001.pdf     接頭辞: caramel
ファイル名: caramel-002.pdf     接頭辞: caramel
ファイル名: caramel-003.pdf     接頭辞: caramel
ファイル名: caramel-004.pdf     接頭辞: caramel
ファイル名: potato-001.pdf      接頭辞: potato
ファイル名: potato-002.pdf      接頭辞: potato
ファイル名: potato-003.pdf      接頭辞: potato
ファイル名: pumpkin-001.pdf     接頭辞: pumpkin
ファイル名: pumpkin-002.pdf     接頭辞: pumpkin
ファイル名: pumpkin-003.pdf     接頭辞: pumpkin
ファイル名: pumpkin-004.pdf     接頭辞: pumpkin
ファイル名: pumpkin-005.pdf     接頭辞: pumpkin

さらにこちら。

# 正規表現
# 先頭から、ハイフン以外の文字の連続部分を接頭辞として使用
# ハイフン以降、拡張子以前の部分を連番部分として使用
$pattern = '^([^-]+)-(.+)\.pdf$'

# 接頭辞と連番部分をグループ化してキャプチャ
Get-ChildItem -File | Where-Object {$_.Name -match $pattern} | ForEach-Object {Write-Output "ファイル名: $($Matches[0])`t接頭辞: $($Matches[1])`t連番部分: $($Matches[2])"}

このようになるはずです。

ファイル名: apple-001.pdf       接頭辞: apple   連番部分: 001
ファイル名: apple-002.pdf       接頭辞: apple   連番部分: 002
ファイル名: apple-003.pdf       接頭辞: apple   連番部分: 003
ファイル名: caramel-001.pdf     接頭辞: caramel 連番部分: 001
ファイル名: caramel-002.pdf     接頭辞: caramel 連番部分: 002
ファイル名: caramel-003.pdf     接頭辞: caramel 連番部分: 003
ファイル名: caramel-004.pdf     接頭辞: caramel 連番部分: 004
ファイル名: potato-001.pdf      接頭辞: potato  連番部分: 001
ファイル名: potato-002.pdf      接頭辞: potato  連番部分: 002
ファイル名: potato-003.pdf      接頭辞: potato  連番部分: 003
ファイル名: pumpkin-001.pdf     接頭辞: pumpkin 連番部分: 001
ファイル名: pumpkin-002.pdf     接頭辞: pumpkin 連番部分: 002
ファイル名: pumpkin-003.pdf     接頭辞: pumpkin 連番部分: 003
ファイル名: pumpkin-004.pdf     接頭辞: pumpkin 連番部分: 004
ファイル名: pumpkin-005.pdf     接頭辞: pumpkin 連番部分: 005

n 番目のグループ化部分は、 $Matches[n] に入ります。

それから、'sweet-potato-001.pdf' のように、接頭辞にもハイフンが入っているファイル名があったら ^(.+)-\d+\.pdf$ などとします。ファイル名に合わせてがんばってください。

フォルダ作成

正規表現の結果をもとにフォルダを作りましょう。

次のファイルがあるディレクトリで、以下の正規表現を使って「完成形」のコードを試してください。

SCAN_20210929-090030111.pdf
SCAN_20210930-120035111.pdf
SCAN_20210930-120035222.pdf
SCAN_20211001-094406111.pdf
SCAN_20211001-094406222.pdf
SCAN_20211001-094406333.pdf
SCAN_20211002-123013111.pdf
SCAN_20211002-123013222.pdf
SCAN_20211002-182154111.pdf
SCAN_20211002-182154222.pdf
SCAN_20211002-182155111.pdf
SCAN_20211003-083123111.pdf

'SCAN_20210929' というフォルダ名にする場合はこうです。

# 'SCAN_20210929'
$pattern = '^(SCAN_\d{8})-\d{9}\.pdf$'

# 「完成形」のコードを実行する

'20210929-0900' というフォルダ名にする場合はこうです。

# '20210929-0900'
$pattern = '^SCAN_(\d{8}-\d{4})\d{5}\.pdf$'

# 「完成形」のコードを実行する

'2021年09月29日' というフォルダ名にする場合は、以下のようにしてください。

# '2021年09月29日'
$pattern = '^SCAN_(\d{4})(\d{2})(\d{2})-\d{9}\.pdf$'

Get-ChildItem -File | Where-Object {$_.Name -match $pattern} | ForEach-Object {$destinationPath = "$($Matches[1])$($Matches[2])$($Matches[3])日"; if ((Test-Path $destinationPath) -eq $false) {New-Item -Name $destinationPath -ItemType Directory}; $_ | Move-Item -Destination $destinationPath}

'2021\0929' という階層にする場合は、以下のようにしてください。

# `yyyy\MMdd`
$pattern = '^SCAN_(\d{4})(\d{4})-\d{9}\.pdf$'

Get-ChildItem -File | Where-Object {$_.Name -match $pattern} | ForEach-Object {$destinationPath = "$($Matches[1])\$($Matches[2])"; if ((Test-Path $destinationPath) -eq $false) {New-Item -Name $destinationPath -ItemType Directory}; $_ | Move-Item -Destination $destinationPath}

'2021\0929' という階層にしつつ、その日付のファイルがひとつしかない場合はフォルダの作成と移動をしない場合は、以下のようにしてください。そんなことあるか分かりませんが。

# `yyyy\MMdd`
$pattern = '^SCAN_(\d{4})(\d{4})-\d{9}\.pdf$'
$patternSub = '(\d{4})\\(\d{4})'

Get-ChildItem -File | Where-Object {$_.Name -match $pattern} | Group-Object -Property {"$($Matches[1])\$($Matches[2])"} | ? {$_.Count -gt 1} | ForEach-Object {$regexResult = [regex]::Match($_.Name, $patternSub); $destinationPath = "$($regexResult.Groups[1])\$($regexResult.Groups[2])"; if ((Test-Path $destinationPath) -eq $false) {New-Item -Name $destinationPath -ItemType Directory}; $_.Group | Move-Item -Destination $destinationPath}

まとめ

もしかしたらね、スキャナにね、フォルダ分けしながらスキャンする機能があるかもしれないけどね、PowerShell 使いたいからね。

今回の正規表現はもっとすっきり書けます。私は意図しないものがマッチするのを防ぐため、必要な部分以外も書いています。好みや状況に応じて改良や簡略化をしてください。

参考

更新履歴