Amazon Linux AMI (2018.03) のサポート期間が2020年12月31日まで延びましたが、目下、過去の遺物を Amazon Linux 2 へ移行しています。
これはそんな中起きた問題で、結論は単純にxpathの演算子の優先順位を勘違いしていたという話です。
自分だけの勘違いではなく、まわりにも何人かいたので、もしかしたら有用かなと思って書きました。
皆さんは、このhtmlの2番目のh3のテキスト "Fuga" をxpathで取りたいときにどう書くでしょうか。
<!DOCTYPE html>
<html>
<div>
<h3>Hoge</h3>
<h3>Fuga</h3>
<h3>Gero</h3>
</div>
<div>
<h3>Foo</h3>
<h3>Bar</h3>
<h3>Baz</h3>
</div>
</html>
私とそのまわりの何人かは何の違和感もなくこう '//h3[2]/text()'
書いていました。
require 'nokogiri'
doc = Nokogiri::HTML(open('test.html'))
p doc.xpath('//h3[2]/text()')
これは Amazon Linuxで yum パッケージの ruby2.0 と nokogiri だとこういう結果になります(これが古すぎるというのは置いといて)。
$ ruby scraping.rb
[#<Nokogiri::XML::Text:0x1160c78 "Fuga">]
そして、Amazon Linux 2 に移行すると、rubyは amazon-linux-extras の ruby2.4、nokogiri は gem で入れることになり、結果はこうなります。
$ ruby scraping.rb
[#<Nokogiri::XML::Text:0x2aea55368304 "Fuga">, #<Nokogiri::XML::Text:0x2aea55368160 "Bar">]
! !
異なる結果になってしまうのでした。これになかなか気づけずはまっていました。
理由
libxml2 の 2.9.2 でこういった修正が入っています。
Fix XPath '//' optimization with predicates (Nick Wellnhofer),
xpath の仕様では、 []
演算子 は //
よりも優先度が高いので、 //h3[2]/text()
と書くと h3[2]
の方に先にバインドされてしまうんですね。上で期待する操作をするときは (//h3)[2]/text()
こう書くのが正でした。
libxml2 のバージョン 2.9.1 までここが間違っていて、Amazon Linux の yum リポジトリの libxml2 が 2.9.1 だったので、これまでうまく動いてしまっていたということです。
ちなみに
- Amazon Linux でも gem で nokogiri をインストールしていれば回避できていました
- Amazon Linux 2 でも yum の libxml2 は 2.9.1 なので、 xmllint なんかは間違ったままです