如何使用 Nokogiri 捕获同级标签之间的连续元素?

How do I use Nokogiri to capture consecutive elements between sibling tags?

提问人:rosswgray 提问时间:5/31/2022 更新时间:6/1/2022 访问量:66

问:

我有类似以下 HTML 的东西,它代表了多项选择题。模式通常是标签(问题),然后是 和 和 四个 s(答案选项)。但是,只是偶尔,一个问题会超过一个标签。<p><ol><li><p>

<ol />
<p class="Question-Stem">Which choice is best?</p>
<ol>
  <li class="Answer-Choice">text</li>
  <li class="Answer-Choice">text</li>
  <li class="Answer-Choice">text</li>
  <li class="Answer-Choice">text</li>
</ol>
<ol />
<p class="Question-Stem">Which choice is best?</p>
<p class="Indented-Sentence">more text</p>
<p class="Question-Stem">more text</p>
<ol>
  <li class="Answer-Choice">text</li>
  <li class="Answer-Choice">text</li>
  <li class="Answer-Choice">text</li>
  <li class="Answer-Choice">text</li>
</ol>
<ol />
<p class="Question-Stem">null</p>
<ol>
  <li class="Answer-Choice">text</li>
  <li class="Answer-Choice">text</li>
  <li class="Answer-Choice">text</li>
  <li class="Answer-Choice">text</li>
</ol>
    

我正在将问题解析为哈希数组,每个问题一个哈希值

questions_array = []

questions_doc.css('p.Question-Stem').each do |p|
  # skip this iteration if the previous tag is not an ol tag 
  # (i.e. it's not the beginning of the stem)
  next if p.previous_element.name != "ol"
  question = { :stem => "", :answer_choices => [] }
  p.inner_html == "null" ? question[:stem] = "" : question[:stem] = p.inner_html
  p.next_element.element_children.each do |child|
    question[:answer_choices] << child.inner_html
  end
  questions_array << question
end

这完全按照我的意愿进行解析,除了问题词干连续三个标签的少数情况。在这些情况下,我希望将所有三个标签的 html 一起推入 .有什么想法如何实现吗? 我已经阅读了如何使用 Nokogiri 解析连续标签? 但没有找到适用于这种情况的解决方案。pquestion[:stem]

ruby html 解析 nokogiri

评论


答:

1赞 Konstantin Strukov 5/31/2022 #1

当文档具有常规结构时,仅使用声明性选择器(css、xpath)处理文档是很好的。

如果没有,并且您必须引入显式控制流(循环、条件等),那么您可能需要执行命令式并逐节点显式遍历文档树。

在这种情况下,它可能非常简单,如下所示:

  1. 找到第一个问题节点,
  2. 检查下一个同级
  • 如果它是另一个节点 - 将其内容添加到当前正在解析的问题中,p
  • 否则,如果它是节点,则完成当前问题并为其提取答案,ol
  • 否则,如果它是其他内容,请跳过它(例如 nokogiri 识别的换行符的空文本节点)
  1. 重复 2,而下一个同级存在

该算法的幼稚(且相当脆弱和肮脏)的实现:

def extract_questions(doc)
  questions = []
  current_node = doc.xpath("//p[contains(@class, 'Question-Stem')]").first
  current_question = [current_node&.text].compact

  loop do
    current_node = current_node&.next_sibling
    break if current_node.nil?

    case current_node.name
    when "p"
      current_question << current_node.text
    when "ol"
      answers = current_node.children.select { |c| c.name == "li" }.map(&:text)
      questions << { stem: current_question.join("\n"), answer_choices: answers}
      current_question = []
    end  
  end

  questions
end

现在

html = <<~HTML
  <p class="Question-Stem">Which choice is best?</p>
  <ol>
    <li class="Answer-Choice">text</li>
    <li class="Answer-Choice">text</li>
    <li class="Answer-Choice">text</li>
    <li class="Answer-Choice">text</li>
  </ol>
  <p class="Question-Stem">Which choice is best?</p>
  <p class="Indented-Sentence">more text</p>
  <p class="Question-Stem">more text</p>
  <ol>
    <li class="Answer-Choice">text</li>
    <li class="Answer-Choice">text</li>
    <li class="Answer-Choice">text</li>
    <li class="Answer-Choice">text</li>
  </ol>
  <p class="Question-Stem">null</p>
  <ol>
    <li class="Answer-Choice">text</li>
    <li class="Answer-Choice">text</li>
    <li class="Answer-Choice">text</li>
    <li class="Answer-Choice">text</li>
  </ol>
HTML

doc = Nokogiri::HTML(html)

extract_questions(doc) # => [{:stem=>"Which choice is best?", :answer_choices=>["text", "text", "text", "text"]},{:stem=>"Which choice is best?\nmore text\nmore text", :answer_choices=>["text", "text", "text", "text"]},{:stem=>"null", :answer_choices=>["text", "text", "text", "text"]}]

评论

0赞 rosswgray 6/1/2022
是的!这很好用。除了有一堆自闭合标签会搞砸循环,所以我需要在里面添加<ol />case "ol"next if current_node.children.empty?