如何找到给定确切 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 的练习 ;-)
问题
当给出确切的 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 的练习 ;-)