将 JSON 解析为 scripting.dictionary 并通过 COM 传递给 VBA

Parse JSON to scripting.dictionary and pass via COM to VBA

背景:我目前正在使用 VBA-JSON 将 json 字符串解析为 VBA (Access) 中的字典对象。这很慢,一个示例过程需要 18 秒。

在 VB.NET 中,JavaScriptSerializer Deserialize 方法对相同的数据需要 0.5 秒。

我希望通过 COM Interop 为我的 VBA 代码提供 VB.NET 方法的性能。但是 COM 不能传递通用对象,虽然我读到解决方案涉及封送处理,但我无法理解该选项。

当我手动生成 scripting.dictionary 类型时,我可以从我的 VB.NET COM class 成功传递它,并且可以在 VBA.

中使用它
    Public Function GetData2() As Scripting.Dictionary

        Dim dict As New Scripting.Dictionary

        dict.Add("a", "Athens")
        dict.Add("b", "Belgrade")
        Return dict

    End Function

但是 JavaScriptSerializer Deserialize 方法 returns 是 IDictionary 类型,而不是 scripting.dictionary。

所以我必须找到一种方法将 json 反序列化为 scripting.dictionary 或将 IDictionary 转换为 scripting.dictionary.

我该怎么做?
鉴于我的总体目标,是否有关于替代方法的建议?

编辑。

该项目使用专有的 REST API 作为会计系统。我想创建通用工具来简化和加速各种不同的任务,来自包括 Access、Excel、vbscript 等在内的环境。API 设计师想要的一切,但从通常对 REST 不友好的工具 API编程。

用途包括从系统读取和写入数据,或将数据加载到另一个数据库,在 excel 中创建自定义报告,导入订单等。

这是销售订单的一些 JSON。

