Java库截断html字符串?

时间:2015-02-17 17:09:58

标签: java string sanitization

在存储到DB&存储器之前,我需要截断已被我的应用程序清理过的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/>

7 个答案:

答案 0 :(得分:2)

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

  • 输入是由(x)html标记组成的字符串。您的示例不包含此内容,但我认为输入可以包含标记之间的文本。
  • 在您的问题的背景下,我们不关心嵌套。因此输入实际上只是与标签混合的文本,其中打开,关闭和自闭标签都被视为等效。
  • 标签可以包含引用值。
  • 您希望截断字符串,使字符串不会在标记中间被截断。所以在截断的字符串中每个'&lt;'角色必须有相应的'&gt;'字符。

我会给你两个解决方案,一个可能不正确的简单解决方案,具体取决于输入的内容,以及更复杂的解决方案。

第一种解决方案

对于第一个解决方案,我们首先找到最后一个'&gt;'截断大小之前的字符(这对应于完全关闭的最后一个标记)。在此字符可能出现不属于任何标记的文本之后,我们会搜索第一个'&lt;'最后一个关闭标签后的字符。在代码中:

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);
    }
}

当然这个解决方案不考虑引用的值字符串:'&lt;'和'&gt;'字符串可能出现在字符串中,在这种情况下应该忽略它们。无论如何我提到了解决方案,因为你提到你的输入已经过sanatized,所以你可以确保引用的字符串永远不会包含'&lt;'和'&gt;'字符。

第二种解决方案

要考虑引用的字符串,我们不能再依赖标准的Java类了,但是我们必须自己扫描输入并记住我们当前是否在标记内和字符串内部。如果我们遇到'&lt;'在字符串之外的字符,我们记住它的位置,这样当我们到达截断点时,我们知道最后打开的标记的位置。如果该标记未关闭,我们会在该标记开头之前截断。在代码中:

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);
}

答案 1 :(得分:1)

一种强有力的方法是使用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() );

输出:

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

它正在添加</img>标签,但这对html无害,如果您认为有必要,可以调整代码以完全匹配输出中的输入。

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

答案 2 :(得分:0)

我知道你想要的是我能提出的最简单的解决方案。

从子字符串末尾开始直到找到&#39;&gt;&#39;这是最后一个标记的结束标记。所以你可以确定在大多数情况下你只有完整的标签。

但是如果&gt;在文本里面?

确定这只是搜索,直到找到&lt;并确保这是标签的一部分(你知道标签字符串吗?),因为你只有链接,图像和格式化你可以很容易地检查这个。如果你找到另一个&gt;在找到&lt;开始标记之前这是字符串的新结尾。

易于操作,正确并适合您。


如果您不确定字符串/属性是否可以包含&lt;或者&gt;你需要检查&#34;的外观。和=&#34;检查你是否在一个字符串里面。 (请记住,您可以剪切属性值)。但我认为这是过度工程。我从未在&lt;中找到属性和&gt;在它中,通常在文本中,它也使用&amp; LT;和类似的东西。

答案 3 :(得分:0)

我不知道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>'

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

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>'

答案 4 :(得分:0)

你可以通过图书馆&#34; JSOUP&#34;来实现这一目标。 - 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());
    }
}

答案 5 :(得分:-1)

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

[更新]

我提到了JSoup和HtmlParser,因为它们在浏览器方面是容错的。请检查它们是否适合您,因为它们非常善于处理格式错误和损坏的HTML文本。从HTML中创建一个DOM并将其写回字符串,您应该删除损坏的标签,您也可以自己过滤DOM,如果必须,可以删除更多内容。

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

答案 6 :(得分:-1)

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

当我没记错的时候,有一些DOM树表示与底层字符串表示密切配合。因此它们是完全一致的。我自己写了一个,但我认为jSoup有这样的模式。由于那里有很多解析器,你应该能找到一个实际的解析器。

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

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

如果您没有找到适合您的解析器,您可以向我索取建议。