5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ServiceNowにかかわった人ならだれもが一度は思う「ExcelをそのままServiceNowに反映したい」を実現する方法

Last updated at Posted at 2025-01-18

Excel VBAでServiceNowテーブルを一括取得・差分更新してみた

お客様とServiceNowで要件定義していると、よく「Excelから一括でデータを登録したい」と言われますよね。また、運用保守の現場でも「大量の情報を一括更新したい」という要望が出ることが多いと思います。

そんな 「Excelの内容をそのままServiceNowに反映したい!」 という声に応えるため、Excel VBAだけで実現できるちょっと夢のあるツールを作ってみました。

当然、ServiceNowにはExcelからのデータ取り込み機能はありますが、Importのロールや場合によってはAdminロールが必要なのですが、このツールはREST APIのロールがあればできるのでちょっとハードルが下がります。

あと、ServiceNowからポチポチインポート操作をしなくていい。というのもユーザーフレンドリーでいい点です。


全体のイメージ

image.png

  1. Configシート
    • ServiceNowのURL、ユーザーID、パスワード、対象テーブル名を設定
  2. Masterシート
    • APIで取得したレコードを整形して表示(最新状態)
  3. Modifyシート
    • Masterシートをコピーし、更新 or 追加入力したい項目を編集するシート
  4. diffシート
    • MasterとModifyを比較して、差分や新規追加分を一覧化するシート
      (*新規追加は今ちょっとうまく動かないです。ごめんなさい💦)

最終的には、diffシートの内容をServiceNowにPOSTすることで、Excel上で編集した変更がそのまま本番データとして反映されます。

事前準備

  1. VBA参照設定
    • VBAエディタ(Alt+F11)を開く
    • 「ツール」→「参照設定」で
      • Microsoft XML, v6.0
      • Microsoft Scripting Runtime
        にチェックを入れる

image.png

  1. JSON解析ライブラリ (VBA-JSON) の導入
    • VBA-JSON (GitHub) から JsonConverter.bas をダウンロード
    • VBAエディタで「ファイル」→「ファイルのインポート」から JsonConverter.bas を取り込む

image.png

  1. Configシートの設定
    • セルB1: ServiceNowのインスタンスURL (例: https://xxxx.service-now.com/)
    • セルB2: ユーザーID (例: taro.yamada@example.com)
    • セルB3: パスワード (例: Passw0rd!)
    • セルB4: テーブル名 (例: incident)

image.png

コードの概要

  • 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する という大筋はシンプルです。
みなさんの要件に合わせて、列名やシート名などを調整するだけでも利用できると思います。

使い方

  1. 作業前にServiceNow側のテーブルをXMLExportして必ずバックアップを取りましょう!
     

  2. ConfigシートにURLや認証情報、テーブル名をセット
     

  3. マクロ実行: 最新データの取り込み()

    • Masterに全レコードが表示され、Modifyにコピーされる
  4. Modifyシートで編集

    • short_descriptionや担当者など、変更したいカラムを自由に編集
       
  5. マクロ実行: 更新データの抽出()

    • MasterとModifyを比較
    • diffシートに「Add」または「Modify」行が出力される
  6. マクロ実行: 差分をServiceNowに反映する()

    • diffシートをもとに、ServiceNowにPOST/PATCHリクエスト
    • Excelの修正内容がServiceNowへ反映

実行画面イメージ

  • Masterシート: 取得したテーブルの一覧
    *2行目以降に何もない列は非表示にしたり列幅調整など見やすさにもこだわっています
    image.png
     
  • Modifyシート: Masterをコピー後、自由に編集
    image.png
     
  • diffシート: 差分だけが表示され、「Add」 or 「Modify」で区別される
    image.png

 

  • ServiceNowに反映される
    image.png

最終的に、diffシートに載った行だけが本番ServiceNowに反映されるため、Excelベースで運用する上での安全性も高いです。
列単位で非表示にしたり、セルの書式を整えたりできるので、運用担当者にも渡しやすい形にできます。

まとめ

  • 「Excelの内容をそのままServiceNowに反映」 は、意外とニーズがある割に実装例が少ない気がします。
  • 本記事のコードは、VBAの参照設定やJSONパーサを用意するだけで動くシンプル構成。
  • 大規模運用の一部として取り込むと、ちょっとした「夢のツール」感が出るかもしれません。

以上、Excel VBAを使って 「ServiceNowテーブルの一括取得→編集→差分更新」 を実現する方法のご紹介でした。
ぜひ皆さんも、Excelを開いてマクロを動かし、「ExcelがそのままServiceNowのテーブルになる!」 を体験してみてください。

参考になった方はぜひ「いいね」をください。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?