id:koharusugiura のブログ

フロントエンドエンジニアの id:koharusugiura による個人ブログです。

フロントエンドで知っておきたい要素指定の考え方

この記事は当時わたしが在籍していた株式会社アニメイトラボが公開していたブログであるアニメイトラボ開発者ブログで書かれたものです。株式会社アニメイトラボは2018年に親会社である株式会社アニメイトに権利義務を継承し、解散となりました。それに伴ってアニメイトラボ開発者ブログの記事も失われてしまったため、わたしの個人ブログであるこちらに転記しています。


はじめまして、アニメイトラボのフロントエンドエンジニア id:koharusugiura です。

今回は XPath について書きたいと思います。

HTML 文書から任意の要素を取得する際、多くの場合 jQuerySelectors API などでセレクターを使いますよね?

セレクターは細かい条件の指定はできませんが、簡単に要素が取得でます。しかし細かい条件によって要素の取得を行うことができません。

細かい条件で要素の取得を行う場合は、XPath を使うことで開発が捗ります。

XPathはセレクターよりも少々複雑な仕様となっていますので、まずは比較をしながらご説明したいと思います。

この記事は animateLAB Advent Calendar 2015 23 日目の記事です。

※ この記事でサンプルとして記載しているコードは特筆のない限り ECMAScript 3.1 に準拠しています。

JavaScript で XPath を扱う

JavaScript で XPath を扱う場合は DOM Level 3 XPath を使います。

DOM Level 3 XPath は複雑な条件指定ができますが、Selectors API と比べると少々難解な仕様です。なので、この記事では以下のような DOM Level 3 XPath をラップする関数を用います。

function $x(xpath, context) {
  context = context || document;
  var result = [];
  var xpathEvaluator = new XPathEvaluator();
  var xpathExpression = xpathEvaluator.createExpression(xpath, null);
  var domObject = xpathExpression.evaluate(context, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
  var element;
  while ((element = domObject.iterateNext())) {
    result.push(element);
  }
  element = undefined;
  return result;
}

タグの名前から任意の要素を取得

// Selectors
var elements = document.querySelectorAll('a');

// XPath
var elements = $x('//a');

XPath は要素の取得を行う際に、ルート要素からたどります。そのため、// を前置します。

// は木構造 (データ構造) の省略を意味します。今回の場合はルート要素からの省略になり、HTML 文書中に含まれる全ての子要素を走査します。

ID から任意の要素を取得

// Selectors
var element = document.querySelector('#element-id');

// XPath
var element = $x('id("element-id")')[0];

XPath には、 JavaScript のように関数があります。

id 関数は引数に与えられた ID を持つ要素を取得します。

任意の親要素を直上に持つ子要素を取得

// Selectors
var elements = document.querySelectorAll('p > *')

// XPath
var elements = $x('//p/*');

XPath は各要素を / で区切ることによって木構造を表現できます。

p 要素の子要素を全て取得する場合はセレクターと同様に * を用います。

任意の親要素を持つ子要素を取得

// Selectors
var elements = document.querySelectorAll('p *');

// XPath
var elements = $x('//p//*');

前述した通り、// は木構造の省略を意味します。これはルート要素からではなく、木構造の途中からでも使えます。

特定の属性を持つ任意の要素を取得

// Selectors
var elements = document.querySelectorAll('a[href="https://example.com/"]');

// XPath
var elements = $x('//a[@href = "https://example.com/]');

XPath では [] で囲むことにより細かい条件を指定できます。

@attribute-name で属性の名前が取れ、=!= で任意の値と合致、もしくは合致しないかを比較します。

クラス名から任意の要素を取得

HTML の class 属性 は複数の値が持てる属性となっています。そのため、セレクターよりも汎用性の高い XPath では条件の指定が複雑です。

// Selectors
var elements = document.querySelectorAll('.element-class-name');

// XPath
var elements = $x('//*[contains(concat(" ", normalize-space(@class), " "), " element-class-name ")]');

normalize-space 関数は引数に指定された文字列の前後の空白を除去し、また連続された空白を一つにし、空白の正規化を行います。正規化を行なった class 属性に concat 関数を使い前後に空白を付与します。

上記の処理を行うことで class 属性が class-name-1 class-name-2 class-name-3 という文字列になります。そこで、contains 関数で前後に空白をつけたクラス名が含まれているか、確認できます。

少し複雑ではありますが、HTML 文書を扱う上では頻出する条件なので覚えてておくと良いでしょう。

XPathの強み

実は、これまでのものはセレクターでもできる条件でした。ここからは、XPathを使うメリットをお伝えします。

セレクターでは要素は名前が完全一致していないと取得ができません。ですが、XPath では local-name 関数で要素の名前を得られるので、要素の名前を元にした条件の指定ができます。

// XPath
var elements = $x('//*[string-length(local-name()) = 2 and starts-with(local-name(), "h")]');

string-length 関数で要素の名前が 2 文字であるかを確認し、starts-with 関数で h という文字列で要素の名前がはじまっているかを確認しています。

上のような XPath 式で h1h2 といった見出し要素の取得ができます。

任意の条件に合致する要素の子要素を取得できる

セレクターでは任意の子要素を持つ親要素を取得することはできません。ですが、XPath はセレクターとは違い [ と ] で囲む条件に子要素を含むことができるため、取得することが可能です。

<div class="wrapper">
  <div class="content">
    <h2 class="title">headline</h2>
    <p>paragraph.</p>
  </div>
</div>
// XPath
$x('//div[contains(concat(" ", normalize-space(@class), " "), " content ") and h2[contains(concat(" ", normalize-space(@class), " "), " title ")]]');

最後に

DOM Level 3 XPath はこの記事を執筆時点 (2015 年 12 月 25 日) での最新版である Internet Explorer 11 を含め、Internet Explorer では使えません。ですが、それ以外のウェブブラウザーでは Firefox、Safari、Google Chrome、Microsoft Edge と多くの環境で使えます。

もちろん、Internet Explorer で使えないというのは、決して無視できる要素ではありません。

ですが、XPath のように複雑な条件指定は魅力的です。対象とする環境を絞れるのであればセレクターではなく XPath を使うことを検討してみてはいかがでしょうか。