こんにちは。エンジニアの高橋です。

NokogiriのCSSセレクタでは「あるテキストを含むノード」を検索することができます。

表から新宿の天気だけ取得したい

▲この表から「新宿の天気」のみ取得することを考えます

<table border="1">
  <tr>
    <td rowspan="3">東京</td>
    <td>新宿:雨</td>
  </tr>
  <tr>
    <td>渋谷:大雨</td>
  </tr>
  <tr>
    <td>池袋:豪雨</td>
  </tr>
</table>

▲写真の表のソースコード。tdが複数あるが、classやidが割り振られていないため、「tdでのループ」をロジックに入れる必要がある。・・?

ここではcontainsというcssセレクタの記法を使用して、at_cssメソッドで、”新宿”をテキストに含むtdタグノードのみ取得ができます。以下のコードで取得を試してみました。

require "nokogiri"

html = <<'EOS'
<table border="1">
  <tr>
    <td rowspan="3">東京</td>
    <td>新宿:雨</td>
  </tr>
  <tr>
    <td>渋谷:大雨</td>
  </tr>
  <tr>
    <td>池袋:豪雨</td>
  </tr>
</table>
EOS

doc = Nokogiri::HTML.parse(html)
puts doc.at_css("td:contains('新宿')").text
# >> 新宿:雨

注意

  • contains記法ですが、Nokogiriの公式ドキュメント等で動作する根拠が確認できていません。使用には十分注意してください。
  • Nokogiriは1.10.1で動作確認をしました。

解説

  • xpathには、”テキストでの検索”が標準機能として存在していますが、css3には機能が存在しません。

これを踏まえて、Nokogiri::XML::Searchable#cssの実装を見てみましょう。

Nokogiri::XML::Searchable#css(104行目)

重要なのはcss_internalメソッドです。

css_internal(170行目)

xpath_internalメソッドに、css_rules_to_xpathメソッドの戻り値を渡しています。

これはその名の通り、cssセレクタを同義のxpathに変換し、xpathでドキュメントのパースを行なっているのです。

ちなみに、上記のcssセレクタ”td:contains(‘新宿’)”は、css_rules_to_xpathメソッドにより、下記のように変換されます。

変換前(cssセレクタ) 変換後(xpathセレクタ)
td:contains(‘新宿’) //td[contains(., ‘新宿’)]

xpathのcontainsの第一引数”.”は、「子孫ノードを含めた、テキスト」で検索するという意味合いです。

まとめ

Nokogiriでcssセレクタ”td:contains(‘新宿’)”がなぜ動くかというと、Nokogiriは内部的にcssセレクタをxpathに変換しているため、xpathのcontains記法を間接的に使用しているからということになります。

Nokogiriのcssセレクタにおける”contains”記法ですが、調査した結果、かなりトリッキーな記法だということがわかりました。使えるには使えるが、サポートや互換性は不明なため、積極的な使用は推奨しない、といった位置付けになるのではないでしょうか。