開発チームの下田です。

XMLの特定の部分を指定するためのXML Path Language(以下、XPathと表記します)という言語があります。
HTMLを解析して自動テストの作成やスクレイピング、XML設定ファイルの書き換えに使います。簡単な割に、覚えておくと地味に便利なやつです。

とりあえず書いてみる

何はともあれ、覚えるためには書いてみます。

ブラウザが一番手軽なXPathの実行環境だと思います。FirefoxとChromeはXPathが実装されています。コンソールで次のコードを実行してみてください。コンソールはF12を押せば開きます。

document.evaluate('//html/head/title', document, null, XPathResult.STRING_TYPE, null).stringValue

正常に実行できれば、開いているページのタイトルが表示されます。

concole
evaluateの1つめの引数にとっている'//html/head/title'がXPathでの表記です。今回はXPathの説明のため、他の引数は割愛します。
なんとなく察しがつくと思いますが、ドキュメント内のhtmlタグ直下のheadタグ直下のtitleタグを表しています。
cssセレクタでいうと'html > head > title'と同じ要素を示します。

xpath1


他にも、次のような書き方でtitleタグを表せます。
//title
titleタグはhtml内は必ず一つです。つまり、ドキュメント内のいずれかのtitleタグを指定するだけで指定できます。

/html/head/title
先頭の/が1つだけです。1つ(/)の場合、直下の要素のみを示します。2つ(//)の場合、子や孫など配下全てを示します。この場合、一番上になるルート要素の中からhtmlタグを選び、その直下のheadの直下のtitleタグを示します。
先ほどの'//title'はドキュメント内のtitleタグを指定したので取得できますが、'/title'とするとルート要素にtitleタグは無いため取得できません。

xpath2

検索

要素やテキストに一致するものを検索することもできます。

要素は「@要素名」と表します。[]で囲んだ内側に条件式を書くことができ、=は完全一致です。
次の例は、descriptionのタグを表しています。
//meta[@name="description"]

テキストを検索する場合は、次のように書きます。テキストが「次のページ」となっているリンクを表しています。
//a[.="次のページ"]
ちょっとわかりづらい構文ですが、.は自身(タグの内容)を表しており、.と比較することでテキストを検索しています。

また、前方一致や中間一致には用意されている関数を使用します。
//a[starts-with(., "2016年")]
//a[contains(., "8月")]

かなり複雑な検索も行えます。
次の例は、当ブログ内でカテゴリが「インフラ」になっている記事のタイトルを表しています。
//*[@class="article-category"][.="インフラ"]/ancestor::*[@class="article-header"]
//*[@class="article-title entry-title"]
子から親方向に検索する書き方があります。「ancestor::*」が祖先方向に検索することを表しています。

xpath3


このような書き方で、EXCELのVLOOKUP関数のような使い方ができます。

他にも、一つ上や下の階層(親や子)、同じ階層の次や前(兄弟)方向に検索したり、多彩な関数が用意されているので、必要に応じてリファレンス等で調べてみると良いでしょう。

設定ファイルの書き換え

ここまではHTMLを例にしていましたが、本来はXML用の言語です。XMLはtomcat等の設定ファイルに使われています。設定ファイルの変更は何度も同じことをする場合もありますが、スクリプトを書いておくと後が楽です。
rubyの標準添付ライブラリのXML パーサ「rexml」を使用し、tomcatの設定ファイルserver.xmlを編集し、HTTPのポートを18080に変更する例を紹介します。

require 'rexml/document'

# tomcatのserver.xml
xml = <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8045" shutdown="SHUTDOWN">
  <Listener SSLEngine="on" className="org.apache.catalina.core.AprLifecycleListener"/>
  <Listener className="org.apache.catalina.core.JasperListener"/>
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
  <Listener className="org.apache.catalina.mbeans.ServerLifecycleListener"/>
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>

  <GlobalNamingResources>
    <Resource auth="Container" description="desc" 
       factory="org.apache.catalina.users.MemoryUserDatabaseFactory" name="UserDatabase" 
        pathname="conf/tomcat-users.xml" type="org.apache.catalina.UserDatabase" >
    </Resource>
  </GlobalNamingResources>
  <Service name="Catalina">
    <Connector connectionTimeout="20000" port="8080" 
            protocol="HTTP/1.1" redirectPort="8443" useBodyEncodingForURI="true"/>
    <Connector port="8049" 
            protocol="AJP/1.3" redirectPort="8443" useBodyEncodingForURI="true"/>
    <Engine defaultHost="localhost" name="Catalina">
      <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
      <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true" 
            xmlNamespaceAware="false" xmlValidation="false">
        <Context docBase="sd_retailer" path="/app" reloadable="true" 
           source="org.eclipse.jst.jee.server:hogehoge" />
      </Host>
    </Engine>
  </Service>
</Server>
EOF


document = REXML::Document.new(xml)

# Connectorタグ(HTTP)を取得する
connectorHTTP = document.elements['//Connector[@protocol="HTTP/1.1"]']

# portを変更する
connectorHTTP.attributes['port'] = '18080'

# 出力して確認してみる
puts document.to_s

おわりに

XPathは簡単な割に柔軟な指定のできることを、つかんでいただけたでしょうか?
この記事でご紹介したようにプログラム内で使用するだけでなく、ChromeのデベロッパーツールのElementsタブの検索で使えたりと、利用シーンは意外と多いです。
xpath_devtool
 
XPathはちょっとした作業を簡略化できる、怠惰なプログラマにはもってこいのツールだと思います。