【问题标题】:Java Library to truncate html strings?Java库截断html字符串?
【发布时间】:2015-04-18 11:25:07
【问题描述】:

在存储到数据库之前,我需要截断已经被我的应用程序清理过的 html 字符串,并且只包含链接、图像和格式化标签。但在呈现给用户时,需要截断以呈现内容概览。

所以我需要在java中缩写html字符串这样

<img src="http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg" />   
<br/><a href="http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg" />

当被截断时不会返回类似的东西

<img src="http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg" />   
<br/><a href="htt

而是返回

<img src="http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg" />   
<br/>

【问题讨论】:

  • 我不清楚你的规格。您只是想删除所有锚(“&lt;a /&gt;”)标签吗?
  • 截断是什么意思?您应该具体说明要删除的内容。
  • 是的,应该清理所有无效和损坏的 html 片段
  • 重点是 DOM 解析器首先读取整个 XML 代码,然后尝试将其解析成树。 SAX 解析器从上到下读取(如果您无论如何都需要底部)并丢弃它无法理解的所有内容。
  • 按字符串长度截断 HTML 代码似乎没有意义。按特定元素的数量(例如最多一张图像)截断似乎更合适。

标签: java string sanitization


【解决方案1】:

即使在阅读了所有 cmets 之后,您的要求也有点模糊。鉴于您的示例和解释,我假设您的要求如下:

  • 输入是一个由 (x)html 标签组成的字符串。您的示例不包含此内容,但我假设输入可以包含标签之间的文本。
  • 就您的问题而言,我们不关心嵌套。所以输入实际上只是与标签混合的文本,其中打开、关闭和自关闭标签都被认为是等价的。
  • 标签可以包含引用的值。
  • 您希望截断字符串,以使字符串不会在标记中间被截断。所以在截断的字符串中,每个 '' 字符。

我会给你两种解决方案,一种可能不正确的简单解决方案,具体取决于输入的确切外观,另一种是正确的更复杂的解决方案。

第一个解决方案

对于第一个解决方案,我们首先找到截断大小之前的最后一个“>”字符(这对应于完全关闭的最后一个标签)。在这个字符之后可能会出现不属于任何标签的文本,因此我们然后搜索最后一个闭合标签之后的第一个'

public static String truncate1(String input, int size)
{
    if (input.length() < size) return input;

    int pos = input.lastIndexOf('>', size);
    int pos2 = input.indexOf('<', pos);

    if (pos2 < 0 || pos2 >= size) {
        return input.substring(0, size);
    }        
    else {
        return input.substring(0, pos2);
    }
}

当然,这个解决方案不考虑带引号的值字符串:'' 字符可能出现在字符串中,在这种情况下它们应该被忽略。无论如何我都提到了解决方案,因为您提到您的输入是经过消毒的,因此您可以确保引用的字符串从不包含“”字符。

第二种解决方案

要考虑带引号的字符串,我们不能再依赖标准 Java 类,但我们必须自己扫描输入并记住我们当前是否在标签内和字符串内。如果我们在字符串之外遇到一个 '

public static String truncate2(String input, int size)
{
    if (input.length() < size) return input;

    int lastTagStart = 0;
    boolean inString = false;
    boolean inTag = false;

    for (int pos = 0; pos < size; pos++) {
        switch (input.charAt(pos)) {
            case '<':
                if (!inString && !inTag) {
                    lastTagStart = pos;
                    inTag = true;
                }
                break;
            case '>':
                if (!inString) inTag = false;
                break;
            case '\"':
                if (inTag) inString = !inString;
                break;
        }
    }
    if (!inTag) lastTagStart = size;
    return input.substring(0, lastTagStart);
}

