【问题标题】:Truncating HTML content at the end of text blocks (block elements)在文本块(块元素)末尾截断 HTML 内容
【发布时间】:2015-09-04 18:14:56
【问题描述】:

主要是当我们缩短/截断文本内容时,我们通常只是在特定字符索引处截断它。无论如何,这在 HTML 中已经很复杂了,但我想使用不同的措施截断我的 HTML 内容(使用 content-editable div 生成):

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

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

示例

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

This DotNetFiddle 显示此问题的文本,同时还在其中添加限制标记(|MIN| 表示字符 750,|LIMIT| 表示字符 1000 和 |MAX| 表示字符 1250)。

从示例中可以看出两个块节点之间最接近的截断边界到字符 1000 是在 </OL>P 之间(我的内容可编辑生成... em>)。这意味着我的 HTML 应该在这两个标签之间被截断,这将导致内容文本长度少于 1000 个字符,但保持截断的内容有意义,因为它不会只是截断某些文本段落中间的某个位置。

我希望这能解释与此算法相关的事情应该如何工作。

问题

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

可能的解决方案

  1. 我可以通过创建一些表示内容节点及其层次结构的对象树来手动解析我的内容
  2. 我可以将 HTML 转换为更易于管理的内容,例如 markdown,然后只需搜索最接近我提供的索引 N 的新行并转换回 HTML
  3. 使用 HTML Agility Pack 之类的东西并用它替换我的 #1 解析,然后以某种方式使用 XPath 提取块节点并截断内容

第二个想法

  • 我确信我可以通过执行 #1 来做到这一点,但感觉我是在重新发明轮子。
  • 我认为 #2 没有任何 C# 库,因此我也应该手动将 HTML 转换为 Markdown,或者将 pandoc 作为外部进程运行。
  • 我可以使用 HAP,因为它在处理 HTML 方面非常出色,但我不确定使用它是否足够简单。恐怕我的自定义代码中的大部分处理仍将在 HAP 之外

应该如何处理这种截断算法?我的脑袋似乎太累了,无法达成共识(或解决方案)。

【问题讨论】:

  • 当然没有灵丹妙药,但我会使用 HAP,HAP 可以通过一个 xpath 获取所有文本://text()。然后,每个节点还有一个XPath 属性,因此您可以从这些文本元素来回遍历树。使用InnerHtml 属性可以很容易地更改这些文本元素内容。最后,HAP 会在输出 HTML 时自动关闭未关闭的元素。
  • @SimonMourier:想在答案中显示一些代码吗?
  • 你有一些示例输入和预期输出吗?
  • 抱歉,我想说点题外话。重新发明轮子没有错,如果你认为你可以让它变得更好或更简单,为什么不呢?毕竟,我们是从几个世纪前的木轮中找到我们现在的轮子的,:p

标签: c# html extract truncate


【解决方案1】:

这里是一些可以截断内部文本的示例代码。它使用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

注意:我没有在单词边界处截断,只是在字符边界处截断,不,它根本没有遵循我评论中的建议:-)

【讨论】:

  • 我追求的不是字符,也不是单词边界,而是块元素边界。因此,修剪后的纯文本内容可能比指定的限制更短或更长,但在一定范围内limit-offset &lt; limit &lt; limit + offset,只要块元素的结尾最接近limit
  • 我不明白你的意思。也许我的回答可以,你试过吗?或者请提供样品。
  • 是的,我已经尝试过并查看了您的代码的作用。由于显然没有很好地解释这一点,我编辑了我的问题,同时还提供了可运行的小提琴,您可以在其中实际看到应该截断哪些内容以及如何截断内容。我什至用过你的代码来加载这个问题。
  • 如果你只是在text != null 上做一个return,也许那会做你想做的事。无论如何,我认为这是一个好的开始。但是你的算法对我来说似乎模棱两可。我不确定你的 N & X 东西总是有解决方案。例如,如果我只有一个大小为 2000 的大文本,其中 N 设置为 1000,X 设置为 250,我该怎么办?返回一个长度为 0 的文本?
  • 请参阅我的问题中的 #5,其中涵盖了您所询问的确切情况。
【解决方案2】:
   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.

【讨论】:

  • 但这不是我想要的,因为您可能会截断&lt;/b&gt; 末尾的内容,这是不正确的。而且它也不会截断到最接近您的maxLimit。你总是在&gt;=maxLimit 上截断,即使某些块元素可能在maxLimit 之前仅结束一个字符。
  • 我刚刚展示了一个小示例,您必须修改此逻辑以满足您的需要,如果没有看到任何示例数据,很难知道您想要什么。如果您可以显示输入和预期输出,我可以进一步调整。
  • 确实如此。但除此之外,您的if 条件(实际上应该读作if (n &gt; maxLimit),因为您已经在它之前将length 添加到n)也应该改变,因为您总是将内容截断为&lt;=maxLimit。如果您更改if 条件以检查maxLimit 的当前长度和新长度增量,那么实际上您已经非常接近了。如果这两个中的任何一个小于我的问题中定义的偏移量X,它将正确截断。
  • 但是您的代码还存在其他问题,因为您在迭代期间更改了枚举,这是一个运行时错误。最好的方法是让你用 HAP 写一个DotNetFiddle,然后自己看看它是否有效以及如何。
  • 我在枚举之前做了 ToArray,所以我可以毫无问题地修改它
【解决方案3】:

我将遍历整个 DOM 树并不断计算出现的文本字符的数量。每当我达到限制 (N) 时,我都会删除该文本节点的多余字符,然后从那里删除所有文本节点。

我相信这是保持所有 HTML+CSS 结构同时只保留 N 个字符的安全方法。

【讨论】:

  • 但这只是通常的 N 个字符截断......不是我试图实现的每个块。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-03
  • 2012-02-06
  • 2011-01-04
  • 1970-01-01
  • 2020-10-30
  • 1970-01-01
相关资源
最近更新 更多