【テクニカル・上級編】【世代別ログアーカイブ】FileSystemObject と Shell.Application を連携させた一定期間経過ログの自動ZIP圧縮 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

VBScriptを掌握する極限の知見:FileSystemObjectとShell.Applicationが織りなす「世代別ログアーカイブ」の極意

レガシーシステムの深部、あるいは厳格な閉域網のエンドポイントにおいて、VBScript(Visual Basic Scripting Edition)とWSH(Windows Script Host)はいまだにインフラストラクチャの隠れた守護神として機能している。

「今どきVBScriptか」と嘲笑する者は、OSのライフサイクルそのものを俯瞰する視座を欠いている。Windows環境であれば追加のランタイムを一切要求せず、NT系カーネルのプリミティブなAPI群に直結できるこの言語は、正しく扱いさえすれば、現代の過剰に複雑化したパイプラインをも凌駕する圧倒的な省リソース性と堅牢性を発揮する。

今回は、現場のシニアエンジニアやインフラ管理者から長年渇望されてきた「一定期間経過ログの自動ZIP圧縮・世代別アーカイブモジュール」を題材にする。
`Scripting.FileSystemObject`(以下、FSO)による高速なファイル走査と、`Shell.Application`によるOSネイティブなZIPストリーム構築を完璧に同期させ、メモリリークの恐怖から完全に解放されたプロダクション品質のコードをここに提示する。

1. アーキテクチャの核心:なぜ「FSO + Shell.Application」なのか?

ログローテーションの自動化において、最大のボトルネックとなるのは「大量のI/O処理」と「圧縮処理のオーバーヘッド」である。外部のサードパーティ製コマンドラインツール(例: 7-ZipやPowerShellの`Compress-Archive`)を持ち込む手法もあるが、厳格なセキュリティポリシーが敷かれた環境では、バイナリの持ち込みやPowerShellの実行ポリシー(ExecutionPolicy)の壁に阻まれるケースが少なくない。

ここで、Windows標準コンポーネントだけで完結するコンビネーションが真価を発揮する。

1. `Scripting.FileSystemObject`

  • ディレクトリツリーの再帰的走査、ファイルのタイムスタンプ(`DateLastModified`)の評価、およびストリーム破棄を最小のメモリフットプリントで実行する。

2. `Shell.Application` (Namespace / CopyHere)

  • Windows ShellのCOMオブジェクトを介し、OS標準のZIP圧縮機能(Compressed Folders)をプログラムからドライブする。専用の圧縮ライブラリをリンクすることなく、ネイティブに「空のZIPファイルの生成」「アイテムの非同期/同期追加」を行える。

しかし、この2つのコンポーネントを安易に組み合わせると、COMの参照カウント管理の不備によるメモリリークや、Shellオブジェクト特有の「非同期処理に起因するファイルロック競合」の罠に沈むことになる。

2. 実装:プロダクション品質・世代別ログアーカイブモジュール

以下のコードは、指定日数(デフォルト30日)を超えたログファイルを検出し、`YYYYMM`単位のZIPアーカイブへ格納した上で、元ファイルを安全に消去する完全版のVBScriptである。

実戦投入を想定し、エラーハンドリング(`On Error Resume Next`の局所化)、オブジェクトの明示的破棄、およびロギング機構を実装している。

‘ ==============================================================================
‘ Script Name : LogArchiver.vbs
‘ Description : FSO と Shell.Application を用いた月別世代ログ自動ZIPアーカイブ
‘ Author : チーフアーキテクト
‘ ==============================================================================
Option Explicit

‘ — 定数定義 —
Const TARGET_DIR = “C:\AppLogs” ‘ 対象ログのルートディレクトリ
Const ARCHIVE_DIR = “C:\AppLogs\Archive” ‘ 圧縮済みZIPの出力先
Const RETENTION_DAYS = 30 ‘ 保持期間(日数を経過したものが対象)
Const LOG_FILE = “C:\AppLogs\archiver.log”

