序文:レガシーの静寂に潜む「表記揺れ」という名のノイズ
現代のエンタープライズシステムにおいて、データクレンジングは永遠の課題である。特に、長年運用されてきたレガシーなWindows環境において、顧客名簿や製品マスタの「表記揺れ」は、自動化の歯車を狂わせる致命的な摩擦係数となる。
「株式会社」と「(株)」、あるいは「サーバー」と「サーバ」。人間には自明なこれらの一致を、システムは冷酷に「別物」として切り捨てる。我々アーキテクトに課せられた使命は、この曖昧な現実を論理的な数値へと変換し、自動補正のスキームを構築することだ。
今回は、Windows Script Host (WSH) 環境下において、外部ライブラリを一切頼らず、VBScriptのみで「レーベンシュタイン距離(編集距離)」を算出する極限の実装を解説する。これは、メモリ管理が制限されたVBScriptにおいて、計算量とリソース消費の均衡を保つための真剣勝負である。
—
1. レーベンシュタイン距離:アルゴリズムの真髄
レーベンシュタイン距離とは、一方の文字列をもう一方の文字列に変形させるために必要な「挿入」「削除」「置換」の最小回数を定義したものである。
VBScriptでこれを実装する際、最も警戒すべきは計算コストだ。二つの文字列の長さを $M$ と $N$ とした場合、単純な動的計画法(DP)では $O(MN)$ の空間計算量を必要とする。数千件のレコードを突き合わせる際、VBScriptのバリアント型配列がメモリをいかに圧迫するか、熟練のエンジニアであれば容易に想像がつくだろう。
2D配列の罠と、1D配列への圧縮
通常のリファレンスでは $M \times N$ の二次元配列を用いた解説がなされるが、実戦的なアーキテクトはこれを選択しない。現在の行の計算には「直前の行」の情報さえあれば事足りるからだ。空間計算量を $O(\min(M,N))$ にまで圧縮する。これが、限られたWSH環境でパフォーマンスを絞り出すための定石である。
—
2. 極限の実装:VBScriptによる高速類似度算出
以下に、メモリ効率を最大限に高めたレーベンシュタイン距離算出関数を示す。このコードは、単なる計算に留まらず、入力値のバリデーション、メモリの明示的な管理、そして実行速度への配慮を凝縮している。
‘
‘ 関数名: GetLevenshteinDistance
‘ 概要 : 二つの文字列間のレーベンシュタイン距離を算出する
‘ 引数 : strS1 – 比較元文字列
‘ strS2 – 比較先文字列
‘ 戻り値: 編集距離(整数)。エラー時は -1
‘ 備考 : 空間計算量を最適化し、1次元配列による計算を行う
‘
Function GetLevenshteinDistance(ByVal strS1, ByVal strS2)
On Error Resume Next
Dim n, m, i, j, cost
Dim prevRow(), currRow()
Dim charS1, charS2
n = Len(strS1)
m = Len(strS2)
‘ 空文字チェック:一方が空なら、もう一方の長さが距離となる
If n = 0 Then GetLevenshteinDistance = m: Exit Function
If m = 0 Then GetLevenshteinDistance = n: Exit Function
‘ メモリ効率のため、短い方の文字列をベースにする
If n < m Then
Dim tmpStr, tmpLen
tmpStr = strS1: strS1 = strS2: strS2 = tmpStr
tmpLen = n: n = m: m = tmpLen
End If
' 配列の動的確保(1次元配列を2つ使い、空間計算量を削減)
ReDim prevRow(m)
ReDim currRow(m)
' 初期化
For j = 0 To m
prevRow(j) = j
Next
' 動的計画法による計算
For i = 1 To n
currRow(0) = i
charS1 = Mid(strS1, i, 1)
For j = 1 To m
charS2 = Mid(strS2, j, 1)
If charS1 = charS2 Then
cost = 0
Else
cost = 1
End If
' 最小値の算出(挿入、削除、置換の最小コストを選択)
currRow(j) = Min3(currRow(j - 1) + 1, _
prevRow(j) + 1, _
prevRow(j - 1) + cost)
Next
' 次の行へ情報を引き継ぎ
For j = 0 To m
prevRow(j) = currRow(j)
Next
Next
GetLevenshteinDistance = prevRow(m)
' 明示的なメモリ解放(VBScriptのガベージコレクションを補助)
Erase prevRow
Erase currRow
If Err.Number <> 0 Then
GetLevenshteinDistance = -1
Err.Clear
End If
End Function
‘ 3つの値から最小値を返す補助関数
Function Min3(a, b, c)
Dim minVal
minVal = a
If b < minVal Then minVal = b
If c < minVal Then minVal = c
Min3 = minVal
End Function
---
3. 実務への適用:表記揺れの自動検知スクリプト
このアルゴリズムを単体で動かすだけでは不十分だ。現場では、CSVファイルやデータベース上のマスタデータと突き合わせ、閾値(Threshold)を超えたものを「名寄せ候補」として抽出する仕組みが必要となる。
パフォーマンスの重みを知る
VBScriptはインタープリタである。数万件の総当たり($O(N^2)$)を行えば、処理時間は指数関数的に増大する。実戦的なアプローチとしては以下の戦略を推奨する。
1. インデックスの事前フィルタリング: 先頭1文字が一致するものだけを比較対象とする、あるいは文字列長が極端に違うものは計算から除外する。
2. 正規化(Normalization): 全角半角の統一、空白の除去、大文字小文字の統一を事前に行うことで、計算精度を飛躍的に高める。
‘ 表記揺れチェックの実装例
Sub AuditCustomerData(targetName, masterArray)
Dim i, dist, similarity
Dim threshold: threshold = 0.7 ‘ 類似度70%以上を抽出
For i = 0 To UBound(masterArray)
‘ 距離の算出
dist = GetLevenshteinDistance(targetName, masterArray(i))
‘ 類似度への変換 (1 – 距離 / 最大長)
Dim maxLen: maxLen = Len(targetName)
If Len(masterArray(i)) > maxLen Then maxLen = Len(masterArray(i))
similarity = 1 – (dist / maxLen)
If similarity >= threshold Then
WScript.Echo “候補発見: [” & targetName & “] -> [” & masterArray(i) & “] (類似度: ” & FormatPercent(similarity, 0) & “)”
End If
Next
End Sub
—
4. アーキテクトの視点:Windows APIへの接続と限界
VBScriptには、C言語のような低レベルなメモリ操作は存在しない。しかし、COMオブジェクト(`ADODB.Recordset`や`Scripting.Dictionary`)を介したデータ操作においては、オブジェクトのライフサイクル管理がパフォーマンスの鍵を握る。
特に、大量の文字列を扱う場合、`String`型の連結操作は内部でメモリの再確保を繰り返すため、極めて遅い。レーベンシュタイン距離の計算過程で文字列操作を最小限にするため、上記のコードでは `Mid` 関数による1文字抽出をループの最小単位に留めている。
さらに高速化が必要な場合は、VBScriptからActiveX DLL(VB6やC++で作成)を呼び出す構成を検討すべきだが、標準環境のみでの運用を求められる保守現場においては、上記のような「1次元配列への圧縮」と「バリアント型への依存最小化」こそが、最高峰の最適化となる。
—
結言:枯れた技術に命を吹き込む
レーベンシュタイン距離の実装は、アルゴリズムとしては古典的である。しかし、それをVBScriptという制約の多い環境で、いかに効率的かつ堅牢に実装するかという問いには、エンジニアの地力が如実に現れる。
最新のフレームワークを追うことも重要だが、目の前のレガシーシステムを、洗練されたアルゴリズムで救い出す。その静かな快感こそが、我々チーフアーキテクトが技術の深淵に立ち続ける理由である。
このスクリプトが、貴殿の管理するシステムの静寂を守る一助となれば幸いである。