{
  "id": 7,
  "orderNo": "0000102692",
  "division": "000",
  "location": "",
  "profitCenter": "",
  "invoiceNo": "",
  "customer": {
    "id": 1996,
    "code": "ER118",
    "customerNo": "ER118",
    "name": "E R Partridge Inc"
  },
  "currency": null,
  "status": "O",
  "type": "O",
  "hold": false,
  "orderDate": "2015-02-13",
  "invoiceDate": null,
  "requiredDate": "2015-02-13",
  "address": {
    "id": 2045,
    "type": "B",
    "linkTable": "SORD",
    "linkNo": "0000102692",
    "shipId": "",
    "name": "E R Partridge Inc",
    "line1": "1531 St Jean Baptiste St",
    "line2": "",
    "line3": "",
    "line4": "",
    "city": "St Ulric",
    "postalCode": "G0J 3H0",
    "provState": "QC",
    "country": "CAN",
    "phone": {
      "number": "4187370284",
      "format": 1
    },
    "fax": {
      "number": "",
      "format": 1
    },
    "email": "van@erpart.com",
    "website": "",
    "shipCode": "",
    "shipDescription": "",
    "salesperson": {
      "code": "",
      "name": ""
    },
    "territory": {
      "code": "",
      "description": ""
    },
    "sellLevel": 1,
    "glAccount": "41100",
    "defaultWarehouse": "VA",
    "created": "2014-08-26T11:44:57.930000",
    "modified": "2015-02-16T09:30:08",
    "contacts": [
      {
        "name": "Van Coon",
        "email": "",
        "phone": {
          "number": "",
          "format": 1
        },
        "fax": {
          "number": "",
          "format": 1
        }
      },
      {
        "name": "",
        "email": "",
        "phone": {
          "number": "",
          "format": 1
        },
        "fax": {
          "number": "",
          "format": 1
        }
      },
      {
        "name": "",
        "email": "",
        "phone": {
          "number": "",
          "format": 1
        },
        "fax": {
          "number": "",
          "format": 1
        }
      }
    ],
    "salesTaxes": [
      {
        "code": 1,
        "exempt": ""
      },
      {
        "code": 2,
        "exempt": ""
      },
      {
        "code": 0,
        "exempt": ""
      },
      {
        "code": 0,
        "exempt": ""
      }
    ]
  },
  "shippingAddress": {
    "id": 2044,
    "type": "S",
    "linkTable": "SORD",
    "linkNo": "SORD0000102692          S",
    "shipId": "",
    "name": "E R Partridge Inc",
    "line1": "1531 St Jean Baptiste St",
    "line2": "",
    "line3": "",
    "line4": "",
    "city": "St Ulric",
    "postalCode": "G0J 3H0",
    "provState": "QC",
    "country": "CAN",
    "phone": {
      "number": "4187370284",
      "format": 1
    },
    "fax": {
      "number": "",
      "format": 1
    },
    "email": "",
    "website": "",
    "shipCode": "",
    "shipDescription": "",
    "salesperson": {
      "code": "",
      "name": ""
    },
    "territory": {
      "code": "",
      "description": ""
    },
    "sellLevel": 1,
    "glAccount": "41100",
    "defaultWarehouse": "VA",
    "created": "2014-08-26T11:44:57.930000",
    "modified": "2014-08-26T11:44:57.930000",
    "contacts": [
      {
        "name": "Van Coon",
        "email": "",
        "phone": {
          "number": "",
          "format": 1
        },
        "fax": {
          "number": "",
          "format": 1
        }
      },
      {
        "name": "",
        "email": "",
        "phone": {
          "number": "",
          "format": 1
        },
        "fax": {
          "number": "",
          "format": 1
        }
      },
      {
        "name": "",
        "email": "",
        "phone": {
          "number": "",
          "format": 1
        },
        "fax": {
          "number": "",
          "format": 1
        }
      }
    ],
    "salesTaxes": [
      {
        "code": 1,
        "exempt": ""
      },
      {
        "code": 2,
        "exempt": ""
      },
      {
        "code": 0,
        "exempt": ""
      },
      {
        "code": 0,
        "exempt": ""
      }
    ]
  },
  "contact": {
    "name": "",
    "email": "",
    "phone": {
      "number": "",
      "format": 0
    },
    "fax": {
      "number": "",
      "format": 0
    }
  },
  "customerPO": "",
  "batchNo": 0,
  "fob": "Your dock",
  "referenceNo": "",
  "shippingCarrier": "",
  "shipDate": null,
  "trackingNo": "",
  "termsCode": "",
  "termsText": "",
  "freight": "41.95",
  "taxes": [
    {
      "code": 1,
      "name": "G.S.T.",
      "shortName": "G.S.T.",
      "rate": "5",
      "exemptNo": "",
      "total": "44.05"
    },
    {
      "code": 2,
      "name": "P.S.T.",
      "shortName": "BC P.S.T.",
      "rate": "7",
      "exemptNo": "",
      "total": "61.67"
    },
    {
      "code": 0,
      "name": "",
      "shortName": "",
      "rate": "0",
      "exemptNo": "",
      "total": 0
    },
    {
      "code": 0,
      "name": "",
      "shortName": "",
      "rate": "0",
      "exemptNo": "",
      "total": 0
    }
  ],
  "subtotal": "839",
  "subtotalOrdered": "839",
  "discount": "0",
  "totalDiscount": "0",
  "total": "986.67",
  "totalOrdered": "986.67",
  "grossProfit": "346.26",
  "items": [
    {
      "id": 8,
      "orderNo": "0000102692",
      "sequence": 1,
      "inventory": {
        "id": 40,
        "whse": "VA",
        "partNo": "INSDB30",
        "description": "InSpire Dumbbell 30"
      },
      "serials": null,
      "whse": "VA",
      "partNo": "INSDB30",
      "description": "InSpire Dumbbell 30",
      "comment": "",
      "orderQty": "4",
      "committedQty": "4",
      "backorderQty": "0",
      "retailPrice": "70",
      "unitPrice": "70",
      "discountable": true,
      "discountPct": "0",
      "discountAmt": "0",
      "taxFlags": [
        true,
        true,
        false,
        false
      ],
      "sellMeasure": "EA",
      "vendor": "INSPIRE",
      "levyCode": "",
      "requiredDate": "2015-08-26",
      "extendedPriceOrdered": "280",
      "extendedPriceCommitted": "280",
      "suppress": false
    },
    {
      "id": 9,
      "orderNo": "0000102692",
      "sequence": 2,
      "inventory": {
        "id": 27,
        "whse": "VA",
        "partNo": "NATACCBAL",
        "description": "National Accupressure Balls"
      },
      "serials": null,
      "whse": "VA",
      "partNo": "NATACCBAL",
      "description": "National Accupressure Balls",
      "comment": "",
      "orderQty": "5",
      "committedQty": "5",
      "backorderQty": "0",
      "retailPrice": "22",
      "unitPrice": "22",
      "discountable": true,
      "discountPct": "0",
      "discountAmt": "0",
      "taxFlags": [
        true,
        true,
        false,
        false
      ],
      "sellMeasure": "EA",
      "vendor": "NATPRO",
      "levyCode": "",
      "requiredDate": "2015-08-26",
      "extendedPriceOrdered": "110",
      "extendedPriceCommitted": "110",
      "suppress": false
    },
    {
      "id": 10,
      "orderNo": "0000102692",
      "sequence": 3,
      "inventory": {
        "id": 33,
        "whse": "VA",
        "partNo": "SPAB",
        "description": "Springfield Ab Toner"
      },
      "serials": null,
      "whse": "VA",
      "partNo": "SPAB",
      "description": "Springfield Ab Toner",
      "comment": "",
      "orderQty": "1",
      "committedQty": "1",
      "backorderQty": "0",
      "retailPrice": "45",
      "unitPrice": "45",
      "discountable": true,
      "discountPct": "0",
      "discountAmt": "0",
      "taxFlags": [
        true,
        true,
        false,
        false
      ],
      "sellMeasure": "EA",
      "vendor": "SPRFIT",
      "levyCode": "",
      "requiredDate": "2015-08-26",
      "extendedPriceOrdered": "45",
      "extendedPriceCommitted": "45",
      "suppress": false
    },
    {
      "id": 11,
      "orderNo": "0000102692",
      "sequence": 4,
      "inventory": {
        "id": 46,
        "whse": "VA",
        "partNo": "INSDB50",
        "description": "InSpire Dumbbell 50"
      },
      "serials": null,
      "whse": "VA",
      "partNo": "INSDB50",
      "description": "InSpire Dumbbell 50",
      "comment": "",
      "orderQty": "2",
      "committedQty": "2",
      "backorderQty": "0",
      "retailPrice": "118",
      "unitPrice": "118",
      "discountable": true,
      "discountPct": "0",
      "discountAmt": "0",
      "taxFlags": [
        true,
        true,
        false,
        false
      ],
      "sellMeasure": "EA",
      "vendor": "INSPIRE",
      "levyCode": "",
      "requiredDate": "2015-08-26",
      "extendedPriceOrdered": "236",
      "extendedPriceCommitted": "236",
      "suppress": false
    },
    {
      "id": 12,
      "orderNo": "0000102692",
      "sequence": 5,
      "inventory": {
        "id": 42,
        "whse": "VA",
        "partNo": "INSDB15",
        "description": "InSpire Dumbbell 15"
      },
      "serials": null,
      "whse": "VA",
      "partNo": "INSDB15",
      "description": "InSpire Dumbbell 15",
      "comment": "",
      "orderQty": "3",
      "committedQty": "3",
      "backorderQty": "0",
      "retailPrice": "34",
      "unitPrice": "34",
      "discountable": true,
      "discountPct": "0",
      "discountAmt": "0",
      "taxFlags": [
        true,
        true,
        false,
        false
      ],
      "sellMeasure": "EA",
      "vendor": "INSPIRE",
      "levyCode": "",
      "requiredDate": "2015-08-26",
      "extendedPriceOrdered": "102",
      "extendedPriceCommitted": "102",
      "suppress": false
    },
    {
      "id": 13,
      "orderNo": "0000102692",
      "sequence": 6,
      "inventory": {
        "id": 9,
        "whse": "VA",
        "partNo": "INSWP50",
        "description": "InSpire Weight Plate 50"
      },
      "serials": null,
      "whse": "VA",
      "partNo": "INSWP50",
      "description": "InSpire Weight Plate 50",
      "comment": "",
      "orderQty": "1",
      "committedQty": "1",
      "backorderQty": "0",
      "retailPrice": "66",
      "unitPrice": "66",
      "discountable": true,
      "discountPct": "0",
      "discountAmt": "0",
      "taxFlags": [
        true,
        true,
        false,
        false
      ],
      "sellMeasure": "EA",
      "vendor": "INSPIRE",
      "levyCode": "",
      "requiredDate": "2015-08-26",
      "extendedPriceOrdered": "66",
      "extendedPriceCommitted": "66",
      "suppress": false
    }
  ],
  "payments": [
    
  ],
  "createdBy": "SS",
  "modifiedBy": "SS",
  "created": "2014-08-26T11:44:57.930000",
  "modified": "2015-02-20T08:09:55",
  "links": {
    "notes": "https://localhost:10880/api/v2/companies/INSPIRE/sales/orders/7/notes/"
  }
}

