Excel VBAでServiceNowテーブルを一括取得・差分更新してみた
お客様とServiceNowで要件定義していると、よく「Excelから一括でデータを登録したい」と言われますよね。また、運用保守の現場でも「大量の情報を一括更新したい」という要望が出ることが多いと思います。
そんな 「Excelの内容をそのままServiceNowに反映したい!」 という声に応えるため、Excel VBAだけで実現できるちょっと夢のあるツールを作ってみました。
当然、ServiceNowにはExcelからのデータ取り込み機能はありますが、Importのロールや場合によってはAdminロールが必要なのですが、このツールはREST APIのロールがあればできるのでちょっとハードルが下がります。
あと、ServiceNowからポチポチインポート操作をしなくていい。というのもユーザーフレンドリーでいい点です。
全体のイメージ
-
Configシート
- ServiceNowのURL、ユーザーID、パスワード、対象テーブル名を設定
-
Masterシート
- APIで取得したレコードを整形して表示(最新状態)
-
Modifyシート
- Masterシートをコピーし、更新 or 追加入力したい項目を編集するシート
-
diffシート
- MasterとModifyを比較して、差分や新規追加分を一覧化するシート
(*新規追加は今ちょっとうまく動かないです。ごめんなさい💦)
- MasterとModifyを比較して、差分や新規追加分を一覧化するシート
最終的には、diffシートの内容をServiceNowにPOSTすることで、Excel上で編集した変更がそのまま本番データとして反映されます。
事前準備
-
VBA参照設定
- VBAエディタ(Alt+F11)を開く
- 「ツール」→「参照設定」で
- Microsoft XML, v6.0
-
Microsoft Scripting Runtime
にチェックを入れる
-
JSON解析ライブラリ (VBA-JSON) の導入
-
VBA-JSON (GitHub) から
JsonConverter.bas
をダウンロード - VBAエディタで「ファイル」→「ファイルのインポート」から
JsonConverter.bas
を取り込む
-
VBA-JSON (GitHub) から
-
Configシートの設定
- セルB1: ServiceNowのインスタンスURL (例:
https://xxxx.service-now.com/
) - セルB2: ユーザーID (例:
taro.yamada@example.com
) - セルB3: パスワード (例:
Passw0rd!
) - セルB4: テーブル名 (例:
incident
)
- セルB1: ServiceNowのインスタンスURL (例:
コードの概要
-
Module1:
最新データの取り込み()
- ServiceNowのテーブルデータをAPIで取得
- Masterシートに整形して配置
- MasterシートをコピーしてModifyシートを作成
-
Module2:
更新データの抽出()
- MasterシートとModifyシートを比較し、差分がある行や新規行だけをdiffシートにまとめる
-
Module3:
差分をServiceNowに反映する()
- diffシートのType列が「Add」ならPOST、「Modify」ならPATCHを送信
- Excel側の更新がそのままServiceNowへ反映される
コード例(ごく一部のみ)
' Module1
Option Explicit
'==== 設定パラメータ ====
Public Const API_LIMIT As Integer = 10000 ' APIで取得するデータ数の制限
Public Sub 最新データの取り込み()
Dim configSheet As Worksheet
Dim diffSheet As Worksheet
Dim masterSheet As Worksheet
Dim modifySheet As Worksheet
Dim http As Object
Dim url As String
Dim username As String
Dim password As String
Dim response As String
Dim json As Object
Dim records As Object
Dim record As Object
Dim i As Long
Dim j As Long
Dim field As Variant
Dim fieldNames() As String
Dim fieldCount As Long
Dim tableName As String
' シートの参照
Set configSheet = ThisWorkbook.Sheets("Config")
Set diffSheet = ThisWorkbook.Sheets("diff")
Set masterSheet = ThisWorkbook.Sheets("Master")
Set modifySheet = ThisWorkbook.Sheets("Modify")
' Configシートからテーブル名を取得
With configSheet
url = .Range("B1").Value & "api/now/table/" & .Range("B4").Value & "?sysparm_limit=" & API_LIMIT & "&sysparm_display_value=true"
username = .Range("B2").Value
password = .Range("B3").Value
tableName = .Range("B4").Value ' ConfigシートのB4セルからテーブル名を取得
End With
' HTTPオブジェクトの作成
Set http = CreateObject("MSXML2.XMLHTTP.6.0")
' HTTPリクエストの設定
http.Open "GET", url, False, username, password
http.setRequestHeader "Accept", "application/json"
' リクエストの送信
http.send
' レスポンスの取得
response = http.responseText
' JSONの解析 (JsonConverter が参照設定orVBAモジュールにある前提)
Set json = JsonConverter.ParseJson(response)
Set records = json("result")
' フィールド名の取得
fieldCount = 0
For Each field In records(1).Keys
fieldCount = fieldCount + 1
ReDim Preserve fieldNames(1 To fieldCount)
fieldNames(fieldCount) = field
Next field
' データを取り込む前に「Master」「Modify」「Diff」のシートをクリア
masterSheet.Cells.Clear
modifySheet.Cells.Clear
diffSheet.Cells.Clear
' シート「Master」にデータを出力
With masterSheet
' ヘッダーの設定
For j = 1 To fieldCount
.Cells(1, j).Value = fieldNames(j)
Next j
' データの書き込み
i = 2
For Each record In records
For j = 1 To fieldCount
If Not IsNull(record(fieldNames(j))) Then
On Error Resume Next
.Cells(i, j).Value = record(fieldNames(j))
On Error GoTo 0
Else
.Cells(i, j).Value = ""
End If
Next j
i = i + 1
Next record
' 書式設定 (Master)
' 背景色を白に変更
.Range("A2").Resize(i - 2, fieldCount).Interior.Color = RGB(255, 255, 255) ' 白背景
' タイトル行設定 (中央揃え、太字、白い罫線)
With .Range("A1").Resize(1, fieldCount)
.HorizontalAlignment = xlCenter
.VerticalAlignment = xlCenter
.Font.Bold = True
.Interior.Color = RGB(0, 51, 102) ' 濃い青
.Font.Color = RGB(255, 255, 255) ' 白文字
.Borders(xlEdgeBottom).Color = RGB(255, 255, 255) ' 白い罫線
End With
' データ行に青い罫線
With .Range("A2").Resize(i - 2, fieldCount).Borders
.LineStyle = xlContinuous
.Color = RGB(0, 51, 102) ' 青
End With
' データ行を左寄せ
.Range("A2").Resize(i - 2, fieldCount).HorizontalAlignment = xlLeft
' 列幅を自動調整
.Cells.EntireColumn.AutoFit
End With
' シート「Modify」に書式ごとコピー
With ThisWorkbook
masterSheet.Cells.Copy
modifySheet.Range("A1").PasteSpecial Paste:=xlPasteAll
Application.CutCopyMode = False
' 書式設定 (Modify)
' 背景色を白に変更
modifySheet.Range("A2").Resize(i - 2, fieldCount).Interior.Color = RGB(255, 255, 255) ' 白背景
' タイトル行設定 (中央揃え、太字、白い罫線)
With modifySheet.Range("A1").Resize(1, fieldCount)
.HorizontalAlignment = xlCenter
.VerticalAlignment = xlCenter
.Font.Bold = True
.Interior.Color = RGB(0, 102, 0) ' 濃い緑
.Font.Color = RGB(255, 255, 255) ' 白文字
.Borders(xlEdgeBottom).Color = RGB(255, 255, 255) ' 白い罫線
End With
' データ行に緑の罫線
With modifySheet.Range("A2").Resize(i - 2, fieldCount).Borders
.LineStyle = xlContinuous
.Color = RGB(0, 102, 0) ' 緑
End With
' データ行を左寄せ
modifySheet.Range("A2").Resize(i - 2, fieldCount).HorizontalAlignment = xlLeft
' 列幅を自動調整
modifySheet.Cells.EntireColumn.AutoFit
End With
' データがない列を非表示にする (MasterとModify)
Dim ws As Worksheet
Dim col As Long
For Each ws In ThisWorkbook.Sheets(Array("Master", "Modify"))
For col = 1 To fieldCount
If Application.WorksheetFunction.CountA(ws.Range(ws.Cells(2, col), ws.Cells(i - 1, col))) = 0 Then
ws.Columns(col).Hidden = True
Else
ws.Columns(col).Hidden = False
End If
Next col
Next ws
End Sub
' Module2
Option Explicit
'==== 設定パラメータ ====
Public Const API_LIMIT As Integer = 10000 ' APIで取得するデータ数の制限
Public Const DATE_TOLERANCE As Double = 1 / 86400 ' 日付比較の誤差(1秒)
'==== 最終行・最終列を取得するユーティリティ ====
Private Function GetLastUsedRow(ws As Worksheet) As Long
If Application.WorksheetFunction.CountA(ws.Cells) = 0 Then
GetLastUsedRow = 1
Else
GetLastUsedRow = ws.Cells.Find(What:="*", _
After:=ws.Range("A1"), _
LookAt:=xlPart, _
LookIn:=xlValues, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious).Row
End If
End Function
Private Function GetLastUsedCol(ws As Worksheet) As Long
If Application.WorksheetFunction.CountA(ws.Cells) = 0 Then
GetLastUsedCol = 1
Else
GetLastUsedCol = ws.Cells.Find(What:="*", _
After:=ws.Range("A1"), _
LookAt:=xlPart, _
LookIn:=xlValues, _
SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious).Column
End If
End Function
'==== メイン処理 ====
Public Sub 更新データの抽出()
' シートの参照
Dim wsMaster As Worksheet, wsModify As Worksheet, wsDiff As Worksheet
Set wsMaster = ThisWorkbook.Sheets("Master")
Set wsModify = ThisWorkbook.Sheets("Modify")
Set wsDiff = ThisWorkbook.Sheets("diff")
' 必要な変数の宣言
Dim lastRowMaster As Long, lastRowModify As Long
Dim lastColMaster As Long, lastColModify As Long
Dim sysIdColMaster As Long, sysIdColModify As Long
Dim dictMaster As Object ' sys_id -> Master行番号
Dim rowM As Long, i As Long, j As Long
Dim sysIdKey As String, currentSysId As String
Dim masterRowIndex As Long
Dim foundDifference As Boolean
Dim diffRow As Long
'=== 最終行・最終列を取得 ===
lastRowMaster = GetLastUsedRow(wsMaster)
lastColMaster = GetLastUsedCol(wsMaster)
lastRowModify = GetLastUsedRow(wsModify)
lastColModify = GetLastUsedCol(wsModify)
' データがない場合のエラーチェック
If lastRowMaster <= 1 Then
MsgBox "Master シートにデータがありません", vbExclamation
Exit Sub
End If
' diffシート初期化
wsDiff.Cells.Clear
' diffシートのヘッダー設定
wsDiff.Range("A1").Value = "Type"
wsMaster.Range(wsMaster.Cells(1, 1), wsMaster.Cells(1, lastColMaster)).Copy _
wsDiff.Range(wsDiff.Cells(1, 2), wsDiff.Cells(1, lastColMaster + 1))
' sys_id 列を検索 (Master)
sysIdColMaster = 0
For j = 1 To lastColMaster
If Trim(LCase(CStr(wsMaster.Cells(1, j).Value))) = "sys_id" Then
sysIdColMaster = j
Exit For
End If
Next j
If sysIdColMaster = 0 Then
MsgBox "Masterの1行目に 'sys_id' がありません", vbExclamation
Exit Sub
End If
' sys_id 列を検索 (Modify)
sysIdColModify = 0
For j = 1 To lastColModify
If Trim(LCase(CStr(wsModify.Cells(1, j).Value))) = "sys_id" Then
sysIdColModify = j
Exit For
End If
Next j
If sysIdColModify = 0 Then
MsgBox "Modifyの1行目に 'sys_id' がありません", vbExclamation
Exit Sub
End If
' Masterの sys_id -> 行番号 を Dictionary に登録
Set dictMaster = CreateObject("Scripting.Dictionary")
dictMaster.CompareMode = vbTextCompare
For rowM = 2 To lastRowMaster
sysIdKey = Trim(CStr(wsMaster.Cells(rowM, sysIdColMaster).Value))
If Len(sysIdKey) > 0 Then
If Not dictMaster.Exists(sysIdKey) Then
dictMaster(sysIdKey) = rowM
End If
End If
Next rowM
' diffRowの初期化
diffRow = 2
' Modifyシートのデータを比較
For i = 2 To lastRowModify
currentSysId = Trim(CStr(wsModify.Cells(i, sysIdColModify).Value))
If Len(currentSysId) = 0 Then
GoTo SkipThisRow
End If
If dictMaster.Exists(currentSysId) Then
' 既存レコード => 差分チェック
masterRowIndex = dictMaster(currentSysId)
foundDifference = False
For j = 1 To lastColMaster
Dim valMaster As Variant, valModify As Variant
valMaster = wsMaster.Cells(masterRowIndex, j).Value
valModify = wsModify.Cells(i, j).Value
' 日付チェック
If IsDate(valMaster) And IsDate(valModify) Then
If Abs(CDbl(valMaster) - CDbl(valModify)) > DATE_TOLERANCE Then
foundDifference = True
Exit For
End If
' 通常文字列比較
ElseIf CStr(valMaster) <> CStr(valModify) Then
foundDifference = True
Exit For
End If
Next j
If foundDifference Then
wsDiff.Cells(diffRow, 1).Value = "Modify"
wsModify.Rows(i).Columns("A:" & GetColumnLetter(lastColMaster)).Copy wsDiff.Range("B" & diffRow)
' Diffシート書式設定
With wsDiff.Rows(diffRow)
.Interior.Color = RGB(255, 255, 255)
.Font.Color = RGB(0, 0, 0)
.HorizontalAlignment = xlLeft
.Borders.LineStyle = xlContinuous
.Borders.Color = RGB(255, 102, 0)
End With
diffRow = diffRow + 1
End If
Else
' 新規レコード => Diffシートに追加
wsDiff.Cells(diffRow, 1).Value = "Add"
wsModify.Rows(i).Columns("A:" & GetColumnLetter(lastColMaster)).Copy wsDiff.Range("B" & diffRow)
' Diffシート書式設定
With wsDiff.Rows(diffRow)
.Interior.Color = RGB(255, 255, 255)
.Font.Color = RGB(0, 0, 0)
.HorizontalAlignment = xlLeft
.Borders.LineStyle = xlContinuous
.Borders.Color = RGB(255, 102, 0)
End With
diffRow = diffRow + 1
End If
SkipThisRow:
Next i
' Diffシートタイトル行書式設定
With wsDiff.Rows(1)
.Interior.Color = RGB(255, 102, 0)
.Font.Color = RGB(255, 255, 255)
.Font.Bold = True
.HorizontalAlignment = xlCenter
.Borders.LineStyle = xlContinuous
.Borders.Color = RGB(255, 255, 255)
End With
' Diffシート列幅調整
wsDiff.Cells.EntireColumn.AutoFit
' データがない列を非表示にする (2行目以降)
Dim col As Long
For col = 1 To GetLastUsedCol(wsDiff)
If Application.WorksheetFunction.CountA(wsDiff.Range(wsDiff.Cells(2, col), wsDiff.Cells(wsDiff.Rows.Count, col))) = 0 Then
wsDiff.Columns(col).Hidden = True
End If
Next col
' 行の高さを変更しないように設定
wsDiff.Rows.RowHeight = wsMaster.Rows.RowHeight
MsgBox "比較処理が完了しました。diffシートに差分/新規行を出力しました。", vbInformation
End Sub
'=== 列番号 → 列文字列 (1->A, 2->B, ...) ===
Private Function GetColumnLetter(colNum As Long) As String
' 簡易版: colNumが26超えるときも対応
Dim temp As Long
Dim letter As String
temp = colNum
Do While temp > 0
Dim remainder As Long
remainder = (temp - 1) Mod 26
letter = Chr(65 + remainder) & letter
temp = (temp - remainder - 1) \ 26
Loop
GetColumnLetter = letter
End Function
' Module3
Option Explicit
'==== 設定パラメータ ====
Public Const API_LIMIT As Integer = 10000 ' APIで取得するデータ数の制限
Public Const DATE_TOLERANCE As Double = 1 / 86400 ' 日付比較の誤差(1秒)
'==== メイン処理: Type別に ServiceNow を更新 ====
Public Sub 差分をServiceNowに反映する()
' シートの参照
Dim configSheet As Worksheet
Dim diffSheet As Worksheet
Dim usedR As Range
Dim firstDataRow As Long, lastDataRow As Long
Dim http As Object
Dim url As String, endpoint As String
Dim username As String, password As String
Dim i As Long, j As Long
Dim colType As Long, colSysId As Long
Dim diffType As String, sysId As String
Dim fieldName As String, fieldValue As Variant
Dim payload As String
Dim recordCount As Long
Dim rowRange As Range
Dim commaNeeded As Boolean
Dim successCount As Long, failCount As Long
Dim tableName As String
' Configシートから認証情報を取得
Set configSheet = ThisWorkbook.Sheets("Config")
Set diffSheet = ThisWorkbook.Sheets("diff")
With configSheet
url = .Range("B1").Value
username = .Range("B2").Value
password = .Range("B3").Value
tableName = .Range("B4").Value ' ConfigシートのB4セルからテーブル名を取得
End With
' APIのエンドポイントを設定
If Right(url, 1) <> "/" Then url = url & "/"
endpoint = url & "api/now/table/" & tableName ' テーブル名を差し替え
' diffシートの UsedRange を取得
Set usedR = diffSheet.UsedRange
firstDataRow = usedR.Row
lastDataRow = usedR.Row + usedR.Rows.Count - 1
' ヘッダー行をチェック: "Type" 列, "sys_id" 列を探す
Dim usedColsCount As Long
usedColsCount = usedR.Columns.Count
Dim headerRow As Long
headerRow = firstDataRow
colType = 0
colSysId = 0
For j = 0 To usedColsCount - 1
fieldName = Trim(CStr(diffSheet.Cells(headerRow, usedR.Column + j).Value))
Select Case LCase(fieldName)
Case "type"
colType = usedR.Column + j
Case "sys_id"
colSysId = usedR.Column + j
End Select
Next j
' HTTP オブジェクト作成
Set http = CreateObject("MSXML2.XMLHTTP.6.0")
recordCount = 0
successCount = 0
failCount = 0
' データ行をループ (ヘッダー以外)
For i = firstDataRow + 1 To lastDataRow
Set rowRange = Intersect(diffSheet.Rows(i), usedR)
If rowRange Is Nothing Then GoTo SkipThisRow
If Application.CountA(rowRange) = 0 Then GoTo SkipThisRow
' Type と sys_id の取得
diffType = Trim(CStr(diffSheet.Cells(i, colType).Value))
sysId = Trim(CStr(diffSheet.Cells(i, colSysId).Value))
' Type別に送信するか判定
Select Case LCase(diffType)
Case "add"
Case "modify"
If sysId = "" Then GoTo SkipThisRow
Case Else
GoTo SkipThisRow
End Select
' JSON ペイロード作成
payload = "{"
commaNeeded = False
' 全列を走査し、"Type" 列は送らない
Dim c As Long
For c = usedR.Column To usedR.Column + usedColsCount - 1
fieldName = Trim(CStr(diffSheet.Cells(headerRow, c).Value))
If LCase(fieldName) = "type" Or fieldName = "" Then
' 何もしない
Else
fieldValue = diffSheet.Cells(i, c).Value
' ダブルクォート等のエスケープ
Dim escVal As String
escVal = Replace(CStr(fieldValue), """", "\""")
escVal = Replace(escVal, vbCrLf, "\n")
escVal = Replace(escVal, vbCr, "\n")
escVal = Replace(escVal, vbLf, "\n")
escVal = Replace(escVal, vbTab, "\t") ' タブ文字もエスケープ
If commaNeeded Then payload = payload & ","
payload = payload & """" & fieldName & """:""" & escVal & """"
commaNeeded = True
End If
Next c
payload = payload & "}"
' ServiceNow に送信 (POST or PATCH)
On Error Resume Next
If LCase(diffType) = "modify" Then
http.Open "PATCH", endpoint & "/" & sysId, False, username, password
Else
http.Open "POST", endpoint, False, username, password
End If
http.setRequestHeader "Content-Type", "application/json"
http.send payload
On Error GoTo 0
If http.Status >= 200 And http.Status < 300 Then
successCount = successCount + 1
Else
failCount = failCount + 1
Debug.Print "[Error] Row=" & i & ", sys_id=" & sysId
Debug.Print "HTTP Status=" & http.Status
Debug.Print "Response=" & http.responseText
End If
SkipThisRow:
Next i
MsgBox "処理が完了しました。 " & successCount & " 件のレコードをServiceNowに反映しました。" & vbCrLf & _
"失敗した件数: " & failCount, vbInformation
End Sub
コード全体は少し長めですが、sys_idでレコードを紐づけ、差分のある行を検出してUpdate/Insertする という大筋はシンプルです。
みなさんの要件に合わせて、列名やシート名などを調整するだけでも利用できると思います。
使い方
-
作業前にServiceNow側のテーブルをXMLExportして必ずバックアップを取りましょう!
-
ConfigシートにURLや認証情報、テーブル名をセット
-
マクロ実行:
最新データの取り込み()
- Masterに全レコードが表示され、Modifyにコピーされる
-
Modifyシートで編集
- short_descriptionや担当者など、変更したいカラムを自由に編集
- short_descriptionや担当者など、変更したいカラムを自由に編集
-
マクロ実行:
更新データの抽出()
- MasterとModifyを比較
- diffシートに「Add」または「Modify」行が出力される
-
マクロ実行:
差分をServiceNowに反映する()
- diffシートをもとに、ServiceNowにPOST/PATCHリクエスト
- Excelの修正内容がServiceNowへ反映
実行画面イメージ
-
Masterシート: 取得したテーブルの一覧
*2行目以降に何もない列は非表示にしたり列幅調整など見やすさにもこだわっています
-
Modifyシート: Masterをコピー後、自由に編集
-
diffシート: 差分だけが表示され、「Add」 or 「Modify」で区別される
最終的に、diffシートに載った行だけが本番ServiceNowに反映されるため、Excelベースで運用する上での安全性も高いです。
列単位で非表示にしたり、セルの書式を整えたりできるので、運用担当者にも渡しやすい形にできます。
まとめ
- 「Excelの内容をそのままServiceNowに反映」 は、意外とニーズがある割に実装例が少ない気がします。
- 本記事のコードは、VBAの参照設定やJSONパーサを用意するだけで動くシンプル構成。
- 大規模運用の一部として取り込むと、ちょっとした「夢のツール」感が出るかもしれません。
以上、Excel VBAを使って 「ServiceNowテーブルの一括取得→編集→差分更新」 を実現する方法のご紹介でした。
ぜひ皆さんも、Excelを開いてマクロを動かし、「ExcelがそのままServiceNowのテーブルになる!」 を体験してみてください。
参考になった方はぜひ「いいね」をください。