Powershellで特定の日付以降のファイルを収集する(絵描いててらくがきまとめ作るのに困ったという話)

お久しぶりです、ているです。

突然ですが、Pixivとかで「らくがきまとめ」って文化(?)があるじゃないですか。

これを月ごととかで定期的に作る人はともかく、僕は思い出したときに作っているので、前回どこまでまとめたんだっけ?とか、 別フォルダにバラけてたりすると集めるのが面倒……とか、そういうので意外と手間を食うんですよね。

というわけで、そういった退屈な作業はPython……ではなくPowershellにやらせましょう。

(別にPythonでも全然いいんですけど、今回はいろんな人に使ってもらえるようにしたかったので。exe配布とかするの面倒だし。あとGUI作るの面倒だし)

今回は

  • 指定したフォルダ内のファイルのうち
  • 最終更新日が指定した日付以降のものを
  • 新しく作ったフォルダにコピーし
  • 日付とフォルダを設定ファイル(XML)に保存して次回以降は指定しなくてもいいようにする

という要件で作りました。設定ファイルがXMLなのは筆者がXMLの読み書きを勉強したかっただけです。

で、以下がコードです。

コード

Param(
    [string]$Date,
    [string]$Path
)

$xmlPath = (Get-Location).Path + "\collectSettings.xml"
$xml = New-Object System.Xml.XmlDocument

# 設定ファイルの読み込み
if (Test-Path $xmlPath) {
    $xml.Load($xmlPath)
    $lastDate = $xml.root.lastDate
    $folderPath = $xml.root.folderPath
} else {
    # ない場合は初回起動
    $Date = Read-Host "収集したい最古の日付を指定してください:"
    $Path = Read-Host "対象のフォルダを指定してください:"
    $firstLine = $xml.CreateXmlDeclaration("1.0", $null, $null)
    $xml.AppendChild($firstLine) | Out-Null
    $rootNode = $xml.CreateElement("root")
    $lastDateNode = $xml.CreateElement("lastDate")
    $folderPathNode = $xml.CreateElement("folderPath")
    $rootNode.AppendChild($lastDateNode) | Out-Null
    $rootNode.AppendChild($folderPathNode) | Out-Null
    $xml.AppendChild($rootNode) | Out-Null
}

# Date型にパースする
if (![string]::IsNullOrEmpty($Date)) {
    try {
        $lastDate = [DateTime]::ParseExact($Date, "yyyyMMdd", $null)
    } catch [System.FormatException] {
        Write-Output "日付はyyyyMMddの形で指定してください。例えば、2021年1月1日であれば、20210101と入力します。"
        Write-Output ("your input:" + $Date)
        exit
    }
}

# Pathが指定されていたらfolderPathを更新する
if (![string]::IsNullOrEmpty($Path)) {
    if (!(Test-Path $Path)) {
        Write-Output "指定したフォルダが存在しません。"
        exit
    }
    $folderPath = Resolve-Path $Path
}

$newestDate = $lastDate

# 集めるためのフォルダを作成する
$today = Get-Date -Format "yyyyMMdd"
$newFolderPath = $lastDate.ToString("yyyyMMdd") + "_" + $today
New-Item -Name $newFolderPath -ItemType Directory | Out-Null

# folderPath内のlastDateより後のファイルを探索して処理を行っていく
# また、一番遅い日付を認識したらnewestDateを更新する
Get-ChildItem -Path $folderPath -Recurse -File | Where-Object { $_.LastWriteTime -gt $lastDate } | ForEach-Object {
    Copy-Item $_.FullName -Destination $newFolderPath
    if ($_.LastWriteTime -gt $newestDate) {
        $newestDate = $_.LastWriteTime
    }
}

# 設定ファイル(XML)に書き込む
$xml.root.lastDate = $newestDate.ToString("yyyyMMdd")
$xml.root.folderPath = $folderPath.ToString()
$xml.Save($xmlPath)

使い方

※もしあなたがPowershellを使ったことがないのであれば、Powershellとは何か、実行にあたってのセキュリティリスクは何か、といったことを調べてから使ってください。

※データ破損等、このスクリプトを実行した場合の損害に関しては筆者は一切責任を負いません。

上記のコードをコピーして、メモ帳等のテキストエディタを開いてペーストし、collectNewFile.ps1とか適当な名前で保存します。

できたps1ファイルを、右クリックから実行するなり、Powershellを開いて実行するなりします。適宜調べてください。

「このシステムではスクリプトの実行が無効になっているため、ファイル collectNewItem.ps1 を読み込むことができません。」とかいうメッセージが出た場合は、以下を自己責任で実行しましょう。

Set-ExecutionPolicy RemoteSigned

初回起動時は日付とフォルダの入力を求められるので、集めたい一番古いファイルの日付と、対象のフォルダパスを入力します。日付は"yyyyMMdd"(年4文字、月2文字、日2文字)の形式でないと受け付けません。

このスクリプトではファイルを収集するときに一番新しいファイルの最終更新日を記憶するため、次回以降の起動時には前回スクリプトを実行したとき以降に作られたファイルのみを勝手に収集してくれます。

また、2回目以降実行時でも、-Date -Path オプションを使用することで任意の日付・フォルダにて実行させることができます。

技術的な話

DateTime型へのパースについて

最初はTryParseExactを使ってその戻り値でパース可能かどうか判定しようとしていたんですけど、うまくいかなかったのでParseExactをtry~catchする形式に変えました。

例外処理はもっときちんとやったほうがいいと思いますが、趣味のスクリプトなんでそこまでちゃんとしていません。

XMLの更新について

XMLへのアクセスがドットでできるのは楽でいいですね。

ただ、読み込みじゃなくて更新をしたいという場合に、読み込んだ値をそのまま突っ込んだらエラーになるのは え~ って感じでした。

そもそもPathが指定されたときに使うResolve-Pathの戻り値が文字列ではないので、そこと処理を統一できて(ToString)いるから別にいいんですが……

XmlDocument.Saveの仕様

これが一番苦戦しました。

PowershellではXmlDocument.Saveでファイルパスを指定する際に、絶対パスでないと受け付けてくれません。

参考

要するにPowershellでは実行時のカレントディレクトリがどこなのか内部的にははっきりしてないから、相対パスが信用できないってことらしい。

というわけで、ユーザがこのスクリプトを実行しているカレントディレクトリを取得して(Get-Location)、絶対パスとして保存することにしたのでした。ちゃんちゃん。

おわり

これでらくがきまとめを作るのが多少楽になったと思います。

このスクリプト、もちろん絵以外でも使えるので、調整してスケジュール処理に使ってもいいかもしれませんね。

ではまた。いつもの漫画感想も更新できるよう頑張りたいです。

(追記)オプション -Dateが機能していなかったので直しました。あと、対象フォルダ内にフォルダがある場合に再帰的に処理をするようにしました。