编辑 2

Erik A 的回答向我展示了如何按需或 'streaming' 完成解析。解析仅在您请求元素时完成。

我想我误解了 VB.NET 如何反序列化 JSON 的本质。它一定在做同样的事情。所以当我看到 500 毫秒而不是 18 秒时,我不知道我在看什么。我怀疑如果我遍历 VB.NET 中的反序列化 json 并检查每个元素,则需要更长的时间。我用于性能测试的样本数据实际上是 JSON 样本中的 124 个样本的集合,其中一些具有更多 ITEMS,因此是 18 秒。 这是正确的吗?

Albert 的回答向我展示了一些我原本想做但无法实现的事情。很好的完整答案,我将在接下来进行深入研究。

为什么不直接使用 .net 字典?您可能只需要计数、设置或按键拉动。

所以,这段代码应该没问题:

Imports System.Runtime.InteropServices
Imports Newtonsoft.Json

<ClassInterface(ClassInterfaceType.AutoDual)>
Public Class MyJSON

   Private m_DICT As New Dictionary(Of String, String)

   Public Function ToJson() As String
       Dim s As String = ""
       s = JsonConvert.SerializeObject(m_DICT)
       Return s
   End Function

   Public Sub JsonToDict(s As String)
       m_DICT = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(s)
   End Sub

   Public Sub Add(sKey As String, sValue As String)
       m_DICT.Add(sKey, sValue)
   End Sub

   Public Function ix(s As String) As String
       Return m_DICT(s)
   End Function

   Public Function Count() As Integer
       Return m_DICT.Count
   End Function

