コンテンツ内のマークアップを保持したまま、Nokogiri ノード内のテキストの一部を置き換える
概要
Nokogiri でノードのコンテンツをスキャンし、gsub を実行することで、多数のファイルにわたる一意の文字列のインスタンスを置き換えようとしています。文字列の一部をそのままにして、アンカー タグに変換します。ただし、大部分のノードの内容にはさまざまな形式のマークアップが含まれており、単なる単純な文字列ではありません。たとえば、次のようなファイルがあるとします。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<html>
<head>
<title>Title</title>
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div>
<p class="header"><<2>>Header</p>
<p class="paragraph">
<p class="text_style">Lorem ipsum blah blah blah. <<3>> Here is more content. <span class="style">Preserve this.</span> Blah blah extra text.</p>
</div>
</body>
</html>
文書全体に数字があり、<< と >> で囲まれています。数値の値を取得して、 のようなタグに変換したいのですが、同じセクション内の他の要素の HTML マークアップ、つまり <span class を保持したいと考えています。 =“style”>これを保存します。。
私が試したことはすべて次のとおりです。
file = File.open("file.xhtml") {|f| Nokogiri::XML(f)}
file.xpath("//text()").each { |node|
if node.text.match(/<<([^_]*)>>/)
new_content = node.text.gsub(/<<([^_]*)>>/,"<a id=\"\\1\"/>")
node.parent.inner_html = new_content
end
}
gsub は正しく動作しますが、.text メソッドを使用しているため、マークアップは無視され、事実上消去されます。この場合、Preserve this. 部分は完全に削除されます。 (参考までに、私が .parent メソッドを使用しているのは、単に node.inner_html = new_content を実行すると、次のエラーが発生するためです: add_child_node’: Nokogiri::XML::Element そこに再親化できません (ArgumentError)。)
代わりにこれを行うと:
new_content = node.text.gsub(/<<([^_]*)>>/,"<a id=\"\\1\"/>")
node.content = new_content
文字が適切にエスケープされていません。ファイルは ではなく で終わります。
代わりに次のように CSS メソッドを使用してみました。
file.xpath("*").each { |node|
if node.inner_html.match(/<<([^_]*)>>/)
new_content = node.inner_html.gsub(/<<([^_]*)>>/,"<a id=\"\\1\"/>")
node.inner_html = new_content
end
}
gsub
は機能し、マークアップは保持され、置換されたタグは適切にエスケープされます。ただし、
タグと
タグが削除されるため、無効なファイルになります。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Title</title>
<link href="style.css" rel="stylesheet" type="text/css"/>
<div>
<p class="header"><a id="2"/>Header</p>
<p class="paragraph">
</p><p class="text_style">Lorem ipsum blah blah blah. <a id="3"/> Here is more content. <span class="style">Preserve this.</span> Blah blah extra text. </p>
</div>
</html>
すべてのノード (file.css(“*“)) を反復しているという事実と関係があるのではないかと思いますが、子ノードに加えて親ノードもスキャンされるため、これも冗長です。
Web を調べましたが、これに対する解決策が見つかりません。マークアップを維持し、正しくエンコードしながら、一意のテキストを交換できるようにしたいだけです。ここで何か明らかに欠けているものはありますか?
解決策
これはかなりうまく機能するようです:
require 'nokogiri'
doc = Nokogiri::XML(<<EOT)
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<html>
<head>
<title>Title</title>
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div>
<p class="header"><<2>>Header</p>
<p class="paragraph">
<p class="text_style">Lorem ipsum. <<3>> more content. <span class="style">Preserve this.</span> extra text.</p>
</div>
</body>
</html>
EOT
doc.search("//text()[contains(.,'<<')]").each do |node|
node.replace(node.content.gsub(/<<(\d+)>>/, '<a id="[\1]" />'))
end
その結果、次のような結果が得られます。
puts doc.to_html
# >> <html>
# >> <head>
# >> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
# >> <title>Title</title>
# >> <link href="style.css" rel="stylesheet" type="text/css">
# >> </head>
# >> <body>
# >> <div>
# >> <p class="header"><a id="[2]"></a>Header</p>
# >> <p class="paragraph">
# >> <p class="text_style">Lorem ipsum. <a id="[3]"></a> more content. <span class="style">Preserve this.</span> extra text.</p>
# >> </p>
# >> </div>
# >> </body>
# >> </html>
ノコギリさんが追加しているのは、
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
おそらくマークアップが XML として定義されているためです。
セレクター “//text()[contains(.,‘<<’)]” は、‘<<’ を含むテキスト ノードのみを検索します。誤検知が発生する可能性がある場合は、これを変更してより具体的にすることができます。構文については、「XPath: contains 関数での正規表現の使用」を参照してください。
replace はトリックを実行しています。 Nokogiri::XML::Text ノードを変更して <a…/> を含めようとしましたが、変更できません。< と > はエンコードする必要があります。ノードを Nokogiri::XML::Element に変更すると (Nokogiri の デフォルト値)、希望どおりに保存できるようになります。