【制御文字クリーニング】Chr / Asc 関数を用いた不可視の制御コードや外字の検出・一括除去ロジック
外部システムから連携されるCSV、固定長ファイル、あるいはWeb APIのレスポンス。
このテキストデータを読み込んだ瞬間、原因不明のデータベースエラーや、画面レイアウトの崩壊に直面したことはないだろうか。
犯人は大抵、目に見えない「制御文字」や「不正なバイト列」だ。
特にレガシーなメインフレームや、異種OS間連携のテキストには、ヌル文字(`Chr(0)`)や垂直タブ、予期せぬ改行コード、さらには文字化けを引き起こす外字や制御コードが混入している。これらを放置したままRDBにインサートしたり、Excelマクロで処理しようものなら、必ずどこかでシステムは沈黙する。
今回は、VBScript / WSHのプリミティブな機能を極限まで利用し、「不可視の敵」を完全に駆逐する堅牢な制御文字クリーニングロジックを伝授する。
—
なぜ「なんとなくの置換」では失敗するのか?
多くの開発者は、データがおかしくなると以下のような安易なコードを書く。
‘ 【アンチパターン】場当たり的な置換処理
strData = Replace(strData, vbCrLf, “”)
strData = Replace(strData, vbTab, “”)
このアプローチが実務で必ず破綻する理由は3つある。
1. 対象範囲の狭さ: `vbCrLf` や `vbTab` 以外の制御文字(`Chr(0)` や `Chr(7)` のベル文字など)がスルーされる。
2. 文字コードの罠: VBScriptの `String` 型は内部でUTF-16(OLECHAR)として保持される。単なるバイト列の置き換えでは、サロゲートペアやマルチバイト文字の境界を破壊する危険性がある。
3. 不可視性の恐怖: ログに出力しても見えないため、デバッグ時に「なぜ文字数が合わないのか」「なぜSQLの構文エラーになるのか」が判別できない。
プロの業務自動化エンジニアであれば、「通すべき文字のホワイトリスト(または厳密なブラックリスト)」を定義し、Asc/Chr関数を用いてスキャン・パージする仕組みを構築しなければならない。
—
制御文字クリーニングのアーキテクチャ設計
堅牢なクレンジングエンジンをVBScriptで実装するにあたり、以下の設計方針を遵守する。
- 処理対象の定義: ASCII制御文字(`0x00` から `0x1F` まで)のうち、通常のテキストで許容される改行(CR/LF)とタブ以外のすべてをパージ対象とする。
- パフォーマンスの担保: VBScriptの文字列結合(`&`)をループ内で多用すると、メモリの再割り当てが発生し劇的に遅くなる。大量データを扱う場合は `RegExp`(正規表現オブジェクト)を併用するのがベストプラクティスだが、極限環境(WSHのセキュリティ制限や外部コンポーネントが使えない状況)を考慮し、純粋な `Chr/Asc` ベースの走査ロジックもマスターしておく必要がある。
- 文字コードの安全地帯: `AscW` 関数を使用し、Unicode(UTF-16)のコードポイントベースで判定を行う。これにより、マルチバイト文字(日本語など)を誤って破壊するリスクを排除する。
—
プロダクションコード:堅牢な制御文字除去スクリプト
実務の現場でそのままコピー&ペーストして利用できる、プロダクション品質のVBScriptコードを提示する。
このスクリプトは、ファイルからテキストを読み込み、不可視の制御文字を完全除去した上で、安全に別ファイルへ出力する完結したモジュールである。
‘ ==============================================================================
‘ スクリプト名: TextCleaner.vbs
‘ 概要 : 外部テキストの制御文字(不可視コード)を検出・一括除去する
‘ 依存関係 : なし (WSH標準機能のみで動作)
‘ ==============================================================================
Option Explicit
‘ メイン処理の実行
Main()
Sub Main()
Dim fso, targetPath, outputPath
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ 入出力パスの定義(環境に合わせて変更してください)
targetPath = fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), “input_data.txt”)
outputPath = fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), “output_clean.txt”)
If Not fso.FileExists(targetPath) Then
WScript.Echo “エラー: 入力ファイルが見つかりません -> ” & targetPath
Exit Sub
End If
‘ ファイルの読み込み (第3引数 -1 はSystem Default / Unicode対応時は -1 または True)
Dim tsRead, rawData
Set tsRead = fso.OpenTextFile(targetPath, 1, False, 0)
rawData = tsRead.ReadAll
tsRead.Close
WScript.Echo “クレンジング前の文字数: ” & Len(rawData)
‘ 制御文字クリーニングの実行
Dim cleanData
cleanData = RemoveControlCharacters(rawData)
WScript.Echo “クレンジング後の文字数: ” & Len(cleanData)
‘ クリーンデータの書き出し (UTF-8等で書き出す場合はADODB.Stream推奨だが今回はFSOを使用)
Dim tsWrite
Set tsWrite = fso.CreateTextFile(outputPath, True, False)
tsWrite.Write cleanData
tsWrite.Close
WScript.Echo “処理完了。出力ファイル: ” & outputPath
Set tsWrite = Nothing
Set tsRead = Nothing
Set fso = Nothing
End Sub
‘ ——————————————————————————
‘ 関数名: RemoveControlCharacters
‘ 概要 : 文字列中の不正な制御文字(ChrW(0)〜ChrW(31)のうち一部を除く)を除去する
‘ ——————————————————————————
Function RemoveControlCharacters(ByVal strInput)
If IsNull(strInput) Or strInput = “” Then
RemoveControlCharacters = “”
Exit Function
End If
Dim i, charCode, sb
‘ VBScriptでの効率的な文字列バッファとして配列を使用
Dim arrChars()
ReDim arrChars(Len(strInput) – 1)
Dim outIndex
outIndex = 0
For i = 1 To Len(strInput)
‘ AscW を使用してUnicodeコードポイントを取得(負数の考慮含む)
charCode = AscW(Mid(strInput, i, 1))
If charCode < 0 Then
charCode = charCode + 65536 ' 符号なし16ビット整数へ変換
End If
' 判定ロジック:
' ASCII制御文字領域 (0x00 〜 0x1F) のうち、以下の「許可文字」以外を排除する
' 許可する制御文字:
' 9 = Tab (HT)
' 10 = Line Feed (LF)
' 13 = Carriage Return (CR)
' ※ 0 (Null文字) や 7 (Bell), 11 (VT), 12 (FF), 26 (EOF) などを確実に排除する
Select Case charCode
Case 9, 10, 13
' 許可された制御文字はそのまま通す
arrChars(outIndex) = ChrW(charCode)
outIndex = outIndex + 1
Case 0 To 31
' その他の制御文字はスキップ(除去)
' 必要に応じてログ出力やカウンターをここに仕込む
Case 127
' DEL文字も排除
Case Else
' 通常文字および拡張文字(日本語等)は通す
If charCode >= 32 Then
arrChars(outIndex) = ChrW(charCode)
outIndex = outIndex + 1
End If
End Select
Next
‘ 有効な文字部分のみを切り出して結合
If outIndex > 0 Then
ReDim Preserve arrChars(outIndex – 1)
RemoveControlCharacters = Join(arrChars, “”)
Else
RemoveControlCharacters = “”
End If
End Function
—
コードの急所:プロフェッショナルの解説
このコードが「なぜ現場で通用するのか」、アーキテクチャの急所を解説する。
1. `AscW` と符号付き整数の罠
VBScriptの `Asc` 関数はANSIコードを返すため、マルチバイト環境やUnicode混入時に意図しない挙動を引き起こす。そのため `AscW` を使用しているが、VBScriptの `Integer` 型は符号付き(-32768 〜 32767)であるため、上位のコードポイントが負数として返されることがある。
コード内の `If charCode < 0 Then charCode = charCode + 65536` という補正処理は、サロゲートペア領域や特殊文字を扱う上で絶対に外せないプロの技法である。
2. 配列と `Join` によるメモリ最適化
数万行のテキストを処理する際、ループ内で `result = result & char` を行うと、VBScriptの内部でメモリの再割り当てが頻発し、処理時間が幾何級数的に増大する($O(N^2)$ の劣化)。
あらかじめ入力文字列長と同じサイズの配列 `arrChars()` を確保し、最後に `Join` 関数で一気に結合する方式($O(N)$)を採用することで、巨大なファイルでも高速に処理が完了する。
—
ファイル・データベース連携時の実務的注意点
このクリーニングスクリプトを業務システムに組み込む際、以下の点に留意してほしい。
- BOM付きUTF-8の扱い: `FileSystemObject.OpenTextFile` はデフォルトではANSI(Shift-JIS等)でファイルを読み込む。もしUTF-8(BOM付き/無し)のファイルを扱う場合は、FSOではなく `ADODB.Stream` オブジェクトを使い、ライブラリ側で文字コードを明示的に指定してメモリ上にロードしてから、上記の `RemoveControlCharacters` に流し込む設計にすべきである。
- データベース(SQL Server / Oracle等)へのインサート前処理: 不可視の制御文字(特に `Chr(0)` ヌル文字)が混入したデータをそのままADO経由でSQL Serverに送ると、「文字列またはバイナリ データが切り捨てられます」 という誤解を招くエラーや、予期せぬトランザクション中断を引き起こす。DB書き込みの直前層に、このクレンジング関数を共通バリデーションとして組み込むのが、システム停止を防ぐための最良の防衛策となる。
—
総括
VBScriptはレガシーな言語と見なされがちだが、Windows環境におけるスクリーニングやちょっとしたバッチ処理の自動化において、その手軽さと強力さはいまだに健在である。
「なぜデータが壊れるのか分からない」と嘆く前に、目に見えないバイトの向こう側を疑い、`AscW / ChrW` を使った厳密な制御文字のコントロールを手に入れてほしい。それこそが、トラブルを未然に防ぐプロフェッショナルな業務自動化エンジニアの姿勢である。