End Class

只需确保将上述项目设置为 x86。选中该框以注册 com interop,然后您就可以参加比赛了。我使用了 NewtonSOFT json,但不清楚您使用的是什么序列化程序库。

所以,现在您的 VBA 代码变为:

Sub TEstMyCom()

  Dim MyJSON  As New TestCom2.MyJSON
   
  MyJSON.Add "a", "aaaaa"
  MyJSON.Add "b", "bbbbb"
  MyJSON.Add "c", "ccccc"
      
  Dim ss As String
  ss = MyJSON.toJSON
  
  Debug.Print MyJSON.toJSON
  
  ' convert the string to array (dict)
  
  Dim MyJSON2 As New TestCom2.MyJSON
 
  MyJSON2.JsonToDict ss
 
 Debug.Print MyJSON2.ix("c")
 
End Sub

输出:

{"a":"aaaaa","b":"bbbbb","c":"ccccc"}
ccccc

请注意,即使 VBA 编辑器中的智能感知也适用于上述内容。因此,只需公开一些额外的方法来使用 .net 字典,您甚至不必为 VBA 中的脚本库操心——无论如何,该库使用起来有点麻烦。因此,您只需像我上面那样使用 .net dict 对象就可以摆脱一个库引用。

编辑

现在用户已经提供了样本 json?

要处理给定的数据,步骤是。

在 .net 中创建一个新空白 class ctrl-a, del 键。有了这个空 class,我们就有了文本文档中的示例 json。 ctrl-a,ctrl-c。现在在 Visual Studio (VS) 中,编辑->选择性粘贴,粘贴为 JSON Class.

此时,您的 classes 已自动为您生成。由于 Neutonsoft 解析器的限制,它不支持 arrays()。 (很伤心,我可能会补充)。

因此,搜索 (),找到数组并替换它们。

所以

Public Property taxes() As Tax

变成

Public Property taxes As IList(Of Tax)

我们使用 iList 代替 List 是因为我们需要 read/write 这里的能力。正如我所说,我们只是在做这项工作,因为 NeutonSoft simple 不喜欢数组。

好的,上面大概有4个。这需要不到 1 分钟的时间来修改。

但是,如果想在 VBA 中获得非常好的智能感知,则需要重新公开上述内容。因此,让我们为这些 ilist 添加一些 classes。

同样,只有大约 4 个。 所以我们有这个:

Public Property salesTaxes As IList(Of Salestax)

Public Property salesTaxesN(ix As Integer) As Salestax
    Get
        Return salesTaxes(ix)
    End Get
    Set(s As Salestax)
        salesTaxes(ix) = s
    End Set

End Property

以上内容会给我们很好的情报。我们不必执行上述操作,但多出来的 2 分钟,我们现在可以在 VBA.

