概要
XSLTの勉強目的で、ADODB.RecordsetをHTMLのテーブルに変換する処理を作成したため、備忘録として残す。
記事内で使用しているファイルやコードは以下の場所にも保存している。
前提
- Windows上のVBA 7.1.1136 64bit(32bitでも問題ないはず)
以下のタイプライブラリを使用する。
- ADODB = Microsoft ActiveX Data Objects 6.1 Library
- MSXML2 = Microsoft XML, v6.0
XSLTのバージョンは1.0とする(=MSXMLが対応しているバージョン)。
XSLT とは?
XMLを他の形式(XMLやHTML、テキスト)に変換する方法のこと。
テンプレートとなるXSLファイル(*.xsl
)を用意し、それをXMLに適用することで結果の文字列を得ることができる。
ADODB.RecordsetのXMLファイルの構造
XSLファイルの作成には、元となるXMLファイルの情報を知る必要がある。
この記事では、以下のVBAコードで作成したRecordsetのXMLファイルを前提とする。
Recordset定義
Sub Sample_CreateRecordset()
'ADODB.Recordset の定義(データベースと接続しない場合)。
Dim rs As ADODB.Recordset
Set rs = VBA.Interaction.CreateObject("ADODB.Recordset")
'列の名前と入る値の種類の定義。
'サンプルの都合上、英数字の名前と日本語の名前としている。
With rs.Fields
.Append "Strings", adBSTR
.Append "日付", adDate
End With
rs.Open
'ADODB.Recordset に中身を設定。
'1行目は Hoge\nFuga と 今日の日付
'2行目は Piyo と 24/04/01 12:34:56
rs.AddNew Array(0, 1), Array("Hoge" & vbLf & "Fuga", Date)
rs.AddNew Array(0, 1), Array("Piyo", #1/23/2024 12:34:56 PM#)
'ADODB.Recordset を XML として保存する。
Dim domDoc As MSXML2.DOMDocument60
Set domDoc = VBA.Interaction.CreateObject("MSXML2.DOMDocument.6.0")
rs.Save domDoc, adPersistXML 'adPersistXML = 1
'ファイルとして保存。
domDoc.Save RecordsetのXMLの保存先
'イミディエイトウィンドウでの確認用。
Debug.Print domDoc.XML
End Sub
Recordsetの情報
作成したRecordsetは以下のような表の情報を格納したものとなる。
Strings | 日付 |
---|---|
Hoge Fuga |
2024-04-07T00:00:00 |
Piyo | 2024-01-23T12:34:56 |
RecordsetのXML
domDoc.Save
で保存されるXMLファイルは以下のようになる。
<xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">
<s:Schema id="RowsetSchema">
<s:ElementType name="row" content="eltOnly" rs:updatable="true">
<s:AttributeType name="Strings" rs:number="1" rs:write="true">
<s:datatype dt:type="string" dt:maxLength="4294967295" rs:precision="0" rs:long="true" rs:maybenull="false"/>
</s:AttributeType>
<s:AttributeType name="c1" rs:name="日付" rs:number="2" rs:write="true">
<s:datatype dt:type="dateTime" rs:dbtype="variantdate" dt:maxLength="16" rs:precision="0" rs:fixedlength="true" rs:maybenull="false"/>
</s:AttributeType>
<s:extends type="rs:rowbase"/>
</s:ElementType>
</s:Schema>
<rs:data>
<rs:insert>
<z:row Strings="Hoge
Fuga" c1="2024-04-07T00:00:00"/>
<z:row Strings="Piyo" c1="2024-01-23T12:34:56"/>
</rs:insert>
</rs:data>
</xml>
ここで注目すべき点としては以下の2点となる。
-
xml/s:Schema/s:ElementType/s:AttributeType
のノードにフィールド名らしき情報(=表の見出し)があること -
xml/rs:data//z:row
の属性としてrs.AddNew
で追加した情報(=表の中身)が含まれていること
XSLファイルの定義
XSLファイルは、XSLTで変換するときのテンプレートとなるファイルのこと。
ここではRecordsetの情報をそのままHTMLの表にするためのXSLを定義する。
XSLファイルの使い方は以降に示す。
XSLファイルの中身
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0"
xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882"
xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
xmlns:rs="urn:schemas-microsoft-com:rowset"
xmlns:z="#RowsetSchema"
xmlns:my="MSXMLのXSLTでcdata-section-elementsを利用するための名前空間定義(任意の文字列で可)"
exclude-result-prefixes="s dt rs z my"
>
<xsl:output
method="xml"
indent="yes"
omit-xml-declaration="yes"
encoding="utf-8"
cdata-section-elements="my:cdata"
media-type="text/html"
/>
<xsl:template match="/">
<!-- <!DOCTYPE html>の出力用。-->
<xsl:text disable-output-escaping="yes"><!DOCTYPE html> </xsl:text>
<html lang="ja">
<head>
<meta charset="utf-8"/>
<title>Recordset XML to HTML</title>
<!-- HTMLファイル内にCSS 定義を含める(cdata-section-elementsのサンプル) -->
<style text="text/stylesheet">/*<my:cdata><![CDATA[*/
table, th, td {
border : solid thin;
}
/*]]></my:cdata>*/</style>
</head>
<body>
<table>
<!-- 表の見出しとしてRecordsetのフィールド名を出力 -->
<thead>
<tr>
<xsl:for-each select="xml/s:Schema/s:ElementType/s:AttributeType">
<th>
<!--
Recordsetのフィールド名に日本語などが含まれる場合、
@nameはc1などの表記となり、実際の名前は@rs:nameで定義される。
そのため、@rs:nameがある場合は@rs:nameを出力し、そうでない場合は@nameを出力している。
-->
<xsl:choose>
<xsl:when test="@rs:name">
<xsl:value-of select="@rs:name" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="@name" />
</xsl:otherwise>
</xsl:choose>
</th>
</xsl:for-each>
</tr>
</thead>
<tbody>
<xsl:for-each select="xml/rs:data//z:row">
<tr>
<xsl:for-each select="@*">
<td>
<!-- LF(
=改行)があった場合、brタグに置き換えるテンプレートを呼び出す。 -->
<xsl:call-template name="replace-text">
<xsl:with-param name="base-text" select = "."/><!-- 元の文字列 -->
<xsl:with-param name="replace-before" select="'
'"/><!-- 置換前の文字列 -->
<xsl:with-param name="replace-after"><br /></xsl:with-param><!-- 置換後の要素 -->
</xsl:call-template>
</td>
</xsl:for-each>
</tr>
</xsl:for-each>
</tbody>
</table>
</body>
</html>
</xsl:template>
<!-- 文字列置換用template -->
<xsl:template name="replace-text">
<xsl:param name="base-text"/><!-- 元の文字列 -->
<xsl:param name="replace-before"/><!-- 置換前の文字列 -->
<xsl:param name="replace-after"/><!-- 置換後の要素 -->
<xsl:choose>
<xsl:when test="contains($base-text, $replace-before)">
<!-- 置換前の文字列がある場合 -->
<xsl:value-of select="substring-before($base-text, $replace-before)"/>
<xsl:copy-of select="$replace-after"/>
<!-- 上の書き方では最初の1箇所しか置換できないため、再帰呼び出しで残りも置換する -->
<xsl:call-template name="replace-text">
<xsl:with-param name="base-text" select="substring-after($base-text, $replace-before)"/>
<xsl:with-param name="replace-before" select="$replace-before"/>
<xsl:with-param name="replace-after" select="$replace-after"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$base-text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
XML宣言
<?xml version="1.0" encoding="UTF-8"?>
この箇所は基本的に変更する必要は無い。
XSLファイルをUTF-8以外の符号化方式で保存する場合はencoding
を適宜変更すること。
xsl:stylesheet
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0"
xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882"
xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
xmlns:rs="urn:schemas-microsoft-com:rowset"
xmlns:z="#RowsetSchema"
xmlns:my="MSXMLのXSLTでcdata-section-elementsを利用するための名前空間定義(任意の文字列で可)"
exclude-result-prefixes="s dt rs z my"
>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0"
>
XSLとしての定義であれば、上記の部分だけで問題無い。
この記事では、Recordsetを対象としたり、CDATAセクションの定義のため、残りの部分を追加している。
なおxmlns:xsl="http://www.w3.org/1999/XSL/Transform"
のURLの部分は、他のXSLでも同じのため、Windowsフォルダなどを適当に*.xsl
で探せば同じものが見つかるはず。
xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882"
xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
xmlns:rs="urn:schemas-microsoft-com:rowset"
xmlns:z="#RowsetSchema"
xmlns:my="MSXMLのXSLTでcdata-section-elementsを利用するための名前空間定義(任意の文字列で可)"
xmlns:
の箇所はXML名前空間の定義となる。
my
以外はRecordsetのXMLの1行目にあったものと同じ物となる。
Recordsetの構造を指定するときにあった方が便利なため、ここで定義している。
<xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">
しかし、既定の設定ではこれらの名前空間の情報がそのまま結果のHTMLにも書き込まれてしまう。
ほとんどの名前空間の情報は結果のHTMLでは不要なため、exclude-result-prefixes
でRecordsetの名前空間の情報が出力されないようにしている。
<html lang="ja" xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">`
xsl:output
<xsl:output
method="xml"
indent="yes"
omit-xml-declaration="yes"
encoding="utf-8"
cdata-section-elements="my:cdata"
media-type="text/html"
/>
method="xml"
としてXMLへの変換を指示しているが、XSLT的にはこの箇所はmethod="html"
と指示することもできるようになっている。
しかし、method="html"
と指示すると整形式のXMLとはならず、個人的に扱いにくかった。
そのため、method="xml"
を明示し、整形式のXMLとして出力されるようにしている。
(参考)method="html"
を指示した場合の結果
- いくつかの終了タグを省略できるタグについて、終了タグが省略される(=整形式のXMLではなくなる)
-
head
タグ直後に以下が挿入される
<META http-equiv="Content-Type" content="text/html">
HTMLファイル冒頭
<xsl:template match="/">
<!-- <!DOCTYPE html>の出力用。-->
<xsl:text disable-output-escaping="yes"><!DOCTYPE html> </xsl:text>
<html lang="ja">
<head>
<meta charset="utf-8"/>
<title>Recordset XML to HTML</title>
<!-- HTMLファイル内にCSS 定義を含める(cdata-section-elementsのサンプル) -->
<style text="text/stylesheet">/*<my:cdata><![CDATA[*/
table, th, td {
border : solid thin;
}
/*]]></my:cdata>*/</style>
</head>
<body>
<xsl:template match="/">
により、ここから</xsl:template>
で閉じられるまでの範囲がHTMLとして出力される範囲となる。
基本的に<xsl:
で始まっていないタグはそのまま出力される。
style
タグ内のmy:cdata
タグは、xsl:outputのcdata-section-elements="my:cdata"
で設定しているタグのため、結果出力時に内部の<![CDATA[ ]>
が維持される。
cdata-section-elements
を設定していない場合は<![CDATA[ ]>
は結果に含まれない。
表見出し作成
<table>
<!-- 表の見出しとしてRecordsetのフィールド名を出力 -->
<thead>
<tr>
<xsl:for-each select="xml/s:Schema/s:ElementType/s:AttributeType">
<th>
<!--
Recordsetのフィールド名に日本語などが含まれる場合、
@nameはc1などの表記となり、実際の名前は@rs:nameで定義される。
そのため、@rs:nameがある場合は@rs:nameを出力し、そうでない場合は@nameを出力している。
-->
<xsl:choose>
<xsl:when test="@rs:name">
<xsl:value-of select="@rs:name" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="@name" />
</xsl:otherwise>
</xsl:choose>
</th>
</xsl:for-each>
</tr>
</thead>
この箇所で、表の見出し部分を作成している。
xsl:chooseを使ってRecordsetのXMLから列名に相当する情報を取得している。
表の中身作成
<tbody>
<xsl:for-each select="xml/rs:data//z:row">
<tr>
<xsl:for-each select="@*">
<td>
<!-- LF(
=改行)があった場合、brタグに置き換えるテンプレートを呼び出す。 -->
<xsl:call-template name="replace-text">
<xsl:with-param name="base-text" select = "."/><!-- 元の文字列 -->
<xsl:with-param name="replace-before" select="'
'"/><!-- 置換前の文字列 -->
<xsl:with-param name="replace-after"><br /></xsl:with-param><!-- 置換後の要素 -->
</xsl:call-template>
</td>
</xsl:for-each>
</tr>
</xsl:for-each>
</tbody>
</table>
</body>
</html>
</xsl:template>
この箇所で表の中身の作成を行っている。
<xsl:for-each select="xml/rs:data//z:row">
により、各z:row
要素についてtr
タグを生成している。
各セルの値は後述の文字列置換テンプレートにより、LF改行("'
'"
)をbr
タグに変換して出力をしている。
文字列置換テンプレート
<!-- 文字列置換用template -->
<xsl:template name="replace-text">
<xsl:param name="base-text"/><!-- 元の文字列 -->
<xsl:param name="replace-before"/><!-- 置換前の文字列 -->
<xsl:param name="replace-after"/><!-- 置換後の要素 -->
<xsl:choose>
<xsl:when test="contains($base-text, $replace-before)">
<!-- 置換前の文字列がある場合 -->
<xsl:value-of select="substring-before($base-text, $replace-before)"/>
<xsl:copy-of select="$replace-after"/>
<!-- 上の書き方では最初の1箇所しか置換できないため、再帰呼び出しで残りも置換する -->
<xsl:call-template name="replace-text">
<xsl:with-param name="base-text" select="substring-after($base-text, $replace-before)"/>
<xsl:with-param name="replace-before" select="$replace-before"/>
<xsl:with-param name="replace-after" select="$replace-after"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$base-text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
このxsl:templateは、RecordsetをHTMLに変換するだけであれば不要だが、自分の用途ではLFで改行された文字列を扱いたいことがあったため、作成したものとなる。
HTMLファイル冒頭ではmatch="/"
を指定していたが、ここではname="replace-text
でname
属性を定義している。
name
属性を指定することでxsl:call-templateにより再利用が可能となっている。
このテンプレートは表の中身作成から呼び出しを行っており、以下のような処理を行う。
-
base-text
内にreplace-before
の文字列が存在するか確認 - 存在する場合、
replace-before
の前後で分割し、replace-after
を間に挟む -
replace-before
の後ろ側をbase-text
として1.に戻る(再帰呼び出し) -
replace-before
が無くなれば終了
VBAによるXSLT変換
Public Sub Sample_Transform()
'Excel の VBA で、MSXML2(=Microsoft XML, v6.0)を参照設定し、
'ブックのあるフォルダ(`rootDir`)各種ファイルを保存している前提の記述。
'`rootDir`には以下のファイルがあること(result.html はこのプロシージャにより作成される)。
'Recordset.xml :Recordsetの中身。
'stylesheet.xsl :XSLファイル。
'result.html :出力ファイル。生成されたHTMLファイル。
'各種ファイルの保存されているフォルダ
Dim rootDir As String
rootDir = ThisWorkbook.Path
'Recordset の中身を XML 文書として読み込み。
Dim basDoc As MSXML2.DOMDocument60
Set basDoc = VBA.Interaction.CreateObject("MSXML2.DOMDocument.6.0")
basDoc.Load rootDir & "\Recordset.xml"
'スタイルシート(XSL ファイル)を XML 文書として読み込み。
'公式リファレンスによるとスタイルシートは MSXML2.FreeThreadedDOMDocument.6.0 とすることになっている。
'https://learn.microsoft.com/ja-jp/previous-versions/windows/desktop/ms762799(v=vs.85)
'簡単に試した限り MSXML2.DOMDocument.6.0 でもエラーになることはない。
Dim stSht As MSXML2.FreeThreadedDOMDocument60
Set stSht = VBA.Interaction.CreateObject("MSXML2.FreeThreadedDOMDocument.6.0")
stSht.Load rootDir & "\stylesheet.xsl"
'basDoc にスタイルシートを適用した XML 文書を取得する。
'結果が整形式の XML ではない場合は ADODB.Stream や文字列として返すようにするのが無難かも?
Dim resultDoc As MSXML2.DOMDocument60
Set resultDoc = TransformXML(basDoc, stSht)
'結果の XML 文書を保存。
resultDoc.Save rootDir & "\result.html"
End Sub
Public Function TransformXML( _
ByVal inBaseDocument As MSXML2.DOMDocument60, _
ByVal inStylesheet As MSXML2.FreeThreadedDOMDocument60 _
) As MSXML2.DOMDocument60
'inBaseDocument に inStylesheet を適用した XML 文書を返す処理。
'inBaseDocument :元となるXML文書。
'inStylesheet: :変換に使用するスタイルシート。
'return :変換後のXML文書。
240505
'xsl:include などで他の .xsl などを参照する場合は True にする必要がある。
'https://www.w3.org/TR/xslt-10/#element-include
'inStylesheet.resolveExternals = True
Dim tmpl As MSXML2.XSLTemplate60
Set tmpl = VBA.Interaction.CreateObject("MSXML2.XSLTemplate.6.0")
Set tmpl.stylesheet = inStylesheet
Dim p As MSXML2.IXSLProcessor
Set p = tmpl.createProcessor()
'結果を出力するXML文書。 p.output に設定する。
Dim destDoc As MSXML2.DOMDocument60
Set destDoc = VBA.Interaction.CreateObject("MSXML2.DOMDocument.6.0")
'<!DOCTYPE html>の箇所で
'destDoc.parseError.reason = "DTD は禁止されています。"
'のエラーとなってしまうため、DTDを許可させる。
'https://learn.microsoft.com/ja-jp/previous-versions/windows/desktop/ms766391(v=vs.85)
destDoc.SetProperty "ProhibitDTD", False
'DTDを元にValidationをしようとしてしまうため、検証を防ぐ。
destDoc.validateOnParse = False
'入力ファイル・結果の出力先を指定して、変換。
p.input = inBaseDocument
p.output = destDoc
p.transform
Set TransformXML = destDoc
Exit Function
'IXSLProcessor.output 備考。
'p.output に何も設定しなかった場合、 output プロパティは変換結果の文字列を返す。
'1回しか参照出来ないため、変数に取得しておいた方が無難(ローカルウィンドウで参照するだけで消えてしまう)。
'p.output には ADODB.Stream も指定可能。
'変換後が整形式のXMLではない場合はこちらを使うのがよさそう。
Dim sr As ADODB.Stream
Set sr = VBA.Interaction.CreateObject("ADODB.Stream")
sr.Open
sr.Type = adTypeText
sr.Charset = "utf-8" 'xsl:output@encoding と設定値を合わせること(合わせない場合は文字化け)。
p.output = sr
p.transform
sr.Position = 0
End Function
ExcelのVBEでADODB
&MSXML2
の参照設定を行い、ブックのあるフォルダに以下のファイルを保存している前提の処理となる。
ファイル名 | 内容 |
---|---|
Recordset.xml | Recordsetの中身 |
stylesheet.xsl | XSLファイル |
基本的な処理の流れはリファレンスから大きく変えてはいないが、
以下の箇所はこの記事の方法でHTMLの表を出力するうえで重要な箇所となる。
'結果を出力するXML文書。 p.output に設定する。
Dim destDoc As MSXML2.DOMDocument60
Set destDoc = VBA.Interaction.CreateObject("MSXML2.DOMDocument.6.0")
'<!DOCTYPE html>の箇所で
'destDoc.parseError.reason = "DTD は禁止されています。"
'のエラーとなってしまうため、DTDを許可させる。
'https://learn.microsoft.com/ja-jp/previous-versions/windows/desktop/ms766391(v=vs.85)
destDoc.SetProperty "ProhibitDTD", False
'DTDを元にValidationをしようとしてしまうため、検証を防ぐ。
destDoc.validateOnParse = False
'入力ファイル・結果の出力先を指定して、変換。
p.input = inBaseDocument
p.output = destDoc
まず、xsl:outputにも記載したように、結果のHTMLは整形式のXMLとしても扱える形式としている。
そのため、結果が整形式のXMLであることを明示するためにIXSLProcessor.output
にはMSXML2.DOMDocument.6.0
を指定している。
またMSXML2.DOMDocument.6.0
の初期設定ではDTDを禁止する設定となっており、結果に<!DOCTYPE html>
が含まれると解析エラーとなってしまう。
そのため、setPropertyでDTDを許可するように設定している。
DTDを許可した場合、<!DOCTYPE html>
の記述では要素の定義がされていない状態のため、XMLを検証するとエラーとなってしまう。
今回の用途では検証は不要なためvalidateOnParse = False
で検証を省略させている。
結果のHTMLファイル
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8"/>
<title>Recordset XML to HTML</title>
<style text="text/stylesheet">/*<my:cdata xmlns:my="MSXMLのXSLTでcdata-section-elementsを利用するための名前空間定義(任意の文字列で可)"><![CDATA[*/
table, th, td {
border : solid thin;
}
/*]]></my:cdata>*/</style>
</head>
<body>
<table>
<thead>
<tr>
<th>Strings</th>
<th>日付</th>
</tr>
</thead>
<tbody>
<tr>
<td>Hoge<br/>Fuga</td>
<td>2024-04-07T00:00:00</td>
</tr>
<tr>
<td>Piyo</td>
<td>2024-01-23T12:34:56</td>
</tr>
</tbody>
</table>
</body>
</html>
参考:PowerShellの場合
using namespace System.Xml;
using namespace System.Xml.Xsl;
[XslCompiledTransform]$trans = [XslCompiledTransform]::new();
$trans.Load("$PSScriptRoot\stylesheet.xsl");
$trans.Transform(
"$PSScriptRoot\Recordset.xml",
"$PSScriptRoot\output.html"
);
参考ページ
- XSLT1.0のリファレンス
- MSXML(VBAのXML操作ライブラリ)についてのリファレンス
- MSXMLのリファレンスの中で特に重要と思われるページ
- Microsoft公式の実装ではXSLT1.0までしか準拠していないことの確認。