Nokogiriのdupメソッドでは、親兄弟は複製できないという話

NokogiriのNodeクラスに定義されているdupメソッドは、ノード自身とその子孫のみ複製します
すなわち、Nokogiriのdupメソッドでは、親要素・兄弟要素は複製出来ないのです。

いやでも、親兄弟も欲しいんだけど・・・と、僕は思ったので、実際に親兄弟も複製した方法を紹介します。

忙しい人向け

親要素や兄弟要素も複製したければDocumentクラスから複製する。

require 'nokogiri'

html = <<'EOS'
<html>
  <body>
    <h1>My First Heading</h1>
    <div>
      <p>My first paragraph.</p>
    </div>
  </body>
</html>
EOS

def complete_nokogiri_dup(node)
  doc_dup = node.document.dup
  doc_dup.at_css(node.css_path)
end

doc = Nokogiri::HTML.parse(html)
div_node = doc.at_css('div')
p complete_nokogiri_dup(div_node).parent # => <Nokogiri::XML::Element:0x3fde220e43f0 name="body" children=[#, #

Nokogiriのdupメソッドの各要素に対する挙動

・子要素

p div_node.at_css('p') # => <Nokogiri::XML::Element:0x3fc163838934 name="p" children=[#]>
p div_node.dup.at_css('p') # => <Nokogiri::XML::Element:0x3fc163838934 name="p" children=[#]>

子要素はちゃんと複製できているみたいですね。 それでは、兄要素はどうでしょうか。

・兄要素

p div_node.previous_element  # => <Nokogiri::XML::Element:0x3fcc048287cc name="h1" children=[#]>
p div_node.dup.previous_sibling # => nil

previous_siblingは兄要素、つまり一つ前の要素であるh1タグを参照するはずのメソッドです。複製したdiv_nodeに実行するとnilが返されますので、兄要素は複製できていないようですね。続いて親要素も見てみましょう。

・親要素

p div_node.parent # => <Nokogiri::XML::Element:0x3fd6891023c8 name="body" children=[#, # nil
p div_node.dup.parent # => nil

兄要素と同様にnilが返されます。親要素も複製できていないみたいです。

完全複製のやり方

def complete_nokogiri_dup(node)
  doc_dup = node.document.dup
  doc.at_css(node.css_path)
end

doc = Nokogiri::HTML.parse(html)
div_node = doc.at_css('div')
p complete_nokogiri_dup(div_node).parent # => <Nokogiri::XML::Element:0x3fde220e43f0 name="body"

dupメソッドで子要素は複製出来るという点を活かして、全てのノードの最上位にあるDocumentクラスから複製します。この複製したDocumentに対して、複製元ノードと同一のCSSセレクタを当てることで、親子兄弟関係を保ったままのノードの複製を作成しています。

終わりに

最後まで目を通していただきありがとうございます。弊社は、Webクローリングやスクレイピングのナレッジが沢山溜まっています。rubyを主としてスクレイピングは開発しているため、特にNokogiriのこういった小ネタは沢山あります。気になる方は、他の方のブログも覗いてみてください。