ChatGPTでVBA(プログラム)作成


ChatGPTの使いこなし方を模索したいと、仕事の合間に仕事の手伝いをしてもらっています。
ピタッと嵌まる使い方が出来た事もあれば、時間だけ掛かって満足いかないこともあり。プロンプトエンジニアとしての知見の蓄積が必要かなと思う毎日です。
さて、今回はエクセルのVBAをChatGPT君に書いて貰った話。
「たったの1行も自分ではVBAを書かず、VBAを完成させる」という事に挑戦しました。

まずは出来上がったコードをご覧頂きましょう。

Sub ImportXMLData()
    Dim xmlFolder As String
    Dim xmlFile As String
    Dim ws As Worksheet
    Dim fd As Office.FileDialog

    Set ws = ThisWorkbook.Sheets("データシート")
    Set fd = Application.FileDialog(msoFileDialogFolderPicker)

    With fd
        .Title = "XMLファイルが保存されているフォルダを選択してください"
        .AllowMultiSelect = False
        If .Show = -1 Then
            xmlFolder = .SelectedItems(1)
        Else
            MsgBox "フォルダが選択されませんでした。"
            Exit Sub
        End If
    End With

    xmlFile = Dir(xmlFolder & "\*.xml")

    Do While xmlFile <> ""
        ' XMLファイルを処理
        Call ProcessXMLFile(xmlFolder & "\" & xmlFile, ws)
        
        xmlFile = Dir ' 次のファイルを取得
    Loop
End Sub


Sub ProcessXMLFile(ByVal xmlFilePath As String, ByRef ws As Worksheet)
    Dim xmlDoc As Object, recordNode As Object, recordNodes As IXMLDOMNodeList
    Dim lastRow As Long, i As Long
    Set xmlDoc = CreateObject("MSXML2.DOMDocument.6.0")
    xmlDoc.Async = False
    xmlDoc.Load (xmlFilePath)
    
    If xmlDoc.parseError.ErrorCode <> 0 Then
        MsgBox "XML解析エラー: " & xmlDoc.parseError.reason
        Exit Sub
    End If
    
    ' recordノードのリストを取得
    Set recordNodes = xmlDoc.SelectNodes("//record")
    For i = 0 To recordNodes.Length - 1
        Set recordNode = recordNodes(i)
        
        ' 各recordごとに最後の行を更新
        lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row + 1

        ' A列からD列のデータを設定

        ' org_name
        On Error Resume Next ' ノードが存在しない場合のエラーを無視
        ws.Cells(lastRow, 1).Value = xmlDoc.SelectSingleNode("//report_metadata/org_name").Text
        If Err.Number <> 0 Then ws.Cells(lastRow, 1).Value = "-"
        On Error GoTo 0
    
        ' domain
        On Error Resume Next
        ws.Cells(lastRow, 4).Value = xmlDoc.SelectSingleNode("//policy_published/domain").Text
        If Err.Number <> 0 Then ws.Cells(lastRow, 4).Value = "-"
        On Error GoTo 0
    
        ' date_range/beginを日本時間に変換して設定
        Dim beginUnixTime As Long
        beginUnixTime = Val(GetNodeText(xmlDoc.SelectSingleNode("//report_metadata/date_range/begin"), "."))
        If beginUnixTime <> 0 Then ' UNIX時刻が0ではない場合のみ変換
            ws.Cells(lastRow, 2).Value = UnixTimeToJST(beginUnixTime)
        Else
            ws.Cells(lastRow, 2).Value = "-"
        End If

        ' date_range/endを日本時間に変換して設定
        Dim endUnixTime As Long
        endUnixTime = Val(GetNodeText(xmlDoc.SelectSingleNode("//report_metadata/date_range/end"), "."))
        If endUnixTime <> 0 Then ' UNIX時刻が0ではない場合のみ変換
            ws.Cells(lastRow, 3).Value = UnixTimeToJST(endUnixTime)
        Else
            ws.Cells(lastRow, 3).Value = "-"
        End If
        
        
        ' E列: record/row/source_ip
        ws.Cells(lastRow, 5).Value = GetNodeText(recordNode, "row/source_ip")

        ' F列: record/row/count
        ws.Cells(lastRow, 6).Value = GetNodeText(recordNode, "row/count")

        ' G列: record/row/policy_evaluated/disposition
        ws.Cells(lastRow, 7).Value = GetNodeText(recordNode, "row/policy_evaluated/disposition")

        ' H列: record/row/policy_evaluated/dkim
        ws.Cells(lastRow, 8).Value = GetNodeText(recordNode, "row/policy_evaluated/dkim")

        ' I列: record/row/policy_evaluated/spf
        ws.Cells(lastRow, 9).Value = GetNodeText(recordNode, "row/policy_evaluated/spf")

        ' J列: record/identifiers/envelope_to
        ws.Cells(lastRow, 10).Value = GetNodeText(recordNode, "identifiers/envelope_to")

        ' K列: record/identifiers/envelope_from
        ws.Cells(lastRow, 11).Value = GetNodeText(recordNode, "identifiers/envelope_from")

        ' L列: record/identifiers/header_from
        ws.Cells(lastRow, 12).Value = GetNodeText(recordNode, "identifiers/header_from")

        ' M列: record/auth_results/dkim/domain
        ws.Cells(lastRow, 13).Value = GetNodeText(recordNode, "auth_results/dkim/domain")

        ' N列: record/auth_results/dkim/selector
        ws.Cells(lastRow, 14).Value = GetNodeText(recordNode, "auth_results/dkim/selector")

        ' O列: record/auth_results/dkim/result
        ws.Cells(lastRow, 15).Value = GetNodeText(recordNode, "auth_results/dkim/result")

        ' P列: record/auth_results/spf/domain
        ws.Cells(lastRow, 16).Value = GetNodeText(recordNode, "auth_results/spf/domain")

        ' Q列: record/auth_results/spf/scope
        ws.Cells(lastRow, 17).Value = GetNodeText(recordNode, "auth_results/spf/scope")

        ' R列: record/auth_results/spf/result
        ws.Cells(lastRow, 18).Value = GetNodeText(recordNode, "auth_results/spf/result")
    Next i
End Sub

' ヘルパー関数: ノードからテキストを取得し、ノードが存在しない場合は "-" を返す
Function GetNodeText(ByRef node As IXMLDOMNode, ByVal xPath As String) As String
    On Error Resume Next
    GetNodeText = node.SelectSingleNode(xPath).Text
    If Err.Number <> 0 Then GetNodeText = "-"
    On Error GoTo 0
End Function
Function UnixTimeToJST(unixTime As Long) As Date
    Const Epoch As Long = 25569 ' 197011日のExcelの日付値
    Const SecondsPerDay As Long = 86400 ' 1日の秒数
    Const JSTOffset As Integer = 9 ' 日本時間のUTCオフセット(時間)
    
    ' UNIX時刻を日付に変換し、JSTオフセットを加算する
    UnixTimeToJST = Epoch + (unixTime / SecondsPerDay) + (JSTOffset / 24)
End Function

こんな感じです。
これが何をするVBAかというと、以下の通りです。

  • あるフォルダにあるDMARCレポートのXMLファイルを全て読み込む。

  • 内容をレコード毎に対応するエクセル上の列にインポートする。

これだけです。

DMARCレポートってなに?

DMARCレポートって何?と思う方も多いと思いますが、ざっくり説明すると「お前のドメイン(例えばinst-web.com)から送られてきたメール、怪しかったから拒絶したぜ」という報告書です(笑)
SPF、DKIMという迷惑メール対策の認証が導入され、DMARCの認証結果のレポートが送信先のメールサーバーから受け取れるようになったんです。
因みにDMARCレポートはこんな感じ。

<?xml version="1.0"?>
<feedback>
	<version>0.1</version>
	<report_metadata>
		<org_name>通販サイト</org_name>
		<email>postmaster@通販サイト.com</email>
		<report_id>01234567890123456789</report_id>
		<date_range>
			<begin>1709683200</begin>
			<end>1709769600</end>
		</date_range>
	</report_metadata>
	<policy_published>
		<domain>inst-web.com</domain>
		<adkim>r</adkim>
		<aspf>r</aspf>
		<p>none</p>
		<sp>none</sp>
		<pct>100</pct>
		<fo>0</fo>
	</policy_published>
	<record>
		<row>
			<source_ip>xxx.xxx.xxx.xxx</source_ip>
			<count>1</count>
			<policy_evaluated>
				<disposition>none</disposition>
				<dkim>pass</dkim>
				<spf>fail</spf>
			</policy_evaluated>
		</row>
		<identifiers>
			<envelope_from>inst-web.com</envelope_from>
			<header_from>inst-web.com</header_from>
		</identifiers>
		<auth_results>
			<dkim>
				<domain>inst-web.com</domain>
				<result>pass</result>
			</dkim>
			<spf>
				<domain>inst-web.com</domain>
				<result>softfail</result>
			</spf>
		</auth_results>
	</record>
</feedback>

ね、何が書いてあるかさっぱり分からないでしょ?(笑)
これが会社のドメインだと10件とか来るの。結構多い。
「身元の正しいメールを受信しましたよ!」という報告もあれば「怪しいメールも来たよ!」という報告もあります。
怪しいメール、つまり「当社のドメインを偽って送られている迷惑メール」なのかもしれない…
実はまだそこは不勉強です。

ChatGPTへお願いする前に

実際にChatGPTにお願いする前に、色々準備をしました。
何を、どのようにしてほしいかまとめる。要件定義ってやつですかね?

A列	rport_metadata/org_name	
B列	rport_metadata/date_ragne/begin	
C列	rport_metadata/date_ragne/end	
D列	policy_published/domain	
E列	record/row/source_ip	
F列	record/row/count	
G列	record/row/policy_evaluated/disposition	
H列	record/row/policy_evaluated/dkim	
I列	record/row/policy_evaluated/spf	
J列	record/identifiers/envelope_to	
K列	record/identifiers/envelope_from	
L列	record/identifiers/header_from	
M列	record/auth_results/dkim/domain	
N列	record/auth_results/dkim/selector	
O列	record/auth_results/dkim/result	
P列	record/auth_results/spf/domain	
Q列	record/auth_results/spf/scope	
R列	record/auth_results/spf/result	

XMLファイルの構造を見て、エクセルの何列にデータを入れてほしいかを検討しておきました。今回話がスムーズに行ったのは多分ここが大きかった。

プロンプトのやりとり

ではさっそくやってきましょう。

こんな風に入力し、XMLファイルのサンプルを3つ添付しました。

まずはこんな感じ。
「XMLファイルの内容を確認してみましょうか?」と問いかけてくれる繊細さ(笑)

実際にはここで具体的なコードも提示してくれました。それを実行したところ、フォルダ名の設定がないので読み込むことが出来ませんでした。
じゃあ、という事で。

文句も言わずに実装してくれます。
そしてやってみたらこう。

「生成AIなんだからいっぱつで作れよ!」というのは大きな間違いですね。一緒に作業していきましょう(笑)
ちゃんと問題点を認識して改善してくれます。

Sub ProcessXMLFile(ByVal xmlFilePath As String, ByRef ws As Worksheet)
    Dim xmlDoc As Object, recordNode As Object, recordNodes As IXMLDOMNodeList
    Dim lastRow As Long, i As Long
    Set xmlDoc = CreateObject("MSXML2.DOMDocument.6.0")
    xmlDoc.Async = False
    xmlDoc.Load(xmlFilePath)
    
    If xmlDoc.parseError.ErrorCode <> 0 Then
        MsgBox "XML解析エラー: " & xmlDoc.parseError.reason
        Exit Sub
    End If
    
    ' recordノードのリストを取得
    Set recordNodes = xmlDoc.SelectNodes("//record")
    For i = 0 To recordNodes.Length - 1
        Set recordNode = recordNodes(i)
        
        ' 各recordごとに最後の行を更新
        lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row + 1

        ' A列からD列のデータを設定
        ' この部分は以前の説明でカバーされています
        
        ' E列のデータを設定
        ws.Cells(lastRow, 5).Value = GetNodeText(recordNode, "row/source_ip")
        
        ' F列からR列までのデータを設定
        ' この部分は各列に対応するXMLのパスに基づいてデータを設定する必要があります
        ' 例:
        ' ws.Cells(lastRow, 6).Value = GetNodeText(recordNode, "適切なXPath")
        ' ...
    Next i
End Sub

' ヘルパー関数: ノードからテキストを取得し、ノードが存在しない場合は "-" を返す
Function GetNodeText(ByRef node As IXMLDOMNode, ByVal xPath As String) As String
    On Error Resume Next
    GetNodeText = node.SelectSingleNode(xPath).Text
    If Err.Number <> 0 Then GetNodeText = "-"
    On Error GoTo 0
End Function

その後数回のテストとフィードバックを受けて概ね良い感じに。
上のコードの「F列からR列までのデータ」が省略されていますね?
これも考えてもらう事にしました。

' E列: record/row/source_ip
ws.Cells(lastRow, 5).Value = GetNodeText(recordNode, "row/source_ip")

' F列: record/row/count
ws.Cells(lastRow, 6).Value = GetNodeText(recordNode, "row/count")

' G列: record/row/policy_evaluated/disposition
ws.Cells(lastRow, 7).Value = GetNodeText(recordNode, "row/policy_evaluated/disposition")

' H列: record/row/policy_evaluated/dkim
ws.Cells(lastRow, 8).Value = GetNodeText(recordNode, "row/policy_evaluated/dkim")

' I列: record/row/policy_evaluated/spf
ws.Cells(lastRow, 9).Value = GetNodeText(recordNode, "row/policy_evaluated/spf")

' J列: record/identifiers/envelope_to
ws.Cells(lastRow, 10).Value = GetNodeText(recordNode, "identifiers/envelope_to")

' K列: record/identifiers/envelope_from
ws.Cells(lastRow, 11).Value = GetNodeText(recordNode, "identifiers/envelope_from")

' L列: record/identifiers/header_from
ws.Cells(lastRow, 12).Value = GetNodeText(recordNode, "identifiers/header_from")

' M列: record/auth_results/dkim/domain
ws.Cells(lastRow, 13).Value = GetNodeText(recordNode, "auth_results/dkim/domain")

' N列: record/auth_results/dkim/selector
ws.Cells(lastRow, 14).Value = GetNodeText(recordNode, "auth_results/dkim/selector")

' O列: record/auth_results/dkim/result
ws.Cells(lastRow, 15).Value = GetNodeText(recordNode, "auth_results/dkim/result")

' P列: record/auth_results/spf/domain
ws.Cells(lastRow, 16).Value = GetNodeText(recordNode, "auth_results/spf/domain")

' Q列: record/auth_results/spf/scope
ws.Cells(lastRow, 17).Value = GetNodeText(recordNode, "auth_results/spf/scope")

' R列: record/auth_results/spf/result
ws.Cells(lastRow, 18).Value = GetNodeText(recordNode, "auth_results/spf/result")

べろべろべろべろっとちゃんと考えてくれます。
こんな感じでVBAが完成しました。

私なら3日~4日の作業が1時間に

私ノンプログラマーです。VBAを組んだことは何度もありますが、その都度調べてはコピペ、エラーが出ては修正という作業を繰り返してやっと「プログラムらしきモノ」が出来て「何となく動いている」という状況になります。
それが今回は僅か1時間にも満たない作業で見やすいVBAが完成しました。
これは使いこなさない手はないよね!

この記事が気に入ったらサポートをしてみませんか?