截断文本块(块元素)末尾的 HTML 内容
本文关键字:HTML 内容 元素 文本 | 更新日期: 2023-09-27 17:56:57
主要是当我们缩短/截断文本内容时,我们通常只是在特定字符索引处截断它。无论如何,这在 HTML 中已经很复杂了,但我想使用不同的措施截断我的 HTML 内容(使用内容可编辑div
生成):
- 我将定义字符索引
N
,该索引将用作截断起点限制 - 算法将检查内容是否至少有
N
个字符长(仅文本;不计算标签);如果没有,它将只返回整个内容 - 然后,它将检查从
N-X
到N+X
字符位置(仅文本)并搜索块节点的末端;X
是预定义的偏移值,可能大约N/5
到N/4
; - 如果多个区块节点在此范围内结束,算法将选择最接近极限索引
N
- 如果在此范围内没有块节点结束,它将在同一范围内找到最接近的单词边界,并选择最接近
N
的索引并在该位置截断。 - 返回具有有效 HTML 的截断内容(所有标记在末尾关闭)
我的内容可编辑生成的内容可能包括段落(带换行符)、预格式化代码块、块引用、有序和无序列表、标题、粗体和斜体(它们是内联节点,不应计入截断过程)等。 最终实现当然将定义哪些元素是可能的截断候选元素。标头即使它们是块 HTML 元素也不会算作截断点,因为我们不想要寡头。段落、列出单个项目、整个有序和无序列表、块引用、预格式化块、空元素等都是很好的。标头和所有内联块元素都不是。
例
让我们把这个非常堆栈溢出的问题作为我想截断的HTML内容的示例。让我们将截断限制设置为 1000,偏移量为 250 个字符 (1/4)。
此 DotNetFiddle 显示此问题的文本,同时还在其中添加了限制标记(|MIN|
表示字符 750,|LIMIT|
表示字符 1000,|MAX|
表示字符 1250)。
从示例中可以看出,两个块节点之间最接近字符 1000 的截断边界在 </OL>
和 P
之间(我的内容可编辑生成...这意味着我的 HTML 应该在这两个标签之间被截断,这将导致内容长度略低于 1000 个字符的文本,但保持截断的内容有意义,因为它不会只是截断某些文本段落中间的某个地方。
我希望这能解释与此算法相关的工作方式。
问题所在
我在这里看到的第一个问题是我正在处理像 HTML 这样的嵌套结构。我还必须检测不同的元素(只有块元素,没有内联元素)。最后但并非最不重要的一点是,我将只需要计算字符串中的某些字符,而忽略属于标签的字符。
可能的解决方案
- 我
- 可以通过创建一些代表内容节点及其层次结构的对象树来手动解析我的内容
- 我可以将 HTML 转换为更易于管理的内容,例如 markdown,然后只需搜索与我提供的索引
N
最近的新行并转换回 HTML - 使用类似 HTML Agility Pack 的东西并用它替换我的 #1 解析,然后以某种方式使用 XPath 提取块节点并截断内容
第二个想法
- 我
- 确信我可以通过做 #1 来做到这一点,但感觉我正在重新发明轮子。
- 我认为 #2 没有任何 C# 库,所以我也应该手动将 HTML 转换为 Markdown 或作为外部进程运行,即 pandoc。
- 我可以使用 HAP,因为它非常适合操作 HTML,但我不确定使用它是否会让我的截断足够简单。恐怕在我的自定义代码中,大部分处理仍将在 HAP 之外
应该如何处理这种截断算法?我的头脑似乎太累了,无法达成共识(或解决方案)。
一些可以截断内部文本的示例代码。它使用 InnerText
属性和CloneNode
方法的递归功能。
public static HtmlNode TruncateInnerText(HtmlNode node, int length)
{
if (node == null)
throw new ArgumentNullException("node");
// nothing to do?
if (node.InnerText.Length < length)
return node;
HtmlNode clone = node.CloneNode(false);
TruncateInnerText(node, clone, clone, length);
return clone;
}
private static void TruncateInnerText(HtmlNode source, HtmlNode root, HtmlNode current, int length)
{
HtmlNode childClone;
foreach (HtmlNode child in source.ChildNodes)
{
// is expected size is ok?
int expectedSize = child.InnerText.Length + root.InnerText.Length;
if (expectedSize <= length)
{
// yes, just clone the whole hierarchy
childClone = child.CloneNode(true);
current.ChildNodes.Add(childClone);
continue;
}
// is it a text node? then crop it
HtmlTextNode text = child as HtmlTextNode;
if (text != null)
{
int remove = expectedSize - length;
childClone = root.OwnerDocument.CreateTextNode(text.InnerText.Substring(0, text.InnerText.Length - remove));
current.ChildNodes.Add(childClone);
return;
}
// it's not a text node, shallow clone and dive in
childClone = child.CloneNode(false);
current.ChildNodes.Add(childClone);
TruncateInnerText(child, root, childClone, length);
}
}
还有一个示例 C# 控制台应用,它将取消此问题作为示例,并将其截断为 500 个字符。
class Program
{
static void Main(string[] args)
{
var web = new HtmlWeb();
var doc = web.Load("http://stackoverflow.com/questions/30926684/truncating-html-content-at-the-end-of-text-blocks-block-elements");
var post = doc.DocumentNode.SelectSingleNode("//td[@class='postcell']//div[@class='post-text']");
var truncated = TruncateInnerText(post, 500);
Console.WriteLine(truncated.OuterHtml);
Console.WriteLine("Size: " + truncated.InnerText.Length);
}
}
运行时,它应该显示以下内容:
<div class="post-text" itemprop="text">
<p>Mainly when we shorten/truncate textual content we usually just truncate it at specific character index. That's already complicated in HTML anyway, but I want to truncate my HTML content (generated using content-editable <code>div</code>) using different measures:</p>
<ol>
<li>I would define character index <code>N</code> that will serve as truncating startpoint <em>limit</em></li>
<li>Algorithm will check whether content is at least <code>N</code> characters long (text only; not counting tags); if it's not it will just return the whole content</li>
<li>It would then</li></ol></div>
Size: 500
注意:我没有在单词边界处截断,只是在字符边界处截断,不,它根本没有遵循我评论中的建议:-)
private void RemoveEmpty(HtmlNode node){
var parent = node.Parent;
node.Remove();
if(parent==null)
return;
// remove parent if it is empty
if(!parent.DescendantNodes.Any()){
RemoveEmpty(parent);
}
}
private void Truncate(DocumentNode root, int maxLimit){
var n = 0;
HtmlTextNode lastNode = null;
foreach(var node in root.DescendantNodes
.OfType<HtmlTextNode>().ToArray()){
var length = node.Text.Length;
n+= length;
if(n + length >= maxLimit){
RemoveEmpty(node);
}
}
}
// you are left with only nodes that add up to your max limit characters.
我将遍历整个 DOM 树并继续计算出现的文本字符数量。每当我达到限制 (N) 时,我都会删除该文本节点的额外字符,从那里开始,我将删除所有文本节点。
我相信这是一种安全的方法,可以保留所有HTML + CSS结构,同时只保留N个字符。