【问题标题】:How to navigate the DOM using Nokogiri如何使用 Nokogiri 导航 DOM
【发布时间】:2010-10-14 00:51:35
【问题描述】:

我正在尝试填充变量parent_element_h1parent_element_h2。谁能帮我使用Nokogiri 将我需要的信息放入这些变量中?

require 'rubygems'
require 'nokogiri'

value = Nokogiri::HTML.parse(<<-HTML_END)
  "<html>
    <body>
      <p id='para-1'>A</p>
      <div class='block' id='X1'>
        <h1>Foo</h1>
        <p id='para-2'>B</p>
      </div>
      <p id='para-3'>C</p>
      <h2>Bar</h2>
      <p id='para-4'>D</p>
      <p id='para-5'>E</p>
      <div class='block' id='X2'>
        <p id='para-6'>F</p>
      </div>
    </body>
  </html>"
HTML_END

parent = value.css('body').first

# start_here is given: A Nokogiri::XML::Element of the <div> with the id 'X2
start_here = parent.at('div.block#X2')

# this should be a Nokogiri::XML::Element of the nearest, previous h1.
# in this example it's the one with the value 'Foo'
parent_element_h1 = 

# this should be a Nokogiri::XML::Element of the nearest, previous h2. 
# in this example it's the one with the value 'Bar'
parent_element_h2 =

请注意:start_here 元素可以在文档中的任何位置。 HTML 数据只是一个示例。也就是说,标题 &lt;h1&gt;&lt;h2&gt; 可能是 start_here 的兄弟姐妹或 start_here 兄弟姐妹的孩子。

以下递归方法是一个很好的起点,但它不适用于&lt;h1&gt;,因为它是start_here 的兄弟姐妹的孩子:

def search_element(_block,_style)
  unless _block.nil?
    if _block.name == _style
      return _block
    else
      search_element(_block.previous,_style)
    end
  else
    return false
  end
end

parent_element_h1 = search_element(start_here,'h1')
parent_element_h2 = search_element(start_here,'h2')

在接受答案后,我想出了my own solution。它就像一个魅力,我认为它很酷。