‘ — メイン処理実行 —
Main

Sub Main()
Dim fso, targetFolder, file, files
Dim fileDate, thresholdDate
Dim archiveName, zipPath, shellApp, sourceFolder, destFolder
Dim dictMonths, key

Set fso = CreateObject(“Scripting.FileSystemObject”)

‘ アーカイブ出力先が存在しない場合は自動生成
If Not fso.FolderExists(ARCHIVE_DIR) Then
fso.CreateFolder(ARCHIVE_DIR)
End If

‘ 閾値日付の算出(現在時刻 – 保持日数)
thresholdDate = DateAdd(“d”, -RETENTION_DAYS, Now)
WriteLog “INFO: アーカイブ処理を開始します。閾値基準日: ” & thresholdDate

If Not fso.FolderExists(TARGET_DIR) Then
WriteLog “ERROR: 対象ディレクトリが存在しません: ” & TARGET_DIR
Set fso = Nothing
Exit Sub
End If

Set targetFolder = fso.GetFolder(TARGET_DIR)
Set files = targetFolder.Files

‘ 1. 対象ファイルを抽出し、年月ごとのグループ化キーを生成する(Dictionaryを使用)
Set dictMonths = CreateObject(“Scripting.Dictionary”)

For Each file in files
‘ ルート直下のログファイルのみを対象とする(サブフォルダ除外)
‘ 拡張子が .log または .txt のみを対象(必要に応じて変更)
Dim ext
ext = LCase(fso.GetExtensionName(file.Name))

If (ext = “log” or ext = “txt”) Then
fileDate = file.DateLastModified

‘ 最終更新日が閾値より古い場合
If fileDate < thresholdDate Then Dim yearMonth yearMonth = Year(fileDate) & Right("0" & Month(fileDate), 2) If Not dictMonths.Exists(yearMonth) Then dictMonths.Add yearMonth, CreateObject("Scripting.Dictionary") End If ' 同一年月ディクショナリにファイルパスを格納 dictMonths(yearMonth).Add file.Path, file.Name End If End If Next Set files = Nothing Set targetFolder = Nothing If dictMonths.Count = 0 Then WriteLog "INFO: 対象となる古いログファイルはありませんでした。" Set fso = Nothing Exit Sub End If ' 2. 年月ごとにZIPアーカイブを生成し、ファイルを格納 For Each key in dictMonths.Keys zipPath = ARCHIVE_DIR & "\Log_" & key & ".zip" ' 空のZIPファイル(ヘッダのみ)がなければ初期化生成 If Not fso.FileExists(zipPath) Then CreateEmptyZip(zipPath) End If Set shellApp = CreateObject("Shell.Application") Set destFolder = shellApp.Namespace(zipPath) If Not destFolder Is Nothing Then Dim filePath For Each filePath in dictMonths(key).Keys ' Shell.Application を用いたZIPへのコピー ' 16: 進捗ダイアログ非表示 ' 4 : 進行状況を表示しない ' 2048: 失敗時のUI表示なし destFolder.CopyHere filePath, 16 + 4 + 2048 ' コピー完了の確実な同期を担保するため短いウェイト(OSのI/O解放待ち) WScript.Sleep 200 ' 元ファイルの削除(FSO) On Error Resume Next fso.DeleteFile filePath, True If Err.Number <> 0 Then
WriteLog “WARN: ファイルの削除に失敗しました (” & filePath & “): ” & Err.Description
Err.Clear
Else
WriteLog “SUCCESS: アーカイブ&削除完了 -> ” & filePath
End If
On Error GoTo 0
Next
End If

‘ オブジェクトの明示的解放(メモリリーク防止)
Set destFolder = Nothing
Set shellApp = Nothing
Next

WriteLog “INFO: すべてのアーカイブ処理が正常終了しました。”

‘ クリーンアップ
Set dictMonths = Nothing
Set fso = Nothing
End Sub

