agenda 2003-01(下旬) おふざけでないッ

PURLのpartial redirectと恒久的なURIについて

公開
2003年1月31日

サイト内の全リソースに普遍的なURI(PURL (英語) )を与えるにはどうすれば良いのかを調べてみました。

リソース一つ一つにPURLを与えるには、リソースを公開するたびにPURLを貰ってくるしかないのですが、partial redirectという代替的な方法がありました。

  1. ユーザー登録が済んでいないならUser Registration Formからユーザー登録をする
  2. Partial Redirect Creation Form (英語) にアクセス
  3. IDPasswordを入力
  4. PR PURL基点となるPURLを入力
  5. Root URLリダイレクト先の基点となるURIを入力
  6. ボタンを押す

例えば基点となるPURL(PR PURL)をhttp://purl.org/jintrick/Personal/とし、リダイレクト先の基点となるURIをhttp://members.jcom.home.ne.jp/jintrick/Personal/としたなら、http://purl.org/jintrick/Personal/agenda.htmlhttp://members.jcom.home.ne.jp/jintrick/Personal/agenda.htmlに自動的にリダイレクトされることになります。

注意点

PURLとpartial redirectとの違い

ただし、partial redirectを利用したURIは、実際のリソースの存在を保証するものではなく、単なるリダイレクトなのだそうです。例えばhttp://purl.org/foo/bar/baz.htmlがpartial redirectでhttp://foo.com/bar/baz.htmlにリダイレクトされていたとします。PURLユーザーは、このhttp://purl.org/foo/bar/baz.htmlというURLを直接メンテナンスすることができません。このURLを保持したければ、実際のリソースの場所を調整するしかないわけです。これはPURLの利点の一つである「サイト構成とURIの分離」が失われていることを意味しますが、サイト構成なるものを変更しない私には無関係なので、他のPURLと同じようにpartial redirectを利用することにしました。

PURLはPR PURLに変更できない

既にPURLが存在している場合、それをPR PURLに変更することはできないようです。例えば私の場合、http://purl.org/jintrick/personnel/ というPURLを貰っていましたが、これをhttp://members.jcom.home.ne.jp/jintrick/Personal/に関連付けたPR PURLとすることは出来ませんでした。そのため、改めてhttp://purl.org/jintrick/Personal/をPR PURLとして取得しました。

Top Level Domainについて

PR PURLの入力欄には/NET/という文字列が最初から入力されていますが、これはtop-level-domainと呼ばれるものを取得していない場合には変更できません。top-level-domainは申請することで取得可能です。Top Level Domain Request Formから申請します。

PURL、PR PURLは変更不可能

コンセプトからすれば当たり前も良いところなのですが、PURLやPR PURLは変更、削除できません。一度作成したら最後です。URIの文字列はよくよく考えねばなりません(自戒)。

PURLに適した用途とは

一意かつ不変であることがPURLの最大の特長であり、実際のリソースの「位置」を示すものとしては若干非力ですから、名前空間URIとして利用するのが一般的な用途です。

再び設置す

公開
2003年1月29日

Safeモードすら起動しない状態になってしまい、再セットアップを余儀なくされたというお話。

くもり窓 指でなぞりし落書きの
消え行く様を 茫然と見き

歌を詠んでいる場合ではないわけですが、これで恨み言の代わりということにします。

バックアップ

この3年間PCは概ね安定しており、バックアップを怠っていたのでありました。

コマンドプロンプトからcopy *.* A:とか色々やらざるを得なかったのですが、フォルダ名に2バイト文字を使っていたものはどうすれば良いのか分からず、editでMS-DOS editorを起動して、しょぼいGUIを使って一つ一つ保存しました。その面倒なことといったら……。何故このような非生産的なことに時間を取られねばならないのでしょうか。それでも生活に関わるファイルは根性でバックアップ。

このサイトへの影響

修理に出すくらいならMacに変えようと思っていたのですが、取りあえず何とかなってしまったのでOSは変わりません。サイトは相変わらずMS寄りの内容です。また、PCの具合が悪いからといってこのサイトが閉鎖するということはありません。むしろもっと沢山のPURLを貰ってこようと思いました。J-COMがどうなるか分かりませんし、今のうちからやっておこうと思ったわけです。

