Excel のマクロでスクレイピングする場合などでJSONを解きたくなることは、しばしばあるかと思います。
巷では、もっぱらVBA-JSONを利用するのが主流かと思います。これは結構辛い制約で VBS で利用できません。また、速度もさほど速くないし、そこそこ癖もありそうです。
今回自作しましたので紹介させて頂きます。ご利用は自由ですので、お仕事、ご趣味にお役立て下さい。
VBA用 https://github.com/biwaz-design/opencode/blob/main/BD-Json/json.vba
VBS用 https://github.com/biwaz-design/opencode/blob/main/BD-Json/json.vbs
VBA-JSON のパースについて
JSON の文法は下記のサイトに公開されています。
https://www.json.org/json-ja.html
これから解く JSON 文字列が、この文法に合致しているかを厳しい目線でチェックするのか?或いは、合致している前提で効率よく解き進めるのか?パーサーを作る上での大きな方針になります。
どこまで厳格にチェックするかは、ケースバイケースになるかと思いますが、VBA-JSON の方針は概ね後者寄り、且つ、一部カスタマイズ仕様を取り入れているようです。
ホワイトスペース
先に挙げた 改行、タブ などホワイトスペースの削除は、""で囲まれた文字列以外の部分で適用される文法ルールなので JSON 文字列全体に対して処理するべき内容ではありません。例えば、"""abc" & vbTab & "xyz""" に replace を適用させると、"abcxyz" と、vbTab が消えてしまいます。
本来 JSON の文法に照らせば、文字列中に vbTab(&H09) なる &H20 未満のコードが存在すればエラーになるのが正しい挙動です。
ただ、取り扱うJSON 文字列が文法に沿っている前提であれば、VBA-JSON のreplaceは、効率の良い処理であると言えます。
エスケープ
文字列中には \ エスケープが定義されています。b f n r t u \ " / これらの文字が続きます。これ以外の文字が \ の次にあれば文法違反ですが、こちらも許容されて \ が抜き取られてしまいます。
(例) v\wxyz -> vwxyz
文字列囲み
また、文字列を囲む "" の代わりに '' が利用できてしまいます。で '" といった変則囲みを読み込ませると暴走しました。正確には閉じない文字列があると暴走します。
オブジェクト,配列のカンマ
次はオブジェクト、配列の , カンマの取り扱いです。なぜか {,"key":true} [,false] といった表記が許容されます。これは効率優先というよりは、解き方が間違ってるだけかと思われます。
数値
数値の解析は非常に特徴的です。+-0123456789.eE の何れかが続くトークンを一旦「数値候補」と認識します。これを val で VBA の数値に変換します。JSON の文法からはちょっと想像できない、ざっくばらんな解き方をしてくれます。0.0.0 でも val を通すと 0 になります。 この辺りも、対象の文字列が文法に合致しているのであれば、効率的と解釈できます。
また 17桁(17文字)以上であれば「ラージナンバー」認定され、文字列として取り扱われます。読み込み元の数値データの桁数が大きい場合、書き出しし直しても桁落ちしないので、このような対応をされているのかなと思われます。もちろん副作用もあり、16桁以下の文字列の体をなす数値 "3.14159265358979" は、書き出すと""が取れて本来の数値に戻ってしまいます。 痛し痒し的な機能と言えます。
VBS/VBA 速度改善の視点について
C,C++で作成されるパーサーは、一般的に文字を1文字ずつ読み込んで解析を行っているかと思います。VBS/VBA で、この一般的な手法を使うと大概遅くなってしまいます。1文字、1文字を独立に処理をする機能に乏しいこともありますし、いくら簡素な処理であっても、行数が増えると、著しく時間が掛かってしまう特徴があるため、設計方針として、できるだけ1文字単位の解析処理を避けて、全体の実行行数を減らす必要があると考えられます。
raplace の多用
これは VBA-JSON でも 改行、タブ などホワイトスペースを削除する方法として、次のように利用されています。
JSONString = VBA.Replace(VBA.Replace(VBA.Replace(JSONString, VBA.vbCr, ""), VBA.vbLf, ""), VBA.vbTab, "")
split の利用
JSON 全体を文脈に応じてブロックに分けえる場合、instr でキーワード検出をしつつ、処理をするよりも split にて配列に分けてしまう方が速いケースがあります。
多用すると逆に遅くなりますので、本作ではリテラル部と非リテラル部を分ける為に1回だけ利用しています。
レシピの調味料は上記の2点。これで全力対応させて頂きました。
速度計測結果
本作では、VBA-JSONに習い性善説でパースを行う速度重視モードと、性悪説でパースを行う厳格モードを用意しました。速度重視モードでは、パース対象の文字列に制御文字は含まれないだろう的な前提をしています。
計測に使用した JSON 文字列は次のようなものです。
いずれのケースも、元サイズ×繰り返しパース回数が大体100MBになるようにしています。
VBA-JSON、本作共に response-2.txt の成績が芳しくないです。短い文字列(例えば "3" "1" ...)が多いと、用意している仕組みが大上段過ぎて、ロスが大きくなる。即ち遅くなるようです。
パース対象仮名称 | ファイルサイズ | 繰返パース回数 | 本作VBA(速度重視) | 本作VBA(文法重視) | VBA-JSON |
---|---|---|---|---|---|
string.txt | 1,023,855 | 100 | 23秒 | 36秒 | 38秒 |
number.txt | 1,023,555 | 100 | 35秒 | 36秒 | 41秒 |
moviedata.txt | 3,715,461 | 27 | 78秒 | 85秒 | 97秒 |
response-1.txt | 647,798 | 160 | 32秒 | 48秒 | 56秒 |
response-2.txt | 458,149 | 220 | 270秒 | 294秒 | 298秒 |
number.txt 小数点(π)で構成された2次元配列
string.txt エスケープ文字を含む文字列で構成された2次元配列
moviedata.txt AWSの説明中にあったサンプル
response-1.txt スクレイピングで取得したサンプル1
response-2.txt スクレイピングで取得したサンプル2
頑張った割には今一つでしたが、とりあえず上記のケースでは VBA-JSON より速く解くことができました。
VBA/VBS について
VBSには Collection がない、使えない為、動的配列で代替してみました。
利用時の違いとして、VBAでは配列の最初は 1 になりますが、VBSでは配列の最初は 0 になります。
それでは速度比較表の方、同様に掲載してみます。さすがに只で動くVBSでは、VBA-JSONに勝てないケースが出てくるようです。
パース対象仮名称 | ファイルサイズ | 繰返パース回数 | 本作VBS(速度重視) | 本作VBS(文法重視) | VBA-JSON |
---|---|---|---|---|---|
string.txt | 1,023,855 | 100 | 25秒 | 42秒 | 38秒 |
number.txt | 1,023,555 | 100 | 53秒 | 56秒 | 41秒 |
moviedata.txt | 3,715,461 | 27 | 45秒 | 54秒 | 97秒 |
response-1.txt | 647,798 | 160 | 46秒 | 69秒 | 56秒 |
response-2.txt | 458,149 | 220 | 135秒 | 164秒 | 298秒 |
最後に
せっかく頑張って作りましたので、本作に名前を付けておこうと思いまして、わたくしめの屋号 Biwaz Design のイニシャルを取って BD-Json にさせて頂きました。もしかして、既に使ってらっしゃる方がいらっしゃったら、ごめんなさい。ご連絡頂けましたら、変更します。