中以 intel-sense 方式遍历数据

而且我们需要一个计数(虽然 iList 看起来 VBA,但出于某种原因它没有公开计数)。我愿意接受其他建议,但让我们在上面添加这个:

Public Function salesTaxesNCout() As Integer

    Return salesTaxes.Count

End Function

是否做了以上修改?我们还不到 5 分钟的时间。如果您在 VS 中使用粘贴为 json 功能,那么执行上述操作的次数越多,您就越能更好地进行这些更改。如前所述,您会发现不到 5 分钟的时间。

同样由于 VBA 中需要智能感知,请在每个 class

之前粘贴
<ClassInterface(ClassInterfaceType.AutoDual)>

现在这是一次又一次的快速粘贴。所以这就是代码的样子(来自我们的 class 的示例片段)。

<ClassInterface(ClassInterfaceType.AutoDual)>
Public Class Salesperson
    Public Property code As String
    Public Property name As String
End Class
<ClassInterface(ClassInterfaceType.AutoDual)>
Public Class Territory
    Public Property code As String
    Public Property description As String
End Class

以上只是一个简短的片段。同样,这真的很快。

好的,我们完成了!

我们的主要 class 现在看起来像这样:

imports System.Runtime.InteropServices
imports Newtonsoft.Json

<ClassInterface(ClassInterfaceType.AutoDual)>
Public Class MyJSON

   Public MyCust As New Jcust

   Public Function ToJson() As String

       Dim s As String = ""
       s = JsonConvert.SerializeObject(MyCust)
       Return s

   End Function

   Public Sub JsonToCust(s As String)

       MyCust = JsonConvert.DeserializeObject(Of Jcust)(s)

   End Sub

End Class

就是这样!你现在有了一个很好的工作设置。

我们上面的 VBA 代码现在是这样的:

Sub custTest()

  Dim strJSON    As String
  Dim intF       As Integer
  Dim strF       As String
  strF = "c:\test2\cust.txt"
  ' read in that file
  intF = FreeFile()
  Open strF For Input As #intF
  strJSON = input(LOF(intF), intF)
  Close intF
  
  Dim cCust As New MyJSON
      
  cCust.JsonToCust strJSON
  
  Debug.Print cCust.MyCust.Address.salesTaxesN(1).Code
  
  Debug.Print cCust.MyCust.Address.City
  
  
End Sub

运行以上? 输出:

 2 
St Ulric

我应该指出,intel-sense 一直有效。

现在我想真正的问题是,如果有人知道如何让 Newtonsoft 使用 Arrays,那么我们将在 2 分钟内完成这件事,而不是这花了我 5 分钟。

事实上,我已经想问这个问题大约 1 年了,今天晚些时候我会的。在上面我确实将 rootobject 重命名为 MyCust。

我个人一直在为 VBA 开发一种快速、灵活的 JSON 解释器,它可能会满足您的需求。

它由两个不同的对象组成:clsStringBuilder(一个非常基本的字符串生成器)和 JSONInterpreter(允许您使用 JSON 的主要对象)。

您可以找到项目here。请注意,我已经为这个问题快速上传了它,目前它缺少很多东西(文档、测试等)。

示例代码:

Dim jsi As New JSONInterpreter
jsi.JSON = SomeString
Debug.Print jsi.item("company", "invoice", 1, "ShipAddress", "AddressLine", 2).VBAVariant

如果您经常使用较大对象中的特定数组或对象,我建议检索 VBAVariant 属性 并进一步处理它,或者创建一个子对象。

示例:

Dim jsi As New JSONInterpreter
jsi.JSON = SomeString
Dim shipAddressJSI as JSONInterpreter
Set shipAddressJSI = jsi.item("company", "invoice", 1, "ShipAddress")
Debug.Print shipAddressJSI.item("AddressLine", 1).VBAVariant
Debug.Print shipAddressJSI.item("AddressLine", 2).VBAVariant

请注意,如果您考虑使用它,我强烈建议您先进行一些测试。示例实现中的 WalkJSON sub 可以帮助解决这个问题。

对您共享的 JSON 文档进行的快速性能测试表明,将整个文档(当存储在 Excel 中的工作表单元格中时)移动到字典需要 0.08 到 0.11 秒在我的系统上(充其量是低端的)。仅将文档的一部分移动到字典时肯定会提高性能,特别是如果您可以使用位置(而不是键名)来指定您想要的部分。