XSLT 1.0で正規表現を使う(MSXML限定)/ XPathのstring-value

公開
2003年1月22日

XSLT 1.0にはビルトイン関数があります。しかしそれらはXPathの文法やデータモデルに従ったものなのだから、XSLTの関数というより、XSLTで使用できるXPathの拡張だと思うのは私だけでしょうか。それはさておき、XPathの関数をMSXMLの独自要素を利用して定義する形で、XSLT内で正規表現を使ってみようという話です。

MSXML(4.0)を使う利点は、今のところ二つ程認識しています。一つは高速であるということ。もう一つは、スクリプト言語で簡単にXPath関数を拡張できるという点です。ところがこの二つの利点は同時に成り立ちません。独自に関数を定義するとスクリプトエンジンを起動するので処理が遅くなるのだそうです。ですから利用する際には気をつけたほうが良いと思います。

具体例

XPathのビルトイン関数にcontains関数というブール値を返す関数がありますが、これに似た、正規表現が使用可能な機能強化版を作ってみます。

まず、普通にJScriptの関数を書きます。

function contains(_str, _sReg)
{
  var re = new RegExp(_sReg);
  return re.test(_str);
}

これは、第一引数(文字列)の中に、第二引数(文字列として表現された正規表現)で表された正規表現にマッチする文字列が存在した場合にtrue, そうでなければfalseを返却するJScript関数です。

注意点
関数の戻り値はXPath 1.0で扱えるデータ型に従って「ノードリスト、文字列、ブール値、数値」の何れかにします。配列オブジェクトなどを戻り値にしてしまうと多分エラーが出るでしょう(もしくはtoString関数が適用されるかも知れません)。引数も同様、XPath 1.0で扱えるデータ型でなければなりません。例えば正規表現オブジェクトを引数にしたつもりで/reg/g等と記述すれば、g要素とやらを含んだノードリストとして評価されてしまいます。因みに、XPathの仕様書ではノードリストはnode set(ノード集合)となっていますが、MSXMLユーザ的にはDOM互換の「ノードリスト」という概念で考えた方が色々と便利です。

次に、作成したJScript関数を、msxsl:script要素の内容としてトップレベル(xsl:stylesheet要素の直下)に配置します。

<msxsl:script language="JScript" implements-prefix="jtr">
function contains(_str, _sReg) {
	var re = new RegExp(_sReg);
	return re.test(_str);
}
</msxsl:script>

language属性に、使用するスクリプト言語を指定します。JScript、JavaScriptで確認しました。implements-prefix属性の値は、関数名(Qname)にくっつけるXML名前空間の接頭辞です。urn:schemas-microsoft-com:xsltと一緒にスコープ内(ルート要素かその辺)で宣言しておく必要があります。

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:jtr="http://purl.org/jintrick/schema/personnel"
 xmlns:msxsl="urn:schemas-microsoft-com:xslt"
 exclude-result-prefixes="jtr msxsl"
>

<msxsl:script language="JScript" implements-prefix="jtr">
function contains(_str, _sReg) {
	var re = new RegExp(_sReg);
	return re.test(_str);
}
</msxsl:script>
</xsl:stylesheet>

これで、例えば次のようにしてこのユーザー定義関数を呼び出すことが出来ます:

<xsl:template match="foo">
 <xsl:choose>
  <xsl:when test="jtr:contains(string(self::node()), '\s')">
   <p>空白文字を検出しました。</p>
  </xsl:when>
  <xsl:otherwise>
   <p>空白文字はありません。</p>
  </xsl:otherwise>
 </xsl:choose>
</xsl:template>

foo要素の子孫ノードの文字列値に空白文字(半角スペース、タブ文字、改行文字等)が含まれているかどうかで条件分岐しています。

XPathのstring-valueについて

