2011-10-19 26 views
2

以前にグループ化されていない兄弟ノードをグループ化して、HTMLまたはXML文書を変換したい。XPathを使用して、HTML/XMLドキュメントから兄弟をグループ化しますか?

例えば、私は次の断片をしたい:この中へ

<h2>Header</h2> 
<p>First paragraph</p> 
<p>Second paragraph</p> 

<h2>Second header</h2> 
<p>Third paragraph</p> 
<p>Fourth paragraph</p> 

<section> 
    <h2>Header</h2> 
    <p>First paragraph</p> 
    <p>Second paragraph</p> 
</section> 

<section> 
    <h2>Second header</h2> 
    <p>Third paragraph</p> 
    <p>Fourth paragraph</p> 
</section> 

は、単純なXPathセレクタと鋸山などのXMLパーサを使用して、このことは可能ですか?あるいは、この作業のためにSAXパーサを実装する必要がありますか?

+0

は(HTTP [他の兄弟まで、すべての次の兄弟を選択]も参照してください。 /stackoverflow.com/questions/2161766/xpath-select-all-following-siblings-until-another-sibling) – Phrogz

答えて

2

更新回答

ここで、ヘッダレベルとその次の兄弟に基づい<section>要素の階層を作成する一般的なソリューションです:ここ

class Nokogiri::XML::Node 
    # Create a hierarchy on a document based on heading levels 
    # wrap : e.g. "<section>" or "<div class='section'>" 
    # stops : array of tag names that stop all sections; use nil for none 
    # levels : array of tag names that control nesting, in order 
    def auto_section(wrap='<section>', stops=%w[hr], levels=%w[h1 h2 h3 h4 h5 h6]) 
    levels = Hash[ levels.zip(0...levels.length) ] 
    stops = stops && Hash[ stops.product([true]) ] 
    stack = [] 
    children.each do |node| 
     unless level = levels[node.name] 
     level = stops && stops[node.name] && -1 
     end 
     stack.pop while (top=stack.last) && top[:level]>=level if level 
     stack.last[:section].add_child(node) if stack.last 
     if level && level >=0 
     section = Nokogiri::XML.fragment(wrap).children[0] 
     node.replace(section); section << node 
     stack << { :section=>section, :level=>level } 
     end 
    end 
    end 
end 

使用中、このコードは、それが与える結果。

オリジナルHTML

<body> 
<h1>Main Section 1</h1> 
<p>Intro</p> 
<h2>Subhead 1.1</h2> 
<p>Meat</p><p>MOAR MEAT</p> 
<h2>Subhead 1.2</h2> 
<p>Meat</p> 
<h3>Caveats</h3> 
<p>FYI</p> 
<h4>ProTip</h4> 
<p>Get it done</p> 
<h2>Subhead 1.3</h2> 
<p>Meat</p> 

<h1>Main Section 2</h1> 
<h3>Jumpin' in it!</h3> 
<p>Level skip!</p> 
<h2>Subhead 2.1</h2> 
<p>Back up...</p> 
<h4>Dive! Dive!</h4> 
<p>...and down</p> 

<hr /><p id="footer">Copyright &copy; All Done</p> 
</body> 

変換コード

# Use XML only so that we can pretty-print the results; HTML works fine, too 
doc = Nokogiri::XML(html,&:noblanks) # stripping whitespace allows indentation 
doc.at('body').auto_section   # make the magic happen 
puts doc.to_xhtml     # show the result with indentation 

結果

