Posts Tagged With: json

[VBA] 轉換 Json 字串到 Excel 表格


前篇:[VBA] 抓取 Json 檔案轉入 Excel

前篇的 Json 字串解析,是從 GitHub 上抓的,別人的程式碼用一陣子總覺得全身不舒服,前陣子要處理 OCPP 的 Json 字串時,就自己寫一個。

呼叫範例:

Sub CommandOpenFile(ByVal strCommand)
   Set tSheets = ThisWorkbook.Sheets(strCommand)
   strFileName = ThisWorkbook.Path & "\ocpp.log"
   strBody = myGetStringFile(strFileName)
   arrBody = Split(strBody, vbLf)
   ubl = UBound(arrBody)
   
   iRow = 1
   For ibl = 0 To ubl
      nLoc = InStr(arrBody(ibl), StringFormat(",{0}{1}{0},{", """", strCommand))
      If nLoc > 0 Then
         sLoc = InStr(nLoc + 1, arrBody(ibl), "{")
         eLoc = Len(arrBody(ibl)) - 1
         strValue = Mid(arrBody(ibl), sLoc, eLoc - sLoc + 1)
         Set pJson = ParseJson(strValue)
         
         If iRow = 1 Then
            uCol = pJson.Count
            arrCol = pJson.Keys
            For iCol = 1 To uCol
               tSheets.Cells(iRow, 1 + iCol) = arrCol(iCol - 1)
            Next
            iRow = iRow + 1
         End If
         
         For iCol = 1 To uCol
            tSheets.Cells(iRow, 1 + iCol) = pJson(arrCol(iCol - 1))
         Next
         
         iRow = iRow + 1
         Set pJson = Nothing
      End If
   Next
End Sub

我把我的 Json 解析獨立放在一個模組內,可參考下方網址

modJsonTool.bas: https://1drv.ms/u/s!AqdV_QuSGVQUy2eZgQaLJJb2BNbz?e=7xFSKW

這段的目的是我的 OCPP.log 是一個通訊過程的紀錄檔,裡面有多段的 Json 字串,大部分的指令所形成的 Json 都比較簡單,就用一個通用的副程式解析,把相同指令的 Json 解析都扔到相同的頁籤。

OCPP 內比較複雜的類似 MetaValues 指令,因為回傳的是樹狀結構,不是列狀 (Row) ,所以要挑自己要留的欄位轉到 Excel 內,就單獨另外寫一個 Sub 處理。

Json 解析我參照前篇的架構,屬於樹狀的資料 (以 {…} 包括) 用 Dictionary 去處理,屬於陣列的資料 (以 […] 包括) ,用 Collection 處理,解析的程式碼就用遞迴處理,所以主要的只有一個函數:

Function ParseJson(ByVal strJson As String, Optional ByVal rootDelimiter As Variant)
   JsonDelimiterInit
   ubd = UBound(jsonDelimiter)
   ReDim arrIndex(ubd)
   
   ReDim colTemp(ubd) As New Collection
   fmtKeepName = "(($VbaJson${0}${1}${2}$))"
   fmtKeepLeft = 11
   
   rtnValue = Empty
   strJson = TrimEx(strJson)
   lenJson = Len(strJson)
   pbd = -1
   startLoc = 0
   
   Do
      nowIndex = lenJson + 1
      nbd = -1
      For ibd = 0 To ubd
         arrIndex(ibd) = InStr(startLoc + 1, strJson, jsonDelimiter(ibd)(0))
         If arrIndex(ibd) > 0 And nowIndex > arrIndex(ibd) Then
            nowIndex = arrIndex(ibd)
            nbd = ibd
         End If
      Next
      
      If nbd = -1 Then
         ' 沒有括號裡面開始解析內容
         Exit Do
      Else
         ' 字串的引號裡面會不會有括號?
         startLoc = arrIndex(nbd)
         endIndex = FoundBlockEndIndex(strJson, jsonDelimiter(nbd), startLoc)
         
         If endIndex > 0 Then
            pbd = nbd
            colIndex = colTemp(nbd).Count + 1
            Select Case jsonDelimiter(nbd)(0)
            Case "{"
               strObjectName = StringFormat(fmtKeepName, "Object", nbd, colIndex)
            Case "["
               strObjectName = StringFormat(fmtKeepName, "Array", nbd, colIndex)
            Case Else
               Err.Raise &H80000001, , "Json 字串解析格式錯誤"
            End Select
            
            subJson = Mid(strJson, startLoc + 1, endIndex - startLoc - 1)
            strJson = Replace(strJson, StringFormat("{1}{0}{2}", subJson, jsonDelimiter(nbd)(0), jsonDelimiter(nbd)(1)), strObjectName)
            
            colTemp(nbd).Add ParseJson(subJson, jsonDelimiter(nbd))
         Else
            Err.Raise &H80000002, , "Json 字串缺括號 " & jsonDelimiter(nbd)(1) & ",字串內容為 " & strJson
            Exit Do
         End If
      End If
   Loop
   
   If lenJson > 0 Then
      ' 回傳物件
      If IsMissing(rootDelimiter) Then
         Set rtnValue = colTemp(pbd)(colTemp(pbd).Count)
      Else
         nowDelimiter = rootDelimiter(0)
         
         Select Case nowDelimiter
         Case "{"
            ' Set rtnValue = New Dictionary ' 需在 選單 工具 設定引用項目 引用 Microsoft Scripting Runtime ,使用其中 Dictionary 類別
            Set rtnValue = CreateScriptingDictionary()
         Case "["
            Set rtnValue = New Collection
         End Select
         
         ' 解析內容
         arrItems = Split(strJson, ",")
         ubi = UBound(arrItems)
         For ibi = 0 To ubi
            arrItems(ibi) = TrimEx(arrItems(ibi))
            
            Select Case nowDelimiter
            Case "{"
               itemRow = Split(arrItems(ibi), ":")
               ubr = UBound(itemRow)
               If ubr > 1 Then
                  For ibr = 2 To ubr
                     itemRow(1) = itemRow(1) & ":" & itemRow(ibr)
                  Next
               End If
               
               For ibr = 0 To 1
                  itemRow(ibr) = TrimEx(itemRow(ibr))
               Next
               itemKey = TrimEx(itemRow(0), TES_SepcialAdd, , """'")
               strItem = itemRow(1)
            Case "["
               itemKey = CStr(ibi + 2)
               strItem = arrItems(ibi)
            End Select
         
            If InStr(strItem, Left(fmtKeepName, fmtKeepLeft)) >= 1 Then
               arrRow = Split(strItem, "$")
               Set itemValue = colTemp(CLng(arrRow(3)))(CLng(arrRow(4)))
            Else
               strValue = TrimEx(strItem, TES_SepcialAdd, , """'")
               If Len(strValue) < Len(strItem) Then
                  itemValue = strValue
               Else
                  itemValue = CVariant(strValue)
               End If
            End If
            
            Select Case nowDelimiter
            Case "{"
               rtnValue.Add itemKey, itemValue
            Case "["
               rtnValue.Add itemValue, itemKey
            End Select
         Next
      End If
   End If
   
   Set ParseJson = rtnValue
   
   Set rtnValue = Nothing
   For ibd = 0 To ubd
      Set colTemp(ibd) = Nothing
   Next
End Function

每一層物件對應到 Json 每一層。

由於用遞迴函數,如果碰上我還沒遭遇過的特殊字串,可能會卡在裡面,建議在這個函數裡面加 DoEvents ,若遭遇到無窮迴圈時,可以透過除錯模式跳出,加入特殊字串處理。

GitHub 範例要求要在 VBA 專案參照物件 (提早連結),好處是執行效能比較快,缺點是不熟的人不會處理,特別是 VBA 的安全信任一直調整,所以我改成延後連結的方式處理:

Function CreateScriptingDictionary() As Object
   Set CreateScriptingDictionary = CreateObject("Scripting.Dictionary")
End Function

目前遭遇過特殊字元已經納入處理的有這個陣列:

If Not IsArray(jsonEscape) Then jsonEscape = Array(Split("\""|\\|\/|\b|\f|\n|\r|\t", "|"), Array("""", "\", "/", Chr(8), Chr(12), Chr(13), Chr(10), Chr(9)))

我自己之後會持續使用自己程式碼,擴大支援範圍。

Categories: 工作點滴, 技術分享 | 標籤: | 發表留言

[VBA] 抓取 Json 檔案轉入 Excel


周一同事說,有個政府網頁的數據因為直接 Show 圖上,當滑鼠停在上面才會跳出數據,所以必須一筆一筆透過移動滑鼠來讀取並記錄。我回覆說,可以把網頁提供給資訊部,看看能不能協助存取。

昨天我收到網址,並由同事告訴我她要抓甚麼數據,確認後,開啟 Edge 的開發者模式,馬上就確認是透過 AJAX 讀取資料更新畫面。

用 Edge 測試了幾個網址,資料內容是 Json 格式,都可以直接撈出來,大概沒啥阻擋,打算用過去寫好 .Net framework 的 WebClient 去抓,一測試,WebClient 連線能建立成功,但是無法取得資料流,檔案大小都是 0 到逾時。


註:寫部落格時才想到,可能是 https 加密問題… 參考先前紀錄:

[CLR] .Net framework 2.0 WebClient 連接 https


剛好我以前是用 VBScript 寫過 AJAX ,有現成程式碼拷貝到 Excel 去測試,在 Excel 測 xmlhttp 抓資料就正常,所以可以推論這個網站有針對 http protocol 的 Header 做偵測與阻擋。

透過 xmlhttp 抓 AJAX 取得 Json 資料後,問題在 Json 解碼,這個在 GitHub 有現成程式碼可以引用:

GitHub – VBA-tools/VBA-JSON: JSON conversion and parsing for VBA

我直接引用他寫好的 JsonConverter ,見下圖的專案視窗。

上面網址的最下方說明,要同時引用他另一個專案:

GitHub – VBA-tools/VBA-Dictionary: Drop-in replacement for Scripting.Dictionary on Mac

我在 Excel 365 跑這段程式碼的時候,會因為他自建類別 Dictionary ,與 Scripting.Dictionary 不相容,所以我去修改他的類別,把原始 Scripting.Dictionary 透過 .This 丟出來,就可以順利的完成執行。

主要程式碼如下圖:

VBA 透過 XmlHttp 抓取 AJAX 資料並分析匯入

順利把 Json 共 24 欄 12,313 列資料匯入 Excel 。

已匯入的原始資料

Categories: 工作點滴, 技術分享 | 標籤: | 1 則迴響

[VBScript] 單緒有 Timeout 的 AJAX


前篇有提到:[VBScript] 傳遞陣列到 VB6 的物件

我工作上有讓一隻 VB6 的程式支援 VBScript ,藉此來呼叫外部巨集的方式來處理非標準的功能。

這次碰上一家德國自訂通訊協定的設備,雖然提供 RS-485 通訊,但他的自訂通訊範例很怪,剪貼後跑起來不對勁,而且裡面的二進位變量沒解釋。設備商有提供 WebBox 接 RS-485 ,透過 WebBox 通訊,就可以使用 Modbus TCP 或 RFC over http ,由於使用的設備為最新型,目前在韌體內還不支援 WebBox ,所以只能先用 RFC 來通訊,整合還算單純,使用 JSON 格式來發命令,設備回應。

嵌入外掛的 VBScript 前,開發測試就用 DHTML 來做,同時測發送的訊號,接收的內容,RegExp 正規運算式解析字串的結果。

所以當然就是用 AJAX ,xmlHttpRequest 這個物件。

一開始寫,沒啥太大的問題,IE 中配合 VBScript ,自動嘗試建立物件:

  1. MSXML2.XMLHTTP.3.0
  2. MSXML2.XMLHTTP
  3. Microsoft.XMLHTTP

遷移到 VBScript 則可直接使用。

由於是要給 VB6 呼叫,設計上是單緒,並沒使用非同步呼叫,所以無法處理 xmlHttp 事件。這周要上線了,先把參數設好,才發現,VB6 那邊一直跳 VBScript Timeout ,查了一下,同步呼叫 xmlHttp.send 後,加上 nslookup, Connect, send, receive 林林總總大概要 90 秒,VBScript 當然可以把逾時加大,但是設備數有 17 個,逾時加大的話,整個輪詢時間會很久,此外還同時有 4 個 Modbus RTU, 2 個 Modbus TCP 設備要通訊,顯然是不合適的。

從 MSDN 可以找到 xmlHttpRequest.Timeout 屬性:

http://msdn.microsoft.com/zh-tw/library/cc304105

看起來是 IE 專用的 xmlHttpRequest 才有?(物件名未知),MSXML 3.0 居然沒這屬性,另外測了:MSXML2.XMLHTTP.6.0 也沒這屬性。

查 MSDN 看到可以使用:

ServerXMLHTTP.setTimeouts

http://msdn.microsoft.com/zh-tw/library/ms762278.aspx

在我的 Win7 x64 上可以跑,放在 IE 裡面會有安全對話盒,確認後仍可正常執行,由於我放在 VBScript 內跑,可以不用擔心這個問題。

移到目標電腦後,XPE (Windows XP Embedded) 沒有封裝 IIS ,無法建立 ServerXMLHttp 物件…

回頭在 IE 除錯模式查了 MSXML2.XMLHTTP.6.0 ,有 setTimeouts 方法,但是呼叫會發生錯誤。

最後查得使用 WinHttp.WinHttpRequest.5.1 的 setTimeouts 在 XPE 測沒啥問題,在 IE 內一樣有安全性警告,功能跟 ServerXMLHTTP 差不多,在 Win2k SP3/WinXP SP1 以後就有了~

http://msdn.microsoft.com/zh-tw/library/windows/desktop/aa384086.aspx

所以 VBScript 這部分就這樣用:

   Set htmHttpRequest = CreateXmlHttpRequestObject(False)
   With htmHttpRequest
      .setTimeouts 2000, 2000, 2000, 2000
      .open "POST", strDataUrl, False
      .setRequestHeader "Content-type", "application/x-www-form-urlencoded"
      .send strSend
      strResult = .responseText
   End With

我是設定 nslookup, Connect, send, receive 各 2000 豪秒,實際逾時大概在 2.5 ~ 3.2 秒間,再小的話可能正常的通訊都會失敗,就不再改小了。

Categories: 工作點滴, 技術分享 | 標籤: | 發表留言

在 WordPress.com 建立免費網站或網誌.