【问题讨论】:

    标签: ruby-on-rails ruby dom xpath nokogiri


    【解决方案1】:

    我会采取的方法(如果我理解您的问题)是使用 XPath 或 CSS 来搜索您的“start_here”元素和您想要在其下搜索的父元素。然后,从父节点开始递归遍历树,当您点击“start_here”元素时停止,并一直保持与您的样式匹配的最后一个元素。

    类似:

    parent = value.search("//body").first
    div = value.search("//div[@id = 'X2']").first
    
    find = FindPriorTo.new(div)
    
    assert_equal('Foo', find.find_from(parent, 'h1').text)
    assert_equal('Bar', find.find_from(parent, 'h2').text) 
    

    FindPriorTo 是一个处理递归的简单类:

    class FindPriorTo
      def initialize(stop_element)
        @stop_element = stop_element
      end
    
      def find_from(parent, style)
        @should_stop = nil
        @last_style  = nil
    
        recursive_search(parent, style)
      end
    
      def recursive_search(parent, style)
        parent.children.each do |ch|
          recursive_search(ch, style)
          return @last_style if @should_stop
    
          @should_stop = (ch == @stop_element)
          @last_style = ch if ch.name == style
        end
    
        @last_style    
      end
    
    end
    

    如果这种方法的可扩展性不够,那么您可以通过重写recursive_search 以不使用递归来优化事物,并传入您正在寻找的两种样式并跟踪最后找到的样式,这样您就不必再遍历树了。

    我还想说尝试猴子修补节点以在文档被解析时挂钩,但看起来所有这些都是用 C 编写的。也许使用具有本机 Ruby 的 Nokogiri 以外的其他东西可能会更好SAX 解析器(可能是 REXML),或者如果您真正关心的是速度,请使用 Xerces 或类似工具在 C/C++ 中执行搜索部分。不过,我不知道这些对解析 HTML 的处理效果如何。

    【讨论】:

    • 问题是,我不知道标题是兄弟姐妹还是兄弟姐妹的孩子。您的解决方案假设我知道它是兄弟姐妹还是兄弟姐妹的孩子。除此之外,我的示例数据比我的真实数据短得多:“my_tag”可以在文档中的任何位置。
    • 当您不确定兄弟/子关系时,您可以在 XPath 中使用 '//' 而不是 '/html/body/' 甚至是 '/html/body//div'。 w3schools.com/Xpath
    • 我认为我的问题不够具体,我已经编辑了问题,希望现在清楚我在寻找什么(检查我试图用数据填充的变量上方的 cmets )。
    • 感谢您的提交。这有点像 hack,但它适用于这个例子。虽然如果 start_here 在另一个 div 块内,它将不起作用。我正在寻找的是一种获取最近的前一个标题的方法,而忽略它在文档中的层次结构。
    • 是的,修剪有点小题大做。看看我编辑的答案是不是你要找的。​​span>
    【解决方案2】:

    我想我发现这个问题已经晚了几年,但我觉得有必要发布,因为所有其他解决方案都太复杂了。

    这是一条带有 XPath 的语句:

    start = doc.at('div.block#X2')
    
    start.at_xpath('(preceding-sibling::h1 | preceding-sibling::*//h1)[last()]')
    #=> <h2>Foo</h2>    
    
    start.at_xpath('(preceding-sibling::h2 | preceding-sibling::*//h2)[last()]')
    #=> <h2>Bar</h2>
    

    这可以容纳直接的先前兄弟姐妹或先前兄弟姐妹的孩子。无论哪个匹配,last() 谓词确保您获得最接近的先前匹配。

    【讨论】:

      【解决方案3】:

      这是我自己的解决方案(感谢我的同事帮助我解决这个问题!)使用递归方法解析所有元素,无论是兄弟姐妹还是另一个兄弟姐妹的孩子。

      require 'rubygems'
      require 'nokogiri'
      
      value = Nokogiri::HTML.parse(<<-HTML_END)
        "<html>
          <body>
            <p id='para-1'>A</p>
            <div class='block' id='X1'>
              <h1>Foo</h1>
              <p id='para-2'>B</p>
            </div>
            <p id='para-3'>C</p>
            <h2>Bar</h2>
            <p id='para-4'>D</p>
            <p id='para-5'>E</p>
            <div class='block' id='X2'>
              <p id='para-6'>F</p>
            </div>
          </body>
        </html>"
      HTML_END
      
      parent = value.css('body').first
      
      # start_here is given: A Nokogiri::XML::Element of the <div> with the id 'X2
      @start_here = parent.at('div.block#X2')
      
      # Search for parent elements of kind "_style" starting from _start_element
      def search_for_parent_element(_start_element, _style)
        unless _start_element.nil?
          # have we already found what we're looking for?
          if _start_element.name == _style
            return _start_element
          end
          # _start_element is a div.block and not the _start_element itself
          if _start_element[:class] == "block" && _start_element[:id] != @start_here[:id]
            # begin recursion with last child inside div.block
            from_child = search_for_parent_element(_start_element.children.last, _style)
            if(from_child)
              return from_child
            end
          end
          # begin recursion with previous element
          from_child = search_for_parent_element(_start_element.previous, _style) 
          return from_child ? from_child : false
        else
          return false
        end
      end
      
      # this should be a Nokogiri::XML::Element of the nearest, previous h1.
      # in this example it's the one with the value 'Foo'
      puts parent_element_h1 = search_for_parent_element(@start_here,"h1")
      
      # this should be a Nokogiri::XML::Element of the nearest, previous h2. 
      # in this example it's the one with the value 'Bar'
      puts parent_element_h2 = search_for_parent_element(@start_here,"h2")
      

      您可以复制/粘贴它并像运行 ruby​​ 脚本一样运行它。

      【讨论】:

        【解决方案4】:

        也许这样就可以了。我不确定性能以及是否有一些我没有想到的情况。

        def find(root, start, tag)
            ps, res = start, nil
            until res or (ps == root)
                ps  = ps.previous || ps.parent
                res = ps.css(tag).last
                res ||= ps.name == tag ? ps : nil
            end
            res || "Not found!"
        end
        
        parent_element_h1 =  find(parent, start_here, 'h1')
        

        【讨论】:

          【解决方案5】:

          您可以使用 CSS 选择器搜索 Nokogiri HTML::Element 的后代。您可以使用.parent 方法遍历祖先。

          parent_element_h1 = value.css("h1").first.parent
          parent_element_h2 = value.css("h2").first.parent
          

          【讨论】:

          • 这不会返回我正在寻找的结果。请再次阅读问题。
          【解决方案6】:

          如果你不知道元素之间的关系,你可以这样搜索它们(在文档的任何地方):

          
          # html code
          text = "insert your html here"
          # get doc object
          doc = Nokogiri::HTML(text)
          # get elements with the specified tag
          elements = doc.search("//your_tag")
          

          但是,如果您需要提交表单,则应该使用 mechanize:

          
          # create mech object
          mech = WWW::Mechanize.new
          # load site
          mech.get("address")
          # select a form, in this case, I select the first form. You can select the one you need 
          # from the array
          form = mech.page.forms.first
          # you fill the fields like this: form.name_of_the_field
          form.element_name  = value
          form.other_element = other_value
          

          【讨论】:

          • 这并不能解决我的问题,但我已将我的问题编辑得更具体。请注意我要填写的两个变量上方的注释。
          • 简而言之:这是行不通的,因为它不仅匹配最近的、以前的 h1 或 h2。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-06-22
          • 2023-03-22
          • 1970-01-01
          • 2017-06-14
          • 1970-01-01
          相关资源
          最近更新 更多