在文本块(块元素)结尾截断HTML内容

10

通常情况下,我们在缩短/截断文本内容时,通常只是在特定字符索引处进行截断。这已经很复杂了,但我希望使用不同的方法截断生成使用可编辑内容div)的HTML内容:

  1. 我会定义一个字符索引N,作为截断起始点的限制
  2. 算法将检查内容是否至少N个字符长(仅限文本;不计标签);如果不是,则返回整个内容
  3. 然后它将从N-XN+X字符位置(仅限文本)进行检查,并搜索块节点的结束位置;X是预定义的偏移值,可能约为N/5N/4;
  4. 如果在此范围内有多个块节点结束,算法将选择距离限制索引N最近的节点。
  5. 如果此范围内没有块节点结束,则它将在同一范围内找到最接近的单词边界,并选择最接近N的索引并在该位置截断。
  6. 返回截断后的内容,其中包含有效的HTML(所有标签在末尾关闭)

我的可编辑内容生成的内容可能包括段落(带有换行符),预格式化代码块,块引用,有序和无序列表,标题,加粗和斜体(这是内联节点,不应计入截断过程)等。当然,最终实现将定义哪些元素可能是截断候选项。标题即使它们是块HTML元素也不会作为截断点计数,因为我们不想要孤立的头部。段落、列表单个项、整个有序和无序列表、块引用、预格式化块、空元素等都是良好的截断候选项。标题和所有内联块元素都不是。

例子

让我们以这个stackoverflow问题作为要截断的HTML内容的例子。让我们将截断限制设置为1000,偏移量设置为250字符(1/4)。

这个 DotNetFiddle 显示了这个问题的文字,同时在其中添加了限制标记(|MIN| 代表字符 750,|LIMIT| 代表字符 1000,|MAX| 代表字符 1250)。

如从示例中可以看出,接近字符 1000 的两个块节点之间的截断边界是 </OL>P(“我的 content-editable 生成的...”)。这意味着我的 HTML 应该在这两个标签之间被截断,从文本角度来看,结果应该是少于 1000 个字符长的内容,但保持截断的内容有意义,因为它不会在某些文本段中间截断。

我希望这解释清楚了这个算法应该如何工作相关的事情。

问题

我看到的第一个问题是我正在处理类似 HTML 的嵌套结构。我还要检测不同的元素(只有块元素而没有内联元素)。最后但并非不重要的是,我将只计算字符串中的某些字符,并忽略属于标签的字符。

可能的解决方案

  1. 我可以通过创建表示内容节点及其层次结构的对象树来手动解析我的内容。
  2. 我可以将 HTML 转换为更易于管理的 Markdown,然后简单地搜索最接近提供的索引 N 的换行符,并将其转换回 HTML。
  3. 使用类似 HTML Agility Pack 的工具替换我的 #1 解析,然后以某种方式使用 XPath 提取块节点并截断内容。

另一种想法

  • 我相信我可以通过执行 #1 来实现,但感觉自己在重复发明轮子。
  • 我不认为有任何 C# 库适用于 #2,因此我也应该手动进行 HTML 到 Markdown 的转换,或运行例如 pandoc 作为外部进程。
  • 我可以使用HAP,因为它擅长操作HTML,但我不确定是否简单地使用它来进行截断足够。我担心大部分处理仍将在我的自定义代码中而非HAP中。
  • 如何处理这种截断算法?我的头脑似乎太疲惫了,无法达成一致意见(或解决方案)。


    当然,这并非一劳永逸的解决方案,但我建议使用HAP。 HAP可以使用一个xpath获取所有文本://text()。然后,每个节点还具有XPath属性,因此您可以从这些文本元素来回遍历树。这些文本元素的内容可以使用InnerHtml属性轻松更改。最后,HAP在输出HTML时会自动关闭未关闭的元素。 - Simon Mourier
    @SimonMourier:能否在回答中展示一些代码? - Robert Koritnik
    你有一些样例输入和期望输出吗? - Simon Mourier
    1
    抱歉,我想说一些题外话。如果你认为你可以做得更好或更简单,那么重新发明轮子并没有什么不对。毕竟,我们从几个世纪前的木制车轮发展到了现在的轮子。 :p - am05mhz
    3个回答

    3

    这里有一段示例代码可以截取内部文本。它使用了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("https://dev59.com/n10Z5IYBdhLWcg3w6jwe");
              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
    

    注意:我没有按单词边界截断,而是按字符边界截断,不过,我并没有完全按照我的评论建议来做 :-)

    我需要的不是字符或单词边界,而是块元素边界。因此,修剪后的文本内容可能比指定的限制更短或更长,但在一定范围内limit-offset < limit < limit + offset,只要块元素的结束最接近于 limit 即可。 - Robert Koritnik
    我不明白你的意思。也许我的回答能解决问题,你试过了吗?或者请给一个示例。 - Simon Mourier
    是的,我已经尝试并看到了您的代码的运行情况。由于似乎没有很好地解释清楚,我编辑了我的问题,并提供了可运行的 fiddle,在那里您可以实际看到内容应该如何被截断。我甚至使用了您加载此问题的代码。 - Robert Koritnik
    如果你只是在 text != null 上执行 return,也许这就是你想要的。我认为这是一个不错的开始。但是你的算法对我来说似乎有些模糊。我不确定在你的 N 和 X 事物中是否总是有解决方案。例如,如果我只有一个大小为 2000 的大文本,其中 N 设置为 1000,X 设置为 250,那我该怎么办?返回一个长度为 0 的文本吗? - Simon Mourier
    请看我的问题中的第5点,它涵盖了你所问的这种情况。 - Robert Koritnik

    0
       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.
    

    但这不是我要求的,因为您可能会在</b>的末尾截断内容,这是不正确的。它也没有截断到最接近的maxLimit。即使某些块元素可能仅在maxLimit之前一个字符结束,您仍然会在>=maxLimit上截断。 - Robert Koritnik
    我刚展示了一个小样本,你需要修改这个逻辑以适应你的需求,如果没有看到任何样本数据,很难知道你想要什么。如果你能展示输入和期望输出,我可以进一步调整它。 - Akash Kava
    我在枚举之前进行了ToArray操作,这样我就可以在没有任何问题的情况下修改它。 - Akash Kava
    好的,我漏掉了这个。但是你怎么得到生成的HTML呢?你正在从数组中删除,那么之后如何访问被截断的内容? - Robert Koritnik
    我并没有从数组中删除,而是从父级中删除,你可以通过获取root.innerHtml来获取修改后的HTML。 - Akash Kava
    显示剩余5条评论

    -1
    我将遍历整个DOM树并计算出现的文本字符数。每当我达到限制(N)时,我将删除该文本节点的额外字符,并从那里开始仅删除所有文本节点。
    我相信这是一种安全的方法,可以保留所有HTML + CSS结构,同时仅保留N个字符。

    但这只是通常的N个字符截断...并非我正试图实现的每个块。 - Robert Koritnik

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接