【问题标题】:Parse HTML string into array将 HTML 字符串解析为数组
【发布时间】:2013-04-29 16:37:10
【问题描述】:

我正在为 TinyMCE 生成的 HTML 正文开发类似于 wiki 的差异功能。 diff-lcs 是一个接受数组或对象的差异 gem。大多数差异任务都在代码上,只是比较行。 HTML 文本正文的区别更为复杂。如果我只是插入文本正文,我会得到一个字符一个字符的比较。虽然输出是正确的,但它看起来像垃圾。

seq1 = "<p>Here is a paragraph. A sentence with <strong>bold text</strong>.</p><p>The second paragraph.</p>"

seq2 = seq1.gsub(/[.!?]/, '\0|').split('|')
=> ["<p>Here is a paragraph.", " A sentence with <strong>bold text</strong>.", "</p><p>The second paragraph.", "</p>"]

如果有人更改了第二段,差异输出涉及到前面的段落结束标记。我不能只使用strip_tags,因为我想在比较视图中保持格式。理想的比较是基于完整的句子,将 HTML 分离出来。

seq2.NokogiriMagic
=> ["<p>", "Here is a paragraph.", " A sentence with ", "<strong>", "bold text", "</strong>", ".", "</p>", "<p>", "The second paragraph.", "</p>"]

我发现了很多简洁的 Nokogiri 方法,但没有发现上述方法。

【问题讨论】:

  • Nokogiri 旨在解析 XML/HTML,因此您的起点 seq2(一个字符串数组)并不适合使用 Nokogiri。 TinyMCE 的全部输出是多少?有根元素吗?
  • TinyMCE 的输出与 seq1 类似。 seq2 并不重要,我只想使用像 seq3 这样的格式。看来我必须为孩子解析 Nokogiri 对象,然后执行 seq2 之类的操作。
  • 您可以使用 SAX 接口并将每个标签附加到一个数组中,对于文本节点,可以按单词拆分。
  • 我对 SAX 不熟悉,您能详细说明一下吗?
  • 您的 HTML 文件有多大? DOM 解析,这是使用 Nokogiri 解析 HTML 的实际方式,要容易得多。好处是整个文件都被解析并在内存中,因此您可以重新构建 HTML,或在其中跳转。缺点是全部都在内存中。我很少看到不适合内存的 HTML 文件,否则它也不适合浏览器。 SAX 只允许对标记进行线性处理,但不会将其拉入内存,因此可以处理巨大的文件,通常是 XML。请参阅nokogiri.org/Nokogiri/HTML/SAX.html 了解更多信息。

标签: ruby nokogiri diff-lcs


【解决方案1】:

以下是使用SAX parser 的方法:

require 'nokogiri'

html = "<p>Here is a paragraph. A sentence with <strong>bold text</strong>.</p><p>The second paragraph.</p>"

class ArraySplitParser < Nokogiri::XML::SAX::Document
  attr_reader :array
  def initialize; @array = []; end
  def start_element(name, attrs=[])
    tag = "<" + name
    attrs.each { |k,v| tag += " #{k}=\"#{v}\"" }
    @array << tag + ">"
  end
  def end_element(name); @array << "</#{name}>"; end
  def characters(str); @array += str.gsub(/\s/, '\0|').split('|'); end
end

parser = ArraySplitParser.new
Nokogiri::XML::SAX::Parser.new(parser).parse(html)
puts parser.array.inspect
# ["<p>", "Here ", "is ", "a ", "paragraph. ", "A ", "sentence ", "with ", "<strong>", "bold ", "text", "</strong>", ".", "</p>"]

请注意,您必须将 HTML 包装在根元素中,以便 XML 解析器不会错过示例中的第二段。像这样的东西应该可以工作:

# ...
Nokogiri::XML::SAX::Parser.new(parser).parse('<x>' + html + '</x>')
# ...
puts parser.array[1..-2]
# ["<p>", "Here ", "is ", "a ", "paragraph. ", "A ", "sentence ", "with ", "<strong>", "bold ", "text", "</strong>", ".", "</p>", "<p>", "The ", "second ", "paragraph.", "</p>"]

[编辑]已更新以演示如何在“start_element”方法中保留元素属性。

【讨论】:

  • 有什么想法可以添加到ArraySplitParser 以保留标签属性吗?
  • @Archonic 提示:查看start_element 方法的参数。解析器传入一个attrs 变量。
  • 我已经更新了答案。它正在解析 attrs,但没有将它们写入 @array。
  • 我回来了。知道如何处理其中的图像吗?
【解决方案2】:

您不是用惯用的 Ruby 编写代码。我们不会在变量名中使用混合大小写,同样,在一般编程中,为了清楚起见,使用助记变量名是一个好主意。重构您的代码,使其更像我的编写方式:

tags = %w[p ol ul li h6 h5 h4 h3 h2 h1 em strong i b table thead tbody th tr td]
# Deconstruct HTML body 1
doc = Nokogiri::HTML.fragment(@versionOne.body)
nodes = doc.css(tags.join(', '))

# Reconstruct HTML body 1 into comparable array
output = []
nodes.each do |node|

  output << [
    "<#{ node.name }",
    node.attributes.map { |param| '%s="%s"' % [param.name, param.value] }.join(' '),
    '>'
  ].join

  output << node.children.to_s.gsub(/[\s.!?]/, '|\0|').split('|').flatten

  output << "</#{ node.name }>"

end

# Same deal for nokoOutput2

sdiff = Diff::LCS.sdiff(nokoOutput2.flatten, output.flatten)

行:

tag | " #{ param.name }=\"#{ param.value }\" "

在您的代码中根本不是 Ruby,因为 String 没有 | 运算符。您是否在代码中添加了 | 运算符而不显示该定义?

我看到的一个问题是:

output << node.children.to_s.gsub(/[\s.!?]/, '|\0|').split('|').flatten

您要查找的许多标签可以包含列表中的其他标签:

<html>
  <body>
    <table><tr><td>
      <table><tr><td>
        foo
      </td></tr></table>
    </td></tr></table>
  </body>
</html>

创建一个递归方法来处理:

node.attributes.map { |param| '%s="%s"' % [param.name, param.value] }.join(' '),

可能会改善您的输出。这是未经测试的,但这是一般的想法:

def dump_node(node)

  output = [
    "<#{ node.name }",
    node.attributes.map { |param| '%s="%s"' % [param.name, param.value] }.join(' '),
    '>'
  ].join

  output += node.children.map{ |n| dump_node(n) }

  output << "</#{ node.name }>"

end

【讨论】:

  • 谢谢!很高兴知道这一切。我是否认为@maerics 使用 sax 解析器回答绕过了递归转储节点的需要?
  • SAX 就其本质而言,按顺序查看每个标签,这有助于以相同的方式输出节点。因此,为此目的,它避免了递归转储节点的需要。请参阅我对有关 SAX 与 DOM 的优缺点的原始问题的评论。
猜你喜欢
  • 2017-02-02
  • 1970-01-01
  • 2012-10-14
  • 2017-04-04
  • 2014-03-04
  • 2019-12-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多