如何找到给定确切 HTML 标记作为字符串的节点(使用 Nokogiri)?

How to find a node given the exact HTML tag as a string (using Nokogiri)?

问题

当给出确切的 HTML 作为字符串时,我需要在给定网页中搜索特定节点。例如,如果给出:

url = "https://www.wikipedia.org/"
node_to_find = "<title>Wikipedia</title>"

我想 "select" 页面上的节点(最终 return 它的 children 和兄弟节点)。我在使用 Nokogiri 文档时遇到问题,以及如何解决这个问题。似乎大多数时候,人们都想使用 Xpath 语法或 #css 方法来查找满足一组条件的节点。我想使用 HTML 语法并在网页中找到完全匹配的内容。


可能开始解决方案?

如果我创建两个 Nokogiri::HTML::DocumentFragment object,它们看起来很相似但由于内存 ID 不同而不匹配。我认为这可能是解决它的前兆?

irb(main):018:0> n = Nokogiri::HTML::DocumentFragment.parse(<title>Wikipedia</title>").child

=> #<Nokogiri::XML::Element:0x47e7e4 name="title" children=[ <Nokogiri::XML::Text:0x47e08c "Wikipedia">]>

irb(main):019:0> n.class

=> Nokogiri::XML::Element

然后我使用完全相同的参数创建第二个。比较它们 - return 错误:

irb(main):020:0> x = Nokogiri::HTML::DocumentFragment.parse("<title>Wikipedia</title>").child 

=> #<Nokogiri::XML::Element:0x472958 name="title" children=[#<Nokogiri::XML::Text:0x4724a8 "Wikipedia">]>

irb(main):021:0> n == x

=> false

所以我在想,如果我能以某种方式创建一个可以找到这样的匹配项的方法,那么我就可以执行该节点的操作。特别是 - 我想找到后代(children 和下一个兄弟姐妹)。

编辑:我应该提到我的代码中有一个方法可以从给定的 URL 创建 Nokogiri::HTML::Document object。所以 - 可以用来比较。

class Page
attr_accessor :url, :node, :doc, :root

def initialize(params = {})
  @url = params.fetch(:url, "").to_s
  @node = params.fetch(:node, "").to_s
  @doc = parse_html(@url)
end

def parse_html(url)
  Nokogiri::HTML(open(url).read)  
end

结束

正如评论者@August 所建议的,您可以使用 Node#traverse 查看任何节点的字符串表示是否与目标节点的字符串形式相匹配。

def find_node(html_document, html_fragment)
  matching_node = nil
  html_document.traverse do |node|
    matching_node = node if node.to_s == html_fragment.to_s
  end
  matching_node
end

当然,这种方法充满了问题,归结为数据的规范表示(您关心属性排序吗?引号等特定语法项?空格?)。

[编辑] 下面是将任意 HTML 元素转换为 XPath 表达式的原型。它需要一些工作,但基本思想(将任何元素与节点名称、特定属性和可能的​​文本子项匹配)应该是一个很好的起点。

def html_to_xpath(html_string)
  node = Nokogiri::HTML::fragment(html_string).children.first
  has_more_than_one_child = (node.children.size > 1)
  has_non_text_child = node.children.any? { |x| x.type != Nokogiri::XML::Node::TEXT_NODE }
  if has_more_than_one_child || has_non_text_child
    raise ArgumentError.new('element may only have a single text child')
  end
  xpath = "//#{node.name}"
  node.attributes.each do |_, attr|
    xpath += "[#{attr.name}='#{attr.value}']" # TODO: escaping.
  end
  xpath += "[text()='#{node.children.first.to_s}']" unless node.children.empty?
  xpath
end
html_to_xpath('<title>Wikipedia</title>') # => "//title[text()='Wikipedia']"
html_to_xpath('<div id="foo">Foo</div>')  # => "//div[id='foo'][text()='Foo']"
html_to_xpath('<div><br/></div>') # => ArgumentError: element may only have a single text child

您似乎可以从 any HTML 片段构建一个 XPath(例如,根据我上面的原型,不限于那些只有一个文本子项的片段)但我会把它留作 reader 的练习 ;-)