HTML文字列をプレーンテキストに変換する必要があります(HTML Agility Packを使用することが望ましい)。適切な空白、特に適切な改行を使用します。
そして、「適切な改行」とは、次のコードを意味します。
<div>
<div>
<div>
line1
</div>
</div>
</div>
<div>line2</div>
として変換する必要があります
line1
line2
つまり1つだけ改行。
私が見た解決策のほとんどは、すべての<div> <br> <p>
タグから\n
明らかに、s * cks。
C#のhtmlからプレーンテキストへのレンダリングlogicの提案はありますか?完全なコードではなく、「一般的なロジックは、「閉じているすべてのDIVを改行で置き換えますが、次の兄弟がDIVでもない場合のみ」などの回答が本当に役立ちます。
試したこと:単に.InnerText
プロパティ(明らかに間違っている)、正規表現(遅い、痛みを伴う、たくさんのハック、また正規表現はHtmlAgilityPackの12倍遅い-私はそれを測定した)、これ solution および同様(より多くの改行を返す必須)
以下のコードは、提供された例で正しく機能します。<div><br></div>
、まだ改善すべきことがいくつかありますが、基本的な考え方はそこにあります。コメントを参照してください。
public static string FormatLineBreaks(string html)
{
//first - remove all the existing '\n' from HTML
//they mean nothing in HTML, but break our logic
html = html.Replace("\r", "").Replace("\n", " ");
//now create an Html Agile Doc object
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(html);
//remove comments, head, style and script tags
foreach (HtmlNode node in doc.DocumentNode.SafeSelectNodes("//comment() | //script | //style | //head"))
{
node.ParentNode.RemoveChild(node);
}
//now remove all "meaningless" inline elements like "span"
foreach (HtmlNode node in doc.DocumentNode.SafeSelectNodes("//span | //label")) //add "b", "i" if required
{
node.ParentNode.ReplaceChild(HtmlNode.CreateNode(node.InnerHtml), node);
}
//block-elements - convert to line-breaks
foreach (HtmlNode node in doc.DocumentNode.SafeSelectNodes("//p | //div")) //you could add more tags here
{
//we add a "\n" ONLY if the node contains some plain text as "direct" child
//meaning - text is not nested inside children, but only one-level deep
//use XPath to find direct "text" in element
var txtNode = node.SelectSingleNode("text()");
//no "direct" text - NOT ADDDING the \n !!!!
if (txtNode == null || txtNode.InnerHtml.Trim() == "") continue;
//"surround" the node with line breaks
node.ParentNode.InsertBefore(doc.CreateTextNode("\r\n"), node);
node.ParentNode.InsertAfter(doc.CreateTextNode("\r\n"), node);
}
//todo: might need to replace multiple "\n\n" into one here, I'm still testing...
//now BR tags - simply replace with "\n" and forget
foreach (HtmlNode node in doc.DocumentNode.SafeSelectNodes("//br"))
node.ParentNode.ReplaceChild(doc.CreateTextNode("\r\n"), node);
//finally - return the text which will have our inserted line-breaks in it
return doc.DocumentNode.InnerText.Trim();
//todo - you should probably add "&code;" processing, to decode all the and such
}
//here's the extension method I use
private static HtmlNodeCollection SafeSelectNodes(this HtmlNode node, string selector)
{
return (node.SelectNodes(selector) ?? new HtmlNodeCollection(node));
}
懸念事項:
代数的決定:
plain-text = Process(Plain(html))
Plain(node-s) => Plain(node-0), Plain(node-1), ..., Plain(node-N)
Plain(BR) => BR
Plain(not-visible-element(child-s)) => nil
Plain(block-element(child-s)) => BS, Plain(child-s), BE
Plain(inline-element(child-s)) => Plain(child-s)
Plain(text) => ch-0, ch-1, .., ch-N
Process(symbol-s) => Process(start-line, symbol-s)
Process(start-line, BR, symbol-s) => Print('\n'), Process(start-line, symbol-s)
Process(start-line, BS, symbol-s) => Process(start-line, symbol-s)
Process(start-line, BE, symbol-s) => Process(start-line, symbol-s)
Process(start-line, hard-space, symbol-s) => Print(' '), Process(not-ws, symbol-s)
Process(start-line, space, symbol-s) => Process(start-line, symbol-s)
Process(start-line, common-symbol, symbol-s) => Print(common-symbol),
Process(not-ws, symbol-s)
Process(not-ws, BR|BS|BE, symbol-s) => Print('\n'), Process(start-line, symbol-s)
Process(not-ws, hard-space, symbol-s) => Print(' '), Process(not-ws, symbol-s)
Process(not-ws, space, symbol-s) => Process(ws, symbol-s)
Process(not-ws, common-symbol, symbol-s) => Process(ws, symbol-s)
Process(ws, BR|BS|BE, symbol-s) => Print('\n'), Process(start-line, symbol-s)
Process(ws, hard-space, symbol-s) => Print(' '), Print(' '),
Process(not-ws, symbol-s)
Process(ws, space, symbol-s) => Process(ws, symbol-s)
Process(ws, common-symbol, symbol-s) => Print(' '), Print(common-symbol),
Process(not-ws, symbol-s)
HtmlAgilityPackおよびSystem.Xml.LinqのC#決定:
//HtmlAgilityPack part
public static string ToPlainText(this HtmlAgilityPack.HtmlDocument doc)
{
var builder = new System.Text.StringBuilder();
var state = ToPlainTextState.StartLine;
Plain(builder, ref state, new[]{doc.DocumentNode});
return builder.ToString();
}
static void Plain(StringBuilder builder, ref ToPlainTextState state, IEnumerable<HtmlAgilityPack.HtmlNode> nodes)
{
foreach (var node in nodes)
{
if (node is HtmlAgilityPack.HtmlTextNode)
{
var text = (HtmlAgilityPack.HtmlTextNode)node;
Process(builder, ref state, HtmlAgilityPack.HtmlEntity.DeEntitize(text.Text).ToCharArray());
}
else
{
var tag = node.Name.ToLower();
if (tag == "br")
{
builder.AppendLine();
state = ToPlainTextState.StartLine;
}
else if (NonVisibleTags.Contains(tag))
{
}
else if (InlineTags.Contains(tag))
{
Plain(builder, ref state, node.ChildNodes);
}
else
{
if (state != ToPlainTextState.StartLine)
{
builder.AppendLine();
state = ToPlainTextState.StartLine;
}
Plain(builder, ref state, node.ChildNodes);
if (state != ToPlainTextState.StartLine)
{
builder.AppendLine();
state = ToPlainTextState.StartLine;
}
}
}
}
}
//System.Xml.Linq part
public static string ToPlainText(this IEnumerable<XNode> nodes)
{
var builder = new System.Text.StringBuilder();
var state = ToPlainTextState.StartLine;
Plain(builder, ref state, nodes);
return builder.ToString();
}
static void Plain(StringBuilder builder, ref ToPlainTextState state, IEnumerable<XNode> nodes)
{
foreach (var node in nodes)
{
if (node is XElement)
{
var element = (XElement)node;
var tag = element.Name.LocalName.ToLower();
if (tag == "br")
{
builder.AppendLine();
state = ToPlainTextState.StartLine;
}
else if (NonVisibleTags.Contains(tag))
{
}
else if (InlineTags.Contains(tag))
{
Plain(builder, ref state, element.Nodes());
}
else
{
if (state != ToPlainTextState.StartLine)
{
builder.AppendLine();
state = ToPlainTextState.StartLine;
}
Plain(builder, ref state, element.Nodes());
if (state != ToPlainTextState.StartLine)
{
builder.AppendLine();
state = ToPlainTextState.StartLine;
}
}
}
else if (node is XText)
{
var text = (XText)node;
Process(builder, ref state, text.Value.ToCharArray());
}
}
}
//common part
public static void Process(System.Text.StringBuilder builder, ref ToPlainTextState state, params char[] chars)
{
foreach (var ch in chars)
{
if (char.IsWhiteSpace(ch))
{
if (IsHardSpace(ch))
{
if (state == ToPlainTextState.WhiteSpace)
builder.Append(' ');
builder.Append(' ');
state = ToPlainTextState.NotWhiteSpace;
}
else
{
if (state == ToPlainTextState.NotWhiteSpace)
state = ToPlainTextState.WhiteSpace;
}
}
else
{
if (state == ToPlainTextState.WhiteSpace)
builder.Append(' ');
builder.Append(ch);
state = ToPlainTextState.NotWhiteSpace;
}
}
}
static bool IsHardSpace(char ch)
{
return ch == 0xA0 || ch == 0x2007 || ch == 0x202F;
}
private static readonly HashSet<string> InlineTags = new HashSet<string>
{
//from https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elemente
"b", "big", "i", "small", "tt", "abbr", "acronym",
"cite", "code", "dfn", "em", "kbd", "strong", "samp",
"var", "a", "bdo", "br", "img", "map", "object", "q",
"script", "span", "sub", "sup", "button", "input", "label",
"select", "textarea"
};
private static readonly HashSet<string> NonVisibleTags = new HashSet<string>
{
"script", "style"
};
public enum ToPlainTextState
{
StartLine = 0,
NotWhiteSpace,
WhiteSpace,
}
}
例:
// <div> 1 </div> 2 <div> 3 </div>
1
2
3
// <div>1 <br/><br/>  <b> 2 </b> <div> </div><div> </div>  3</div>
1
2
3
// <span>1<style> text </style><i>2</i></span>3
123
//<div>
// <div>
// <div>
// line1
// </div>
// </div>
//</div>
//<div>line2</div>
line1
line2
以下のクラスは、innerText
の代替実装を提供します。異なるテキストコンテンツを区別するタグのみを考慮するため、後続のdivに対して複数の改行を送信しません。すべてのテキストノードの親が評価され、改行またはスペースを挿入するかどうかが決定されます。したがって、直接テキストを含まないタグは自動的に無視されます。
提示したケースは、希望どおりの結果をもたらしました。さらに:
<div>ABC<br>DEF<span>GHI</span></div>
与える
ABC
DEF GHI
ながら
<div>ABC<br>DEF<div>GHI</div></div>
与える
ABC
DEF
GHI
div
はブロックタグであるためです。 script
およびstyle
要素は完全に無視されます。 HttpUtility.HtmlDecode
ユーティリティメソッド(System.Web
内)は、&
などのHTMLエスケープテキストをデコードするために使用されます。空白(\s+
)の複数の出現は、単一のスペースに置き換えられます。 br
タグは、繰り返されても複数の改行を引き起こしません。
static class HtmlTextProvider
{
private static readonly HashSet<string> InlineElementNames = new HashSet<string>
{
//from https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elemente
"b", "big", "i", "small", "tt", "abbr", "acronym",
"cite", "code", "dfn", "em", "kbd", "strong", "samp",
"var", "a", "bdo", "br", "img", "map", "object", "q",
"script", "span", "sub", "sup", "button", "input", "label",
"select", "textarea"
};
private static readonly Regex WhitespaceNormalizer = new Regex(@"(\s+)", RegexOptions.Compiled);
private static readonly HashSet<string> ExcludedElementNames = new HashSet<string>
{
"script", "style"
};
public static string GetFormattedInnerText(this HtmlDocument document)
{
var textBuilder = new StringBuilder();
var root = document.DocumentNode;
foreach (var node in root.Descendants())
{
if (node is HtmlTextNode && !ExcludedElementNames.Contains(node.ParentNode.Name))
{
var text = HttpUtility.HtmlDecode(node.InnerText);
text = WhitespaceNormalizer.Replace(text, " ").Trim();
if(string.IsNullOrWhiteSpace(text)) continue;
var whitespace = InlineElementNames.Contains(node.ParentNode.Name) ? " " : Environment.NewLine;
//only
if (EndsWith(textBuilder, " ") && whitespace == Environment.NewLine)
{
textBuilder.Remove(textBuilder.Length - 1, 1);
textBuilder.AppendLine();
}
textBuilder.Append(text);
textBuilder.Append(whitespace);
if (!char.IsWhiteSpace(textBuilder[textBuilder.Length - 1]))
{
if (InlineElementNames.Contains(node.ParentNode.Name))
{
textBuilder.Append(' ');
}
else
{
textBuilder.AppendLine();
}
}
}
else if (node.Name == "br" && EndsWith(textBuilder, Environment.NewLine))
{
textBuilder.AppendLine();
}
}
return textBuilder.ToString().TrimEnd(Environment.NewLine.ToCharArray());
}
private static bool EndsWith(StringBuilder builder, string value)
{
return builder.Length > value.Length && builder.ToString(builder.Length - value.Length, value.Length) == value;
}
}
SOは完全なコードソリューションを作成するための報奨金を交換することに関するものではありません。最良の答えは、ガイダンスを提供し、それを自分で解決するのを助けるものです。私には動作するはずです:
</div>
のすべてのインスタンスを改行で置き換えます</p>
、<br>
、および<br/>
のインスタンスを改行で置き換えます™
必要に応じて基本的には、各段落または改行タブごとに1つの改行が必要ですが、複数のdivクロージャーを1つのもので折りたたむため、最初にそれらを行います。
最後に、実際にHTMLレイアウトを実行していることに注意してください。これはタグのCSSに依存します。表示される動作は、divがデフォルトでブロック表示/レイアウトモードになっているために発生します。 CSSはそれを変えるでしょう。ヘッドレスレイアウト/レンダリングエンジン、つまりCSSを処理できるものがなければ、この問題の一般的な解決策への簡単な方法はありません。
ただし、単純な例の場合、上記のアプローチは適切です。
以下のコードは私のために働く:
static void Main(string[] args)
{
StringBuilder sb = new StringBuilder();
string path = new WebClient().DownloadString("https://www.google.com");
HtmlDocument htmlDoc = new HtmlDocument();
////htmlDoc.LoadHtml(File.ReadAllText(path));
htmlDoc.LoadHtml(path);
var bodySegment = htmlDoc.DocumentNode.Descendants("body").FirstOrDefault();
if (bodySegment != null)
{
foreach (var item in bodySegment.ChildNodes)
{
if (item.NodeType == HtmlNodeType.Element && string.Compare(item.Name, "script", true) != 0)
{
foreach (var a in item.Descendants())
{
if (string.Compare(a.Name, "script", true) == 0 || string.Compare(a.Name, "style", true) == 0)
{
a.InnerHtml = string.Empty;
}
}
sb.AppendLine(item.InnerText.Trim());
}
}
}
Console.WriteLine(sb.ToString());
Console.Read();
}
私はhtml-agility-packについてあまり知りませんが、これはc#の代替です。
public string GetPlainText()
{
WebRequest request = WebRequest.Create("URL for page you want to 'stringify'");
WebResponse response = request.GetResponse();
Stream data = response.GetResponseStream();
string html = String.Empty;
using (StreamReader sr = new StreamReader(data))
{
html = sr.ReadToEnd();
}
html = Regex.Replace(html, "<.*?>", "\n");
html = Regex.Replace(html, @"\\r|\\n|\n|\r", @"$");
html = Regex.Replace(html, @"\$ +", @"$");
html = Regex.Replace(html, @"(\$)+", Environment.NewLine);
return html;
}
これをHTMLページで表示する場合は、Environment.NewLineを<br/>
に置き換えます。
私はいつもプロジェクトに CsQuery を使用しています。おそらくHtmlAgilityPackよりも高速で、xpathの代わりにcssセレクターを使用する方がはるかに簡単です。
var html = @"<div>
<div>
<div>
line1
</div>
</div>
</div>
<div>line2</div>";
var lines = CQ.Create(html)
.Text()
.Replace("\r\n", "\n") // I like to do this before splitting on line breaks
.Split('\n')
.Select(s => s.Trim()) // Trim elements
.Where(s => !s.IsNullOrWhiteSpace()) // Remove empty lines
;
var result = string.Join(Environment.NewLine, lines);
上記のコードは期待どおりに機能しますが、予想される結果を伴うより複雑な例がある場合、このコードは簡単に対応できます。
たとえば、<br>
を保持する場合は、html変数で「--- br ---」のようなものに置き換えて、最終結果で再度分割できます。
正規表現なしのソリューション:
while (text.IndexOf("\n\n") > -1 || text.IndexOf("\n \n") > -1)
{
text = text.Replace("\n\n", "\n");
text = text.Replace("\n \n", "\n");
}
正規表現:
text = Regex.Replace(text, @"^\s*$\n|\r", "", RegexOptions.Multiline).TrimEnd();
また、私が覚えているように、
text = HtmlAgilityPack.HtmlEntity.DeEntitize(text);
好意を行います。