前述の最後の例のどこに「子孫ノード」なんていう表現があるのかと思った方はXML Path Language (XPath) (英語) の5. Data Modelを参照してください。XPathでは、ノードはそれぞれstring-valueという特別なプロパティを持っていることになっています。要素ノードにおいては、子孫の(DOMでいう)nodeValueを文書順(document order)に継ぎ足した値になります。string関数はこのstring-valueを取り出すわけです。極めて重要な約束事だと思うのですが、仕様書以外ではお目にかかった試しがありません。手持ちの本(付録のリファレンスしか使っていない)でも、何だかはぐらかしてあります。 関係ないけどp.533の非XMLなXML Schemaインスタンスは何時になったら訂正されるのだろう 訂正されました(XML SQUARE 正誤表

dl要素をtable要素に変換(XSLT)

公開
2003年1月19日

属性ノードの場合、値が正規化されたものがstring-valueになるそうです。というわけで、去年(2002年)書いた複数の属性値を扱うテンプレートに属性値を正規化する手順(normalize-space())は不必要だったのでした。

軸名
意味
主ノード型
ancestor
祖先
要素ノード
attribute
属性
属性ノード
<dl title="XPathにおける軸名とその性質">
<dt>軸名</dt>
<dd>意味</dd>
<dd>主ノード型</dd>

<dt>ancestor</dt>
<dd>祖先</dd>
<dd>要素ノード</dd>

<dt>attribute</dt>
<dd>属性</dd>
<dd>属性ノード</dd>
</dl>

このようなdl要素を次のようなtable要素に変換したいわけです。

XPathにおける軸名とその性質
軸名 意味 主ノード型
ancestor 祖先 要素ノード
attribute 属性 属性ノード
<table summary="XPathにおける軸とその性質">
 <caption>XPathにおける軸名とその性質</caption>
 <thead>
  <tr>
   <th scope="col">軸名</th>
   <th scope="col">意味</th>
   <th scope="col">主ノード型</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <th scope="row">ancestor</th>
   <td>祖先</td>
   <td>要素ノード</td>
  </tr>
  <tr>
   <th scope="row">attribute</th>
   <td>属性</td>
   <td>属性ノード</td>
  </tr>
 </tbody>
</table>

フラットにだらだらと並んだdt、dd要素をグループ化するテンプレートを書くのは結構面倒なのですが、好い加減私も慣れました。グループ化の応用例はこれで最後にします。

このdl要素を他の普通のdl要素と区別する為名前空間を与えます。その接頭辞をjtrとすれば、jtr:dl要素にマッチするテンプレートを書くことになります。

例によってソース文書となる似非XHTML文書のデフォルトの名前空間がnullであるとしますと:

<xsl:template match="jtr:dl">
 <table summary="{attribute::title}">
  <caption><xsl:value-of select="string(attribute::title)" /></caption>
  <thead>
   <tr>
    <xsl:for-each select="child::dt[position() = 1]">
     <th scope="col"><xsl:value-of select="string(self::*)" /></th>
     <xsl:for-each select="following-sibling::dt[position() = 1]/preceding-sibling::dd">
      <th scope="col"><xsl:value-of select="string(self::*)" /></th>
     </xsl:for-each>
    </xsl:for-each>
   </tr>
  </thead>
   <tbody>
   <xsl:for-each select="child::dt[position() &gt; 1]">
    <xsl:variable name="posDT">
     <xsl:number level="single" count="*" />
    </xsl:variable>
    <tr>
     <th scope="row"><xsl:value-of select="string(self::*)" /></th>
     <xsl:for-each select="following-sibling::dt[position() = 1]/preceding-sibling::dd">
      <xsl:variable name="posDD">
       <xsl:number level="single" count="*" />
      </xsl:variable>
      <xsl:if test="$posDT &lt; $posDD">
       <td><xsl:apply-templates select="child::node()" /></td>
      </xsl:if>
     </xsl:for-each>
    </tr>
   </xsl:for-each>
  </tbody>
 </table>
</xsl:template>

position関数がもう少し賢ければ「まともな」テンプレートが書けそうなものなのですが、取り敢えず見た目シンプルなものを追求すれば私にはこれが限界です。

膨大なデータを扱う場合この方法はナンセンスで、予め変数にdt要素とdd要素の位置をバインドして、for-each要素のselect属性のロケーションパスのみで範囲を特定しなければなりません。そのかわり見た目が腐ってしまい後で見た際に訳が分からなくなってしまいます。この例ではxsl:ifで条件を絞り込むする方法を取っていますが、本来XPathで解決したいところです。

XPathとXSLTにおけるコンテクストポジション(position関数)の扱いの違い

公開
2003年1月17日

実装の不具合かと思ったのですが、preceding-sibling軸でカレントノードリストを変更すると、各カレントノードのコンテクストポジションは文書順になります。次のHTML文書の断片をご覧下さい:

<p>テキストノード[1]<em>em要素[2]</em>テキストノード[3]<br />テキストノード</p>

このp要素にマッチするテンプレート内で、子のbr要素より前に登場する「弟ノード達」をカレントノードリストにしてみます。尚、簡略化の為このHTMLの各ノードは名前空間を持たないことにしておきます。

<xsl:for-each select="child::br[position() = 1]/preceding-sibling::node()">
  <!-- テンプレート -->
  [<xsl:value-of select="string(position())" />]
</xsl:for-each>

「<!-- テンプレート -->」にはノードをそのままコピーするものを適用するとして、これは次のような結果を構築します。

  • テキストノード[1][1]<em>em要素[2][2]</em>テキストノード[3][3]

ソース文書と同じ順番にインスタンス化されています。私はこれに驚きました。カレントノードリストは、XPathのロケーションパスで指定されたノードリストを文書順に並び替えたものになるということですから。

  • child::br[position() = 1]/preceding-sibling::node()

というロケーションパスで指定されたノードリスト内の各ノードは次の順番で並んでいます。正確には、この順番に応じたコンテクストポジションを持っています:

  1. テキストノード[3]
  2. em要素[2]
  3. テキストノード[1]

ところが、XSLTプロセッサはこれを文書順に並べ替えたものをカレントノードリストにしてしまいます。

  1. テキストノード[1]
  2. em要素[2]
  3. テキストノード[3]

「してしまいます」というか、まあ有り難い場面は多いのですが、XPathユーザ的に若干不自然な挙動ではあります。

因みにカレントノードリストを、「コンテクストポジションが1である弟ノード」にしてみますと:

<xsl:for-each select="child::br[position() = 1]/preceding-sibling::node()[position() = 1]">
  <!-- ノードコピー用テンプレート -->
  [<xsl:value-of select="string(position())" />]
</xsl:for-each>

結果は次のようになります。

  • テキストノード[3][1]

先程の例と比べれば、並べ替えが行われていることは明らかです。

おまけ:対策

カレントノードリスト内のノードの順番を、ロケーションパスで指定されたノードリストと同じにするにはどうすれば良いでしょうか。

軸がpreceding軸、preceding-sibling軸、ancestor軸、ancestor-or-self軸の場合、コンテクストポジションを見て、降順(数値の大きい順)にソートしてやれば解決しました。というか文書順になるだろうと思って最初にこれを試したら逆だったので驚いたわけですが。

<xsl:for-each select="child::br[position() = 1]/preceding-sibling::node()">
  <xsl:sort order="descending" data-type="number" select="position()" />
  <!-- テンプレート -->
  [<xsl:value-of select="string(position())" />]
</xsl:for-each>

結果は次のようになります:

  • テキストノード[3][1]<em>em要素[2][2]</em>テキストノード[1][3]

ロケーションステップが一回であり、軸がchild軸、descendant軸、descendant-or-self軸、following軸、following-sibling軸の場合は特に気遣う必要はありません。ロケーションステップが2回以上行われ、これらの軸以外で構成されるものが出てくる際には場合によっては分割する必要があります。for-each要素を入れ子にします。

まとめ

以上、br要素で分割されたノードリストを、それぞれXHTML 2.0のl要素(line要素)としてグループ化するという試みの最中に出くわしたケースについて書いてみました。要点を纏めると次のようになります:

  • ロケーションパスの述語内に登場するposition()関数は、コンテクストポジションを返す
  • それ以外では、position()関数はカレントノードリスト内のカレントノードの位置を返す
  • カレントノードリストは、ロケーションパスで指定されたノード集合を文書順に並べ替えたものになる

br要素からl要素(line要素)を生成

公開
2003年1月16日

www-html@w3.org from January 2003: XHTML 2.0 considered harmful (英語)の一連の議論で、br要素が如何に「便利」であるかを熱弁している人がいるのですが、この糞要素のお陰でHTMLという言語は勘違いされるのでありますよ。

より良い言語を作るのに、人間様が過去の古い計算機の都合に合わせてどうするのですか。backwards compatibilityが大事なら、あまり良くない言語を使い続ければ宜しいでしょう。私も当分そうするつもりです。何故足を引っ張る必要がありますか。本気で理解不可能です。

というわけで(どういうわけで?)、br要素で区切られたノードリストを、XHTML 2.0のl要素(line要素)としてグループ化するXSLTテンプレートを考案してみました。XHTML 2.0は草案ですし名前空間もどうなるか分からないので、とりあえず名前空間は無いものとして扱います。ソース文書の方も名前空間無しってことで。

これは例えば、hr要素か何かで区切られたノードリストをdiv要素か何かとしてグループ化する際にも応用できる……と……思います……?

<xsl:template name="brでl要素に分割するテンプレ">
 <xsl:for-each select="child::br[position() = 1]">
  <l>
   <xsl:for-each select="preceding-sibling::node()">
    <xsl:apply-templates select="self::node()" />
   </xsl:for-each>
  </l>
 </xsl:for-each>
 <xsl:for-each select="child::br">
  <l>
   <xsl:if test="position() = last()">
    <xsl:for-each select="following-sibling::node()">
     <xsl:apply-templates select="self::node()" />
    </xsl:for-each>
   </xsl:if>
   <xsl:variable name="posBR">
    <xsl:for-each select="following-sibling::br[position() = 1]">
     <xsl:number level="single" count="node()" />
    </xsl:for-each>
   </xsl:variable>
   <xsl:for-each select="following-sibling::node()">
    <xsl:variable name="posNode">
     <xsl:number level="single" count="node()" />
    </xsl:variable>
    <xsl:if test="name() != 'br' and $posNode &lt; $posBR">
     <xsl:apply-templates select="self::node()" />
    </xsl:if>
   </xsl:for-each>
  </l>
 </xsl:for-each>
</xsl:template>

主にp要素にマッチするテンプレート内でこのように呼び出します。

<xsl:template match="p">
 <p>
  <xsl:apply-templates select="attribute::*" />
  <xsl:choose>
   <xsl:when test="boolean(child::br) = true()">
    <xsl:call-template name="brでl要素に分割するテンプレ" />
   </xsl:when>
   <xsl:otherwise>
    <xsl:apply-templates select="child::node()" />
   </xsl:otherwise>
  </xsl:choose>
 </p>
</xsl:template>

XML的に腐ったデータを再生するには、冗長なXSLT文書を書かなくてはならないと言うことでしょうか。一方DOMインターフェイスを使えばこのような変換方法の考案はサクサクですね(謎)。XSLT(1.0)やXPath(1.0)はまだ非力ですから、文章を書くようにDOMを使いこなせるなら(謎)、無理して勉強する必要も無いと思います。

というか、スタイルシートが整形過程であるとするなら、DOMインターフェイスを使った変換だって立派な「スタイルシート」です。コードはアホみたいに膨らむし処理は遅めかもしれませんが、こっちの方が痒いところに手が届きます。もう比較にならないくらい圧倒的。SAXってどうなんだろう。

ロケーションパスと式(Expression)の冗長性について

先程のサンプルではロケーションパスと式(Expression)について省略せずに書いていますが、頻繁にXSLTを弄る機会を持てない方にはお勧めの書き方です。常にデータ型とカレントノードを意識して書くことが出来るからです。