‘ — 空のZIPファイルを強制生成するサブルーチン —
Sub CreateEmptyZip(ByVal path)
Dim fso, ts
Set fso = CreateObject(“Scripting.FileSystemObject”)
Set ts = fso.CreateTextFile(path, True)

‘ ZIPファイルの空構造(PK頭出しシグネチャ + 空ディレクトリオブジェクト構造)
‘ ※WindowsのShell.Applicationが正常に認識するためのお呪い(Magic Number)
ts.Write Chr(80) & Chr(75) & Chr(5) & Chr(6) & String(18, Chr(0))
ts.Close

Set ts = Nothing
Set fso = Nothing
End Sub

‘ — ロギングサブルーチン —
Sub WriteLog(ByVal message)
Dim fso, ts
On Error Resume Next
Set fso = CreateObject(“Scripting.FileSystemObject”)
Set ts = fso.OpenTextFile(LOG_FILE, 8, True) ‘ 8 = ForAppending
ts.WriteLine “[” & Now & “] ” & message
ts.Close
Set ts = Nothing
Set fso = Nothing
On Error GoTo 0
End Sub

3. シニアエンジニアが押さえるべき「3つの極限知見」

上記のコードが、単なるサンプルスクリプトと一線を画す「プロダクション水準」たる所以を解説する。

① `Shell.Application.CopyHere` の非同期罠と排他制御

`Shell.Application` による `CopyHere` メソッドは、内部的にバックグラウンドスレッドで非同期に処理を実行する。そのため、スクリプトが連続してファイルをZIPに放り込もうとすると、ZIP内部のファイルインデックスの更新が追いつかずに「ファイルが使用中です」エラーや破損を引き起こす。
これを回避するため、コード内では `WScript.Sleep 200` を挟み、OSのファイルシステムキャッシュとShellスレッドの同期を強制している。大量ファイルを扱う場合はこのウェイト調整が極めて重要となる。

② ヘッダなしZIPの悲劇を防ぐ `CreateEmptyZip` の魔術

`Shell.Application.Namespace(path)` は、対象ファイルが「有効なZIPアーカイブの構造」を持っていないと、`Nothing` を返し、`CopyHere` が無言で失敗する。
レジストリや特殊な外部ツールを使わず、VBScript単体で最小のZIPヘッダ(PK頭出しシグネチャ:`PK\005\006` とそれに続く18バイトのゼロパディング)を書き込むことで、どんなクリーン環境であっても確実にお手製のZIPコンテナを爆誕させることができる。このハックはインフラエンジニアの必須教養である。

③ COMオブジェクトのライフサイクルとメモリマネジメント

VBScriptのガベージコレクタは、スコープを抜けるまでCOMオブジェクトの参照を保持し続ける傾向がある。特にループ内で `CreateObject` や Shell Namespace を乱用すると、VBScriptプロセス(`wscript.exe` / `cscript.exe`)のメモリフットプリントが肥大化し、最終的に Out of Memory(メモリ不足)を引き起こす。
本スクリプトでは、ループのイテレーションごとに `Set destFolder = Nothing` および `Set shellApp = Nothing` を明示的に呼び出し、参照カウントを即座にゼロに落としてCOMサーバーを解放する設計を徹底している。

4. 運用・拡張のヒント(タスクスケジューラ連携)

このスクリプトを本番運用に組み込む際は、GUIのポップアップを出さないために、必ず `cscript.exe` を用いて実行すること。

cscript.exe //nologo “C:\AppLogs\LogArchiver.vbs”

Windowsの「タスク スケジューラ」に登録し、毎日深夜のトラフィックが枯渇する時間帯にサイレント実行させることで、ディスク容量の圧迫というシステム管理者の悪夢を半永久的に自動で防ぐ防壁が完成する。

枯れた技術と侮るなかれ。VBScriptのプリミティブな構造を熟知し、OSのAPIと対話するコードを書けるエンジニアこそが、真に信頼性の高いインフラを支え続けるのである。

タイトルとURLをコピーしました