偶然看到了一段JSON解析代码,能在VBA/VB6运行。分析了一下,是利用的webbroswer处理,感觉有点奇葩。
分析
1. 初始化
在 Class_Initialize
过程里,借助 CreateObject("htmlfile")
构建一个 HTML 文档对象,并且运用 execScript
方法将 JSON 解析和序列化的 JavaScript 代码注入其中。初始化完成后,会生成一个空的 JSON 对象 JsonObj
。
2. 属性和方法
属性
Item
:用来获取或设置 JSON 对象里指定键的值。ItemEx
:可用于添加数组或者子 JSON 对象。stringify
:把 JSON 对象转换为 JSON 字符串,并且可以选择是否添加换行符。JsonStr
:获取或者设置整个 JSON 对象的字符串表示。
方法
KeysCount
:返回指定键下的子键数量。AddObject
:在指定键下添加一个新的 JSON 子对象。AddArray
:在指定键下添加一个新的数组。Delete
:删除指定键及其对应的数据。IsType
:判断指定键的数据类型是否为特定类型(如Object
、Array
、String
等)。TypeName2
:返回指定键的数据类型名称。TypeNameJs
:返回指定键的数据类型的 JavaScript 表示。GetAllKeys
和GetAllKeys2
:返回指定键下的所有子键名称。parse
:将 JSON 字符串解析为 JSON 对象。SetJsonStr
:为 JSON 对象设置一整段 JSON 字符串。GetJsonStr
:返回指定键的 JSON 字符串表示。GetJsonStrObject
:返回指定对象的 JSON 字符串表示。eval
:执行 JavaScript 代码并返回结果。Arraylength
:获取指定数组的长度。RemoveArrayByIndex
:删除数组中指定索引的元素。ArrayAdd
:往数组中添加一个新元素。
3. 示例用法
下面是一些可能的使用示例:
Sub TestXiaoYaoJson()
Dim Json As New XiaoYaoJson
' 设置 JSON 对象的值
Json.Item("name") = "John"
Json.Item("age") = 30
' 添加一个子 JSON 对象
Json.ItemEx("address") = "{""city"":""New York"",""zip"":""10001""}"
' 添加一个数组
Json.ItemEx("hobbies") = "[ ""reading"", ""swimming"", ""running"" ]"
' 获取 JSON 字符串
Dim jsonStr As String
jsonStr = Json.stringify
Debug.Print jsonStr
End Sub
4. 注意事项
- 此组件支持 32 位和 64 位的 VBA 以及 VB6,经过修改后也可在 VBS 中使用。
- 组件能够免费商用,但需要保留作者信息。
- 代码中运用了 HTML 文档对象的
execScript
方法来执行 JavaScript 代码,以此实现 JSON 的解析和序列化。
代码如下
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "HtmlJson"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
Private Doc As Object
Private HtmlWindowA As Object
Public JsonObj As Object
' 获取或设置 JSON 对象中指定键的值
Public Property Get Item(ItemName As String) As Variant
Item = HtmlWindowA.eval("JsonObj." & ItemName)
End Property
Public Property Let Item(ItemName As String, ByVal vNewValue As Variant)
If TypeName(vNewValue) = "String" Then vNewValue = """" & vNewValue & """"
HtmlWindowA.execScript "JsonObj." & ItemName & "=" & vNewValue
End Property
' 返回指定键下的子键数量
Function KeysCount(KeyName As String) As Long
HtmlWindowA.execScript "var keys = Object.keys(" & FullKey(KeyName) & ");var KeysCount=keys.length;"
KeysCount = Doc.Script.KeysCount
End Function
' 添加一个子 json 对象,方便添加新值
Function AddObject(KeyName As String) As Boolean
On Error Resume Next
HtmlWindowA.execScript FullKey(KeyName) & "={}"
AddObject = Err.Number = 0
End Function
' 添加一个数组
Function AddArray(KeyName As String) As Boolean
On Error Resume Next
HtmlWindowA.execScript FullKey(KeyName) & "=[]"
AddArray = Err.Number = 0
End Function
' 删除一个键和数据
Function Delete(KeyName As String) As Boolean
On Error Resume Next
If KeyName = "" Then Exit Function
HtmlWindowA.execScript "delete " & FullKey(KeyName)
Delete = Err.Number = 0
End Function
' 判断指定键的数据类型是否为特定类型
Function IsType(Key As String, ByVal TypeName1 As String) As Boolean
TypeName1 = LCase(TypeName1)
Select Case TypeName1
Case "object": TypeName1 = "Object"
Case "string": TypeName1 = "String"
Case "array": TypeName1 = "Array"
Case "long", "number": TypeName1 = "Number"
End Select
IsType = HtmlWindowA.eval("Object.prototype.toString.call(" & FullKey(Key) & ") === '[object " & TypeName1 & "]'")
End Function
' 检测数据类型
Function TypeName2(Key As String) As String
Dim TypeNameJsA As String
TypeNameJsA = HtmlWindowA.eval("Object.prototype.toString.call(" & FullKey(Key) & ")")
If Left(TypeNameJsA, 8) = "[object " Then
TypeName2 = Mid(TypeNameJsA, 9, Len(TypeNameJsA) - 9)
End If
End Function
' 返回指定键的数据类型的 JavaScript 表示
Function TypeNameJs(Key As String) As String
TypeNameJs = HtmlWindowA.eval("Object.prototype.toString.call(" & FullKey(Key) & ")")
End Function
' 获取指定键下的所有子键名称
Function GetAllKeys(Optional KeyName As String) As String()
Dim KeysCountA As Long
Dim KeyNameArr() As String
KeysCountA = KeysCount(KeyName)
If KeysCountA > 0 Then
Dim AllKeys As String
AllKeys = HtmlWindowA.eval("keys.join('@@--')")
KeyNameArr = Split(AllKeys, "@@--")
Else
ReDim KeyNameArr(-1 To -1)
End If
GetAllKeys = KeyNameArr
End Function
Function GetAllKeys2(Optional KeyName As String) As String()
Dim KeysCountA As Long
Dim KeyNameArr() As String
KeysCountA = KeysCount(KeyName)
If KeysCountA > 0 Then
ReDim KeyNameArr(KeysCountA - 1) As String
Dim I As Long
For I = 0 To KeysCountA - 1
KeyNameArr(I) = HtmlWindowA.eval("keys[" & I & "]")
Next
Else
ReDim KeyNameArr(-1 To -1)
End If
GetAllKeys2 = KeyNameArr
End Function
' 添加数组或对象的方法
Public Property Let ItemEx(ItemName As String, ByVal vNewValue As String)
On Error Resume Next
HtmlWindowA.execScript "JsonObj." & ItemName & "=" & vNewValue
If Err.Number <> 0 Then Debug.Print "ItemEx err:" & Err.Description
End Property
' 返回一行的 JSON 字符串
Public Property Get stringify(Optional ItemName As String, Optional WithVbcrlf As Boolean) As String
stringify = HtmlWindowA.eval("JSON.stringify(" & "JsonObj" & IIf(ItemName = "", "", "." & ItemName) & IIf(WithVbcrlf, ", null, 2", "") & ")")
End Property
' 将 JSON 字符串解析为 JSON 对象
Public Function parse(ByVal vNewValue As String) As Boolean
On Error Resume Next
Doc.Script.SetJsonStr vNewValue
parse = Err.Number = 0
End Function
' 生成完整的键路径
Private Function FullKey(Key As String) As String
FullKey = IIf(Key <> "", "JsonObj" & IIf(Left(Key, 1) = "[", "", ".") & Key, "JsonObj")
End Function
' 初始化类
Private Sub Class_Initialize()
Set Doc = CreateObject("htmlfile")
Set HtmlWindowA = Doc.parentWindow
' JSON 2.0.js 代码
Dim JsCode As String
JsCode = "if(typeof JSON!== ""object""){JSON={}}(function(){""use strict"";var g=/^[\],:{}\s]*$/;var h=/\\(?:[""\\\/bfnrt]|u[0-9a-fA-F]{4})/g;var l=/""[^""\\\n\r]*""|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;var m=/(?:^|:|,)(?:\s*\[)+/g;var o=/[\\""\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;var p=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;function f(n){return(n<10)?""0""+n:n}function this_value(){return this.valueOf()}if(typeof Date.prototype.toJSON!== ""function""){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?(this.getUTCFullYear()+""-""+f(this.getUTCMonth()+1)+""-""+f(this.getUTCDate())+""T""+f(this.getUTCHours())+"":""+f(this.getUTCMinutes())+"":""+f(this.getUTCSeconds())+""Z""):null};Boolean.prototype.toJSON=this_value;Number.prototype.toJSON=this_value;String.prototype.toJSON=this_value}var q;var r;var s;var t;function quote(b){o.lastIndex=0;return o.test(b)?""\""""+b.replace(o,function(a){var c=s[a];return typeof c===""string""?c:""\\u""+(""0000""+a.charCodeAt(0).toString(16)).slice(-4)})+""\"""":""\""""+b+""\""""}function str(a,b){var i;var k;var v;var c;var d=q;var e;var f=b[a];if(f&&typeof f===""object""&&typeof f.toJSON===""function""){f=f.toJSON(a)}if(typeof t===""function""){f=t.call(b,a,f)}switch(typeof f){case""string"":return quote(f);case""number"":return(isFinite(f))?String(f):""null"";case""boolean"":case""null"":return String(f);case""object"":if(!f){return""null""}q+=r;e=[];if(Object.prototype.toString.apply(f)===""[object Array]""){c=f.length;for(i=0;i<c;i+=1){e[i]=str(i,f)||""null""}v=e.length===0?""[]"":q?(""[\n""+q+e.join("",\n""+q)+""\n""+d+""]""):""[""+e.join("","")+""]"";q=d;return v}if(t&&typeof t===""object""){c=t.length;for(i=0;i<c;i+=1){if(typeof t[i]===""string""){k=t[i];v=str(k,f);if(v){e.push(quote(k)+((q)?"": "":"":"")+v)}}}}else{for(k in f){if(Object.prototype.hasOwnProperty.call(f,k)){v=str(k,f);if(v){e.push(quote(k)+((q)?"": "":"":"")+v)}}}}v=e.length===0?""{}"":q?""{\n""+q+e.join("",\n""+q)+""\n""+d+""}"":""{""+e.join("","")+""}"";q=d;return v}}if(typeof JSON.stringify!== ""function""){s={""\b"":""\\b"",""\t"":""\\t"",""\n"":""\\n"",""\f"":""\\f"",""\r"":""\\r"",""\"""":""\\\"""",""\\"":""\\\\""};JSON.stringify=function(a,b,c){var i;q="""";r="""";if(typeof c===""number""){for(i=0;i<c;i+=1){r+="" ""}}else if(typeof c===""string""){r=c}t=b;if(b&&typeof b!== ""function""&&(typeof b!== ""object""||typeof b.length!== ""number"")){throw new Error(""JSON.stringify"");}return str("""",{"""":a})}}if(typeof JSON.parse!== ""function""){JSON.parse=function(d,e){var j;function walk(a,b){var k;var v;var c=a[b];if(c&&typeof c===""object""){for(k in c){if(Object.prototype.hasOwnProperty.call(c,k)){v=walk(c,k);if(v!== undefined){c[k]=v}else{delete c[k]}}}}return e.call(a,b,c)}d=String(d);p.lastIndex=0;if(p.test(d)){d=d.replace(p,function(a){return(""\\u""+(""0000""+a.charCodeAt(0).toString(16)).slice(-4))})}if(g.test(d.replace(h,""@"").replace(l,""]"").replace(m,""""))){j=eval(""(""+d+"")"");return(typeof e===""function"")?walk({"""":j},""""):j}throw new SyntaxError(""JSON.parse"");}}}());"
' 其他 JavaScript 代码
Dim Sz() As String
Sz = Split("function JsonFormat(JsonObj_or_Str) { if (JsonObj_or_Str==="""") { return JSON.stringify(JsonObj, null, 2); } else if(typeof JsonObj_or_Str === 'object'){return JSON.stringify(JsonObj_or_Str, null, 2);} else{return JSON.stringify(eval(""JsonObj.""+JsonObj_or_Str), null, 2);} } if (!Object.keys) { Object.keys = (function() { return function(obj) { var keys = []; for (var key in obj) { if (obj.hasOwnProperty(key)) { keys.push(key); } } return keys; }; })(); }", vbCrLf)
JsCode = JsCode & vbCrLf & Join(Sz, vbCrLf)
HtmlWindowA.execScript JsCode, "JScript"
Set JsonObj = HtmlWindowA.JsonObj
End Sub
' 获取整个 JSON 对象的字符串表示
Public Property Get JsonStr() As String
JsonStr = Doc.Script.JsonFormat("")
End Property
' 设置整个 JSON 对象的字符串表示
Public Property Let JsonStr(ByVal vNewValue As String)
On Error Resume Next
Doc.Script.SetJsonStr vNewValue
Set JsonObj = HtmlWindowA.JsonObj
End Property
' 给 Json 设置一整段字符串
Public Function SetJsonStr(JsonStr As String) As Boolean
On Error Resume Next
Doc.Script.SetJsonStr JsonStr
Set JsonObj = HtmlWindowA.JsonObj
SetJsonStr = Err.Number = 0
End Function
' 返回 JSON 字符串
Public Function GetJsonStr(Optional Key As String) As String
GetJsonStr = Doc.Script.JsonFormat(Key)
End Function
' 返回指定对象的 JSON 字符串表示
Public Function GetJsonStrObject(obj1 As Object) As String
GetJsonStrObject = Doc.Script.JsonFormat(obj1)
End Function
' 执行 JavaScript 代码并返回结果
Function eval(code As String) As String
On Error GoTo ERR
eval = HtmlWindowA.eval(code)
Exit Function
ERR:
eval = "Err:" & Err.Number & ",信息:" & Err.Description
End Function
' 取数组成员数
Public Property Get Arraylength(ItemName As String) As Long
Arraylength = HtmlWindowA.eval("JsonObj." & ItemName & ".length")
End Property
' 删除数组中第 N 个值(INDEX=N-1)
Function RemoveArrayByIndex(ItemName As String, ByVal IndexA As Long) As Boolean
On Error Resume Next
HtmlWindowA.execScript "JsonObj." & ItemName & ".splice(" & IndexA & ", 1)"
RemoveArrayByIndex = Err.Number = 0
End Function
' 数组添加一个值
Public Sub ArrayAdd(ItemName As String, ByVal vNewValue As Variant, Optional AutoSetType As Boolean = True)
If AutoSetType Then
If TypeName(vNewValue) = "String" Then vNewValue = """" & vNewValue & """"
End If
HtmlWindowA.execScript "JsonObj." & ItemName & ".push(" & vNewValue & ")"
End Sub