VBScriptでバイナリデータを扱う
VBScript(VBS、WSH)でバイナリデータ・バイナリファイルの扱いについて調べてみると、なかなか上手く行うのは難しいものであるようです。ここでは、これをやれば一応可能です、ということで共有します。
要約
バイナリファイルの読み書き自体は、ADODBを使えば可能です。
しかし、これで扱うデータはBytes()という型のオブジェクト(以下、バイト列
と称します)で、これはVBScriptにとって標準的な型ではなく、生成や編集ができない、配列っぽくて配列ではない扱いにくいものです。
しかし、DOMDocumentを使えば、このバイト列(例:{15,00,255}
)と16進数文字列(例:"0F00FF"
)の相互変換が可能になります。一度変換してしまえば16進数文字列は単なる文字列なので、速度を気にしなければMIDやREPLACEなどで抽出や加工が可能ですし、数値配列に変換することもできます。
ライブラリ
ADODB.Stream
前回にも出てきたADODBです。Typeを1にするとバイナリになります。Read()
で全データが取れます。
全データを取っていますが、ReadTextで全件取るのとは異なりバイナリだと速いようなのでそのまま使えそうです。
やろうとすれば一部範囲のみをとることはできます。
MSXML2.DOMDocument
MSXML2.DOMDocumentは文字通り本来DOMを扱うもので、XMLの解析など出来ます。ネット越しに読み込む機能もあるようです。
DataTypeをbin.hex
というものに指定すると、dom.Textが16進数文字列、NodeTypedValueがバイト列に対応するようになり片側に値を代入すると反対側に変換後の値が入っている状態になります。
見つけたサンプルはMicrosoft.XMLDOM
でしたが、こちらの方が後継で同じことができました。
実装:バイナリファイルを16進数文字列で
二つのライブラリを同時に利用して、VBSの変数を経由せずに片方の戻り値をもう片方の引数に直接渡します。
DOMDocumentの処理はかなり速く、速度について意識する必要(あるいは余地)はありません。
Function loadBinaryToHexString(filename)
loadBinaryToHexString = ""
Dim dom : Set dom = CreateObject("MSXML2.DOMDocument").createElement("temp")
dom.DataType = "bin.hex"
Dim stream: Set stream = WScript.CreateObject("ADODB.Stream")
stream.Type = 1 ' adTypeBinary
stream.Open
stream.LoadFromFile filename
dom.NodeTypedValue = stream.Read()
loadBinaryToHexString = UCASE(dom.Text)
stream.Close
Set stream = Nothing
Set dom = Nothing
End Function
Function saveHexStringToBinary(filename, hexStr)
saveHexStringToBinary = false
Dim dom : Set dom = CreateObject("MSXML2.DOMDocument").createElement("temp")
dom.DataType = "bin.hex"
Dim stream: Set stream = WScript.CreateObject("ADODB.Stream")
stream.Type = 1 ' adTypeBinary
stream.Open
dom.Text = hexStr
stream.Write dom.NodeTypedValue
stream.SaveToFile filename , 2 ' adSaveCreateOverWrite
stream.Close
Set stream = Nothing
Set dom = Nothing
saveHexStringToBinary = true
End Function
2文字で1バイトを表すので、文字列長の半分がファイルサイズになります。
MIDでiバイト目のデータを取り出すなら、CBYTE("&H" & MID(hexStr, i*2 + 1, 2)となります。
実装:16進数文字列を数値配列で
数値配列(ここではByte型)の方が処理しやすいということで、16進数文字列と数値配列の相互変換です。
Function convertHexStringToByteArray(hexStr)
Dim i
Dim size : size = LEN(hexStr) / 2
Dim byteArray() : ReDim byteArray(size - 1)
For i= 0 To size - 1
byteArray(i) = CBYTE("&H" & MID(hexStr,i*2 + 1,2))
Next
convertHexStringToByteArray = byteArray
End Function
Function convertByteArrayToHexString(byteArray)
Dim i
Dim size : size = UBOUND(byteArray) + 1
Dim hexArray() : ReDim hexArray(size - 1)
For i= 0 To size - 1
hexArray(i) = RIGHT("00" & HEX(byteArray(i)), 2)
Next
convertByteArrayToHexString = JOIN(hexArray,"")
End Function
~~~
[前回](https://qiita.com/Tabito/items/3772ec852908c7a1988f "【VBScript】使える時に使いたいVBScript(WSH)のコード")にも書いた通り、文字列の結合は文字列配列に入れてJOINするのが速いです。
とはいえ、ループ回数が多いので、どちらの変換もそれなりにモタつきます。
## 使ってみる
使ってみます。出力の所はコメント化して可読性を上げています。
```vb
'WScript.Echo(TIME & " : load start")
Dim hexStr : hexStr = loadBinaryToHexString("C:\_Work_\vbScript\BinaryFileUtil\input.png")
'WScript.Echo(TIME & " : loaded")
'WScript.Echo(TIME & " : dump:" & Left(hexStr,128) & "...")
'WScript.Echo(TIME & " : filesize :" & (LEN(hexStr)/2))
Dim byteArray : byteArray = convertHexStringToByteArray(hexStr)
'WScript.Echo(TIME & " : converted array")
'WScript.Echo(byteArray(1) & "," & byteArray(2) & "," &byteArray(3) & vbcrlf _
' & CHR(byteArray(1)) & "," & CHR(byteArray(2)) & "," & CHR(byteArray(3)))
Dim w : w = ((byteArray(16 + 0) * 256 + byteArray(16 + 1) ) * 256 + byteArray(16 + 2) ) * 256 + byteArray(16 + 3)
Dim h : h = ((byteArray(20 + 0) * 256 + byteArray(20 + 1) ) * 256 + byteArray(20 + 2) ) * 256 + byteArray(20 + 3)
'WScript.Echo(TIME & " : imagesize:" & w & "x" & h)
saveHexStringToBinary "C:\_Work_\vbScript\BinaryFileUtil\output.png", hexStr
'WScript.Echo(TIME & " : saved 1")
h = FIX(h / 2)
byteArray(20 + 3) = h mod 256 : h = FIX(h / 256)
byteArray(20 + 2) = h mod 256 : h = FIX(h / 256)
byteArray(20 + 1) = h mod 256 : h = FIX(h / 256)
byteArray(20 + 0) = h mod 256
hexStr = convertByteArrayToHexString(byteArray)
'WScript.Echo(TIME & " : converted hex")
saveHexStringToBinary "C:\_Work_\vbScript\BinaryFileUtil\output2.png", hexStr
'WScript.Echo(TIME & " : saved 2")
PNGファイルは、(0バイト目から数えて)1~3バイトにはPNGという文字列が入っています。
また、16~19バイト目に横方向のサイズ、20~23バイト目に縦方向のサイズが入っています。
それらを表示して、無変換の16進数文字列をoutput.pngで保存。
縦方向のサイズを半分にして、20~24バイト以降を更新してからoutput2.pngで保存してみます。
C:_Work_\vbScript\BinaryFileUtil>cscript //nologo SamplePng.vbs
7:25:14 : load start
7:25:14 : loaded
7:25:14 : dump:89504E470D0A1A0A0000000D49484452000014000000140008020000003957E4D
1000000097048597300000EC400000EC401952B0E1B0000200049444154789C...
7:25:14 : filesize :13342807
7:25:25 : converted array
80,78,71
P,N,G
7:25:25 : imagesize:5120x5120
7:25:25 : saved 1
7:25:37 : converted hex
7:25:37 : saved 2
output.pngとして同じファイルが保存できています。
output2.pngはファイルサイズは同じでヘッダしか違わないわけですが、サムネイルで見ると分かるとおり縦が半分として扱われています。(もちろん厳密には不正なファイルなので実用はできませんけれど)
速度について
上記出力の通り、13MBのPNGファイル(縦横5120pxの正方形)を、読出と保存、バイト列と16進数文字列の相互変換は一瞬で行う事ができます。一方で、16進数文字列と数値配列に変換するのはそれぞれ10秒少しも掛かっています。(純粋にバイト数に比例するようで、1MBなら1秒くらいですみます)
扱うファイルサイズによっては、必要な部分だけ切り出して数値配列を行ったり、16進数文字列のまま処理をしたりと、環境・やりたいこと・待てるかなどに応じた最適化が必要なようです。
余談
なお、ADODBで読み込んだバイト列ですが、むりやりMIDB関数で文字列として扱いつつ1文字を抽出。
ASCBでその文字コードを取る、という2段攻撃で16進数文字列化しなくとも数値として取り出すことが出来ます。
Dim bytes : bytes = stream.Read()
For i= 0 To UBOUND(bytes) : WScript.Echo(ASCB(MIDB(bytes,i + 1,1))) : Next
さらに、bytesをCSTRで文字列にすると、型の解釈がシンプルになるのか目に見えて高速になります。
Dim byteStr : byteStr = CSTR(stream.Read())
For i= 0 To LENB(byteStr)-1 : WScript.Echo(ASCB(MIDB(byteStr,i + 1,1))) : Next
ただし、これをしたところで、
上記の16進数文字列→数値配列化と同じくらいの時間が掛かり(文字列1バイトずつ抽出して数値変換‥‥と、やってることが同じ)なので、あまり意味はありません。DOMDocumentの16進数文字列が一瞬でできるのが強い。
加えて、保存時にはこういった事が出来ないので結局16進数文字列にしなければなりません。
VBScriptは手軽に扱えるのがメリットですが、大きいデータを扱うとなるとそれなりに大変です。最適解とかを考え出すと逆に手軽さが失われるので、こだわりすぎに注意が必要ですね。
とにかく、こうすることで可能になるよ、ということで残しておきます。
独学のため正確でない可能性があります。
(っ・x・)っ きゅ