【讨论】:

    【解决方案2】:

    一种可靠的方法是使用 hotsax code 来解析 HTML,让您使用传统的低级 SAX XML API 与解析器交互 [注意它不是它解析的 XML 解析器格式不佳的 HTML 仅选择让您使用标准 XML API 与其交互)。

    Here on github 我创建了一个工作快速且简单的示例项目,其中有一个 main class 来解析您截断的示例字符串:

        XMLReader parser = XMLReaderFactory.createXMLReader("hotsax.html.sax.SaxParser");
    
        final StringBuilder builder = new StringBuilder();
    
        ContentHandler handler = new DoNothingContentHandler(){
    
            StringBuilder wholeTag = new StringBuilder();
            boolean hasText = false;
            boolean hasElements = false;
            String lastStart = "";
    
            @Override
            public void characters(char[] ch, int start, int length)
                    throws SAXException {
                String text = (new String(ch, start, length)).trim();
                wholeTag.append(text);
                hasText = true;
            }
    
            @Override
            public void endElement(String namespaceURI, String localName,
                    String qName) throws SAXException {
                if( !hasText && !hasElements && lastStart.equals(localName)) {
                    builder.append("<"+localName+"/>");
                } else {
                    wholeTag.append("</"+ localName +">");
                    builder.append(wholeTag.toString());
                }
    
                wholeTag = new StringBuilder();
                hasText = false;
                hasElements = false;
            }
    
            @Override
            public void startElement(String namespaceURI, String localName,
                    String qName, Attributes atts) throws SAXException {
                wholeTag.append("<"+ localName);
                for( int i = 0; i < atts.getLength(); i++) {
                    wholeTag.append(" "+atts.getQName(i)+"='"+atts.getValue(i)+"'");
                    hasElements = true;
                }
                wholeTag.append(">");
                lastStart = localName;
                hasText = false;
            }
    
        };
        parser.setContentHandler(handler);
    
        //parser.parse(new InputSource( new StringReader( "<div>this is the <em>end</em> my <br> friend <a href=\"whatever\">some link</a>" ) ));
        parser.parse(new InputSource( new StringReader( "<img src=\"http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg\" />\n<br/><a href=\"htt" ) ));
    
        System.out.println( builder.toString() );
    

    它输出:

    &lt;img src='http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg'&gt;&lt;/img&gt;&lt;br/&gt;

    它添加了一个&lt;/img&gt; 标记,但这对 html 无害,如果您认为有必要,可以调整代码以与输出中的输入完全匹配。

    Hotsax 实际上是使用 yacc/flex 编译器工具生成的代码,该编译器工具在定义 html 的低级语法的 HtmlParser.yStyleLexer.flex 文件上运行。因此,您将从创建该语法的人的工作中受益;您需要做的就是编写一些相当简单的代码和测试用例来重新组装已解析的片段,如上所示。这比尝试编写自己的正则表达式或最糟糕的编码字符串扫描器来尝试解释非常脆弱的字符串要好得多。

    【讨论】:

    • 真的是让那些解析器丢掉自己看不懂的代码吗?据我所知,默认行为是忽略最后一个/下一个标记,直到当前规则再次开始变得有意义。他要求的恰恰相反...... .
    • 以他给出的输入为例并输出他说的他想要的代码怎么可能是“不是他要求的”?您对 SAX 解析器如何工作的其余评论没有意义。
    • 据我所知,他想从标签汤中删除不完整和错误的标签。我怀疑通常容错/失败的解析器工作的方式与丢弃不适合的部分完全相反。这就是为什么我问它的原因,除非它的实现方式与我们习惯的不同。
    • 你为什么认为我没有能力去理解和询问事情。这是为了得出这个结论的大量侵略和偏见。您提供的内容可能无济于事。请将此添加到您的测试中: 它打印出什么?如果我理解这个问题,这就是他所要求的。如果它有效,我会很满意。既然你说 Sax 的东西是从 yacc/flex 生成的,我认为它带有这样的解析器的默认行为。
    • 我现在明白他在做什么了。但是对于损坏的 HTML 内容,您的解决方案将无法按预期工作。
    【解决方案3】:

    在我了解您想要的这里是我能想出的最简单的解决方案之后。

    只需从子字符串的末尾开始工作,直到找到 '>' 这是最后一个标签的结束标记。所以你可以确定在大多数情况下你只有完整的标签。

    但是如果 > 在文本中呢?

    为了确定这一点,只需搜索直到找到 ,这就是字符串的新结尾。

    容易做,正确,应该适合你。


    如果您不确定字符串/属性是否可以包含 ,您需要检查 " 和 =" 的出现以检查您是否在字符串内。 (记住你可以剪切一个属性值)。但我认为这是过度工程。我从来没有找到一个带有 的属性,通常在文本中它也使用 & lt ;和类似的东西。

    【讨论】:

      【解决方案4】:

      我不知道 OP 需要解决的问题的上下文,但我不确定通过源代码的长度而不是视觉表示的长度截断 html 代码是否有意义(当然,它可以变得任意复杂)。

      也许一个组合的解决方案会很有用,所以你不会惩罚带有大量标记或长链接的 html 代码,但还要设置一个明确的总限制,不能超过。像其他人已经写过的那样,使用像 JSoup 这样的专用 HTML 解析器可以处理格式不正确甚至无效的 HTML。

      该解决方案大致基于 JSoup 的Cleaner。它遍历源代码的已解析 dom 树并尝试重新创建目标树,同时不断检查是否已达到限制。

      import org.jsoup.nodes.*;
      import org.jsoup.parser.*;
      import org.jsoup.select.*;
      
          String html = "<img src=\"http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg\" />" +
                        "<br/><a href=\"http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg\" />";
      
          //String html = "<b>foo</b>bar<p class=\"baz\">Some <img />Long Text</p><a href='#'>hello</a>";
      
          Document srcDoc = Parser.parseBodyFragment(html, "");
          srcDoc.outputSettings().prettyPrint(false);
      
          Document dstDoc = Document.createShell(srcDoc.baseUri());
          dstDoc.outputSettings().prettyPrint(false);
      
          Element dst = dstDoc.body();
      
          NodeVisitor v = new NodeVisitor() {
              private static final int MAX_HTML_LEN = 85;
              private static final int MAX_TEXT_LEN = 40;
      
              Element cur = dst;
              boolean stop = false;
              int resTextLength = 0;
      
              @Override
              public void head(Node node, int depth) {
                  // ignore "body" element
                  if (depth > 0) {
                      if (node instanceof Element) {
                          Element curElement = (Element) node;
                          cur = cur.appendElement(curElement.tagName());
                          cur.attributes().addAll(curElement.attributes());
                          String resHtml = dst.html();
                          if (resHtml.length() > MAX_HTML_LEN) {
                              cur.remove();
                              throw new IllegalStateException("html too long");
                          }
                      } else if (node instanceof TextNode) {
                          String curText = ((TextNode) node).getWholeText();
                          String resHtml = dst.html();
                          if (curText.length() + resHtml.length() > MAX_HTML_LEN) {
                              cur.appendText(curText.substring(0, MAX_HTML_LEN - resHtml.length()));
                              throw new IllegalStateException("html too long");
                          } else if (curText.length() + resTextLength > MAX_TEXT_LEN) {
                              cur.appendText(curText.substring(0, MAX_TEXT_LEN - resTextLength));
                              throw new IllegalStateException("text too long");
                          } else {
                              resTextLength += curText.length();
                              cur.appendText(curText);
                          }
                      }
                  }
              }
      
              @Override
              public void tail(Node node, int depth) {
                  if (depth > 0 && node instanceof Element) {
                      cur = cur.parent();
                  }
              }
          };
      
          try {
              NodeTraversor t = new NodeTraversor(v);
              t.traverse(srcDoc.body());
          } catch (IllegalStateException ex) {
              System.out.println(ex.getMessage());
          }
      
          System.out.println(" in='" + srcDoc.body().html() + "'");
          System.out.println("out='" + dst.html() + "'");
      

      对于最大长度为 85 的给定示例,结果为:

      html too long
       in='<img src="http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg"><br><a href="http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg"></a>'
      out='<img src="http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg"><br>'
      

      它还在嵌套元素中正确截断,对于 16 的最大 html 长度,结果是:

      html too long
       in='<i>f<b>oo</b>b</i>ar'
      out='<i>f<b>o</b></i>'
      

      对于最大长度为 2 的文本,长链接的结果是:

      text too long
       in='<a href="someverylonglink"><b>foo</b>bar</a>'
      out='<a href="someverylonglink"><b>fo</b></a>'
      

      【讨论】:

        【解决方案5】:

        您可以使用库 "JSOUP" - html 解析器来实现此目的。

        您可以从下面的链接下载它。

        Download JSOUP

        import org.jsoup.Jsoup;
        import org.jsoup.nodes.Document;
        import org.jsoup.select.Elements;
        
        public class HTMLParser 
        {
            public static void main(String[] args)
            {
                String html = "<img src=\"http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg\" /><br/><a href=\"http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg\" /><img src=\"http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg\" /><br/><a href=\"http://d2qxdzx5iw7vis.cloudfront.net/34775606.jpg\" />";
        
                Document doc = Jsoup.parse(html);
                doc.select("a").remove();
        
                System.out.println(doc.body().children());
            }
        }
        

        【讨论】:

        • 我也一直在使用 jsoup,并且非常惊讶它的效果。
        • 对不起,但我不明白这如何回答 OPs 问题。据我了解,@user01 想将给定的 html sn-p 截断(例如缩短)到给定的长度,同时保持 html 代码的正确性。相反,此代码选择并删除某些元素。它如何决定要删除哪些元素?为什么”?如果 更短或没有 怎么办?
        【解决方案6】:

        不管你想做什么。我倾向于使用两个库 jSoup 和 HtmlParser。请检查一下。我也看到了熊熊的 XHTML。现在更多的是关于 HTML5(没有 XHTML 对应物)。

        [更新]

        我提到 JSoup 和 HtmlParser 是因为它们在某种程度上是浏览器的容错能力。请检查它们是否适合您,因为它们非常擅长处理格式错误和损坏的 HTML 文本。从您的 HTML 创建一个 DOM 并将其写回字符串,您应该摆脱损坏的标签,您也可以自己过滤 DOM,并在必要时删除更多内容。

        PS:我想 XML 的十年终于(并且很高兴)结束了。今天 JSON 将被过度使用。

        【讨论】:

          【解决方案7】:

          我认为作为潜在解决方案的第三个潜在答案是首先不使用字符串。

          如果我没记错的话,有一些 DOM 树表示可以与底层字符串表示紧密配合。因此它们是精确的。我自己写了一个,但我认为 jSoup 有这样的模式。由于那里有很多解析器,您应该能够找到一个真正的解析器。

          使用这样的解析器,您可以轻松查看哪个标签从哪个字符串位置运行到另一个位置。实际上,这些解析器维护文档的字符串并对其进行更改,但仅存储范围信息,例如文档中的开始和停止位置,避免为嵌套节点乘以这些信息。

          因此,您可以找到给定位置的最外层节点,准确地知道从什么到哪里,并且可以轻松地决定是否可以在您的 sn-p 中使用该标签(包括其所有子节点)。因此,您将有机会打印完整的文本节点等,而不会冒仅显示部分标签信息或标题文本等的风险。

          如果你没有找到适合你的解析器,你可以向我寻求建议。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2016-12-22
            • 1970-01-01
            • 1970-01-01
            • 2014-05-15
            • 2012-01-20
            • 1970-01-01
            相关资源
            最近更新 更多