<body> 
    <section> 
    <h1>Main Section 1</h1> 
    <p>Intro</p> 
    <section> 
     <h2>Subhead 1.1</h2> 
     <p>Meat</p> 
     <p>MOAR MEAT</p> 
    </section> 
    <section> 
     <h2>Subhead 1.2</h2> 
     <p>Meat</p> 
     <section> 
     <h3>Caveats</h3> 
     <p>FYI</p> 
     <section> 
      <h4>ProTip</h4> 
      <p>Get it done</p> 
     </section> 
     </section> 
    </section> 
    <section> 
     <h2>Subhead 1.3</h2> 
     <p>Meat</p> 
    </section> 
    </section> 
    <section> 
    <h1>Main Section 2</h1> 
    <section> 
     <h3>Jumpin' in it!</h3> 
     <p>Level skip!</p> 
    </section> 
    <section> 
     <h2>Subhead 2.1</h2> 
     <p>Back up...</p> 
     <section> 
     <h4>Dive! Dive!</h4> 
     <p>...and down</p> 
     </section> 
    </section> 
    </section> 
    <hr /> 
    <p id="footer">Copyright All Done</p> 
</body> 

オリジナルの回答

ここではXPathを使用しない回答ですが、Nokogiriです。私は解決策をいくらか柔軟にし、任意の開始/停止(ネストされたセクションではない)を処理する自由を取った。 XPathのために

html = "<h2>Header</h2> 
<p>First paragraph</p> 
<p>Second paragraph</p> 

<h2>Second header</h2> 
<p>Third paragraph</p> 
<p>Fourth paragraph</p> 

<hr> 
<p id='footer'>All done!</p>" 

require 'nokogiri' 
class Nokogiri::XML::Node 
    # Provide a block that returns: 
    # true - for nodes that should start a new section 
    # false - for nodes that should not start a new section 
    # :stop - for nodes that should stop any current section but not start a new one 
    def group_under(name="section") 
    group = nil 
    element_children.each do |child| 
     case yield(child) 
     when false, nil 
      group << child if group 
     when :stop 
      group = nil 
     else 
      group = document.create_element(name) 
      child.replace(group) 
      group << child 
     end 
    end 
    end 
end 

doc = Nokogiri::HTML(html) 
doc.at('body').group_under do |node| 
    if node.name == 'hr' 
    :stop 
    else 
    %w[h1 h2 h3 h4 h5 h6].include?(node.name) 
    end 
end 

puts doc 
#=> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"> 
#=> <html><body> 
#=> <section><h2>Header</h2> 
#=> <p>First paragraph</p> 
#=> <p>Second paragraph</p></section> 
#=> 
#=> <section><h2>Second header</h2> 
#=> <p>Third paragraph</p> 
#=> <p>Fourth paragraph</p></section> 
#=> 
#=> <hr> 
#=> <p id="footer">All done!</p> 
#=> </body></html> 

、XPathを使用してXPath : select all following siblings until another sibling

+0

これは完璧です。ありがとう! –

+0

@DavidJacobs喜んで助けてください。おそらくあなたはいいですが、翌日には入れ子にする[Docubot](http://github.com/Phrogz/docubot)用のアルゴリズムを移植することを望みます。私はこの答えを編集し、その場合に新しいコメントを追加します。 – Phrogz

+0

見出しレベルに基づいてセクション化されていないHTMLを 'section'タグにラップするための一般的なコードです。 – Phrogz

2

一つの方法は、あなたのH2をたどると、そこからまた次のH2に従うp要素を引くすべてのp要素を選択することです参照してください。

doc = Nokogiri::HTML.fragment(html) 
doc.css('h2').each do |h2| 
    nodeset = h2.xpath('./following-sibling::p') 
    next_h2 = h2.at('./following-sibling::h2') 
    nodeset -= next_h2.xpath('./following-sibling::p') if next_h2 
    section_tag = h2.add_previous_sibling Nokogiri::XML::Node.new('section',doc) 
    h2.parent = section_tag 
    nodeset.each {|n| n.parent = section_tag} 
end 
1

XPathは入力ドキュメントからのみ選択できますが、新しいドキュメントに変換することはできません。そのためには、XSLTやその他の変換言語が必要です。私はあなたが鋸山にしている場合は、前の答えは有用であろうと思いますが、完全を期すために、ここではXSLT 2.0で次のようになります。/:

<xsl:for-each-group select="*" group-starting-with="h2"> 
    <section> 
    <xsl:copy-of select="current-group()"/> 
    </section> 
</xsl:for-each-group> 
関連する問題