我该如何在Java中取消转义HTML字符实体?

187

基本上,我想解码给定的HTML文档,并替换所有特殊字符,例如" "" "">"">"

.NET中,我们可以使用HttpUtility.HtmlDecode方法。

Java中的等效函数是什么?


5
被称为字符实体。已编辑标题。 - Eugene Yokota
12个回答

223

我已经使用了Apache Commons StringEscapeUtils.unescapeHtml4()来实现:

将包含实体转义符的字符串还原为包含与转义符相对应的Unicode字符的字符串。支持HTML 4.0实体。


23
很遗憾,我今天才意识到它不能很好地解码HTML特殊字符 :( - Sid
1
一个不太光彩的技巧是将值最初存储在隐藏字段中以逃避它,然后目标字段应该从隐藏字段获取该值。 - setzamora
3
StringEscapeUtils类已过时并已移至Apache commons-text - Pauli
2
我想将字符串 <p>&uuml;&egrave;</p> 转换为 <p>üé</p>,使用 StringEscapeUtils.unescapeHtml4() 方法得到的结果是 &lt;p&gt;üè&lt;/p&gt;。有没有一种方法可以保留现有的 HTML 标记? - Nickkk
如果我有类似于 &#147; 的东西,它在 Windows-1252 中转义为引号,但在 Unicode 中是一些控制字符,那么转义编码可以更改吗? - ifly6
链接半损坏(页面锚点)。 - Peter Mortensen

67
其他答案提到的库可能是不错的解决方案,但如果你已经在项目中处理真实世界的HTML内容,Jsoup项目比仅仅管理“ampersand pound FFFF semicolon”要好得多。
// textValue: <p>This is a&nbsp;sample. \"Granny\" Smith &#8211;.<\/p>\r\n
// becomes this: This is a sample. "Granny" Smith –.
// with one line of code:
// Jsoup.parse(textValue).getText(); // for older versions of Jsoup
Jsoup.parse(textValue).text();

// Another possibility may be the static unescapeEntities method:
boolean strictMode = true;
String unescapedString = org.jsoup.parser.Parser.unescapeEntities(textValue, strictMode);

此外,您还可以获得方便的 API 用于提取和操作数据,使用最佳的 DOM、CSS 和 jQuery 类似的方法。它是开源的,并且采用 MIT 许可证


4
点赞+,但我应该指出,新版本的Jsoup使用.text()代替.getText() - SourceVisor
6
可以直接使用org.jsoup.parser.Parser.unescapeEntities(String string, boolean inAttribute)方法进行转义。API文档:https://jsoup.org/apidocs/org/jsoup/parser/Parser.html#unescapeEntities-java.lang.String-boolean- - danneu
3
非常好,因为我已经在我的项目中使用Jsoup。此外,@danneu是正确的——Parser.unescapeEntities确实像宣传的那样工作。 - MandisaW
为什么以下代码不能返回未转义的HTML内容:Parser.unescapeEntities(Jsoup.parse("<div>•</div>").text(), true) - undefined
@DavesPlanet,当你尝试使用Parser.unescapeEntities("<div>&#8226;</div>", true)时会发生什么? - undefined
1
@Dale,请查看问题77405300以获取详细信息,但问题是我得到的是一个带有一个点的UTF16字符串,而不是文本。 - undefined

46

我在我的项目中尝试使用Apache CommonsStringEscapeUtils.unescapeHtml3(),但我对它的性能不满意。事实证明,它执行了许多无用的操作。首先,即使字符串中没有要取消转义的内容,每次调用它都会分配一个StringWriter。我已经以不同的方式重写了代码,现在它的速度快多了。

下面的代码取消转义所有HTML 3符号和数字转义(相当于Apache的unescapeHtml3)。如果需要HTML 4,您只需向映射添加更多条目即可。

package com.example;

import java.io.StringWriter;
import java.util.HashMap;

public class StringUtils {

    public static final String unescapeHtml3(final String input) {
        StringWriter writer = null;
        int len = input.length();
        int i = 1;
        int st = 0;
        while (true) {
            // Look for '&'
            while (i < len && input.charAt(i-1) != '&')
                i++;
            if (i >= len)
                break;

            // Found '&', look for ';'
            int j = i;
            while (j < len && j < i + MAX_ESCAPE + 1 && input.charAt(j) != ';')
                j++;
            if (j == len || j < i + MIN_ESCAPE || j == i + MAX_ESCAPE + 1) {
                i++;
                continue;
            }

            // Found escape
            if (input.charAt(i) == '#') {
                // Numeric escape
                int k = i + 1;
                int radix = 10;

                final char firstChar = input.charAt(k);
                if (firstChar == 'x' || firstChar == 'X') {
                    k++;
                    radix = 16;
                }

                try {
                    int entityValue = Integer.parseInt(input.substring(k, j), radix);

                    if (writer == null)
                        writer = new StringWriter(input.length());
                    writer.append(input.substring(st, i - 1));

                    if (entityValue > 0xFFFF) {
                        final char[] chrs = Character.toChars(entityValue);
                        writer.write(chrs[0]);
                        writer.write(chrs[1]);
                    } else {
                        writer.write(entityValue);
                    }

                } catch (NumberFormatException ex) {
                    i++;
                    continue;
                }
            }
            else {
                // Named escape
                CharSequence value = lookupMap.get(input.substring(i, j));
                if (value == null) {
                    i++;
                    continue;
                }

                if (writer == null)
                    writer = new StringWriter(input.length());
                writer.append(input.substring(st, i - 1));

                writer.append(value);
            }

            // Skip escape
            st = j + 1;
            i = st;
        }

        if (writer != null) {
            writer.append(input.substring(st, len));
            return writer.toString();
        }
        return input;
    }

    private static final String[][] ESCAPES = {
        {"\"",     "quot"}, // " - double-quote
        {"&",      "amp"}, // & - ampersand
        {"<",      "lt"}, // < - less-than
        {">",      "gt"}, // > - greater-than

        // Mapping to escape ISO-8859-1 characters to their named HTML 3.x equivalents.
        {"\u00A0", "nbsp"},   // Non-breaking space
        {"\u00A1", "iexcl"},  // Inverted exclamation mark
        {"\u00A2", "cent"},   // Cent sign
        {"\u00A3", "pound"},  // Pound sign
        {"\u00A4", "curren"}, // Currency sign
        {"\u00A5", "yen"},    // Yen sign = yuan sign
        {"\u00A6", "brvbar"}, // Broken bar = broken vertical bar
        {"\u00A7", "sect"},   // Section sign
        {"\u00A8", "uml"},    // Diaeresis = spacing diaeresis
        {"\u00A9", "copy"},   // © - copyright sign
        {"\u00AA", "ordf"},   // Feminine ordinal indicator
        {"\u00AB", "laquo"},  // Left-pointing double angle quotation mark = left pointing guillemet
        {"\u00AC", "not"},    // Not sign
        {"\u00AD", "shy"},    // Soft hyphen = discretionary hyphen
        {"\u00AE", "reg"},    // ® - registered trademark sign
        {"\u00AF", "macr"},   // Macron = spacing macron = overline = APL overbar
        {"\u00B0", "deg"},    // Degree sign
        {"\u00B1", "plusmn"}, // Plus-minus sign = plus-or-minus sign
        {"\u00B2", "sup2"},   // Superscript two = superscript digit two = squared
        {"\u00B3", "sup3"},   // Superscript three = superscript digit three = cubed
        {"\u00B4", "acute"},  // Acute accent = spacing acute
        {"\u00B5", "micro"},  // Micro sign
        {"\u00B6", "para"},   // Pilcrow sign = paragraph sign
        {"\u00B7", "middot"}, // Middle dot = Georgian comma = Greek middle dot
        {"\u00B8", "cedil"},  // Cedilla = spacing cedilla
        {"\u00B9", "sup1"},   // Superscript one = superscript digit one
        {"\u00BA", "ordm"},   // Masculine ordinal indicator
        {"\u00BB", "raquo"},  // Right-pointing double angle quotation mark = right pointing guillemet
        {"\u00BC", "frac14"}, // Vulgar fraction one quarter = fraction one quarter
        {"\u00BD", "frac12"}, // Vulgar fraction one half = fraction one half
        {"\u00BE", "frac34"}, // Vulgar fraction three quarters = fraction three quarters
        {"\u00BF", "iquest"}, // Inverted question mark = turned question mark
        {"\u00C0", "Agrave"}, // А - uppercase A, grave accent
        {"\u00C1", "Aacute"}, // Б - uppercase A, acute accent
        {"\u00C2", "Acirc"},  // В - uppercase A, circumflex accent
        {"\u00C3", "Atilde"}, // Г - uppercase A, tilde
        {"\u00C4", "Auml"},   // Д - uppercase A, umlaut
        {"\u00C5", "Aring"},  // Е - uppercase A, ring
        {"\u00C6", "AElig"},  // Ж - uppercase AE
        {"\u00C7", "Ccedil"}, // З - uppercase C, cedilla
        {"\u00C8", "Egrave"}, // И - uppercase E, grave accent
        {"\u00C9", "Eacute"}, // Й - uppercase E, acute accent
        {"\u00CA", "Ecirc"},  // К - uppercase E, circumflex accent
        {"\u00CB", "Euml"},   // Л - uppercase E, umlaut
        {"\u00CC", "Igrave"}, // М - uppercase I, grave accent
        {"\u00CD", "Iacute"}, // Н - uppercase I, acute accent
        {"\u00CE", "Icirc"},  // О - uppercase I, circumflex accent
        {"\u00CF", "Iuml"},   // П - uppercase I, umlaut
        {"\u00D0", "ETH"},    // Р - uppercase Eth, Icelandic
        {"\u00D1", "Ntilde"}, // С - uppercase N, tilde
        {"\u00D2", "Ograve"}, // Т - uppercase O, grave accent
        {"\u00D3", "Oacute"}, // У - uppercase O, acute accent
        {"\u00D4", "Ocirc"},  // Ф - uppercase O, circumflex accent
        {"\u00D5", "Otilde"}, // Х - uppercase O, tilde
        {"\u00D6", "Ouml"},   // Ц - uppercase O, umlaut
        {"\u00D7", "times"},  // Multiplication sign
        {"\u00D8", "Oslash"}, // Ш - uppercase O, slash
        {"\u00D9", "Ugrave"}, // Щ - uppercase U, grave accent
        {"\u00DA", "Uacute"}, // Ъ - uppercase U, acute accent
        {"\u00DB", "Ucirc"},  // Ы - uppercase U, circumflex accent
        {"\u00DC", "Uuml"},   // Ь - uppercase U, umlaut
        {"\u00DD", "Yacute"}, // Э - uppercase Y, acute accent
        {"\u00DE", "THORN"},  // Ю - uppercase THORN, Icelandic
        {"\u00DF", "szlig"},  // Я - lowercase sharps, German
        {"\u00E0", "agrave"}, // а - lowercase a, grave accent
        {"\u00E1", "aacute"}, // б - lowercase a, acute accent
        {"\u00E2", "acirc"},  // в - lowercase a, circumflex accent
        {"\u00E3", "atilde"}, // г - lowercase a, tilde
        {"\u00E4", "auml"},   // д - lowercase a, umlaut
        {"\u00E5", "aring"},  // е - lowercase a, ring
        {"\u00E6", "aelig"},  // ж - lowercase ae
        {"\u00E7", "ccedil"}, // з - lowercase c, cedilla
        {"\u00E8", "egrave"}, // и - lowercase e, grave accent
        {"\u00E9", "eacute"}, // й - lowercase e, acute accent
        {"\u00EA", "ecirc"},  // к - lowercase e, circumflex accent
        {"\u00EB", "euml"},   // л - lowercase e, umlaut
        {"\u00EC", "igrave"}, // м - lowercase i, grave accent
        {"\u00ED", "iacute"}, // н - lowercase i, acute accent
        {"\u00EE", "icirc"},  // о - lowercase i, circumflex accent
        {"\u00EF", "iuml"},   // п - lowercase i, umlaut
        {"\u00F0", "eth"},    // р - lowercase eth, Icelandic
        {"\u00F1", "ntilde"}, // с - lowercase n, tilde
        {"\u00F2", "ograve"}, // т - lowercase o, grave accent
        {"\u00F3", "oacute"}, // у - lowercase o, acute accent
        {"\u00F4", "ocirc"},  // ф - lowercase o, circumflex accent
        {"\u00F5", "otilde"}, // х - lowercase o, tilde
        {"\u00F6", "ouml"},   // ц - lowercase o, umlaut
        {"\u00F7", "divide"}, // Division sign
        {"\u00F8", "oslash"}, // ш - lowercase o, slash
        {"\u00F9", "ugrave"}, // щ - lowercase u, grave accent
        {"\u00FA", "uacute"}, // ъ - lowercase u, acute accent
        {"\u00FB", "ucirc"},  // ы - lowercase u, circumflex accent
        {"\u00FC", "uuml"},   // ь - lowercase u, umlaut
        {"\u00FD", "yacute"}, // э - lowercase y, acute accent
        {"\u00FE", "thorn"},  // ю - lowercase thorn, Icelandic
        {"\u00FF", "yuml"},   // я - lowercase y, umlaut
    };

    private static final int MIN_ESCAPE = 2;
    private static final int MAX_ESCAPE = 6;

    private static final HashMap<String, CharSequence> lookupMap;
    static {
        lookupMap = new HashMap<String, CharSequence>();
        for (final CharSequence[] seq : ESCAPES)
            lookupMap.put(seq[1].toString(), seq[0]);
    }

}

后来我发现当给空字符串作为参数时,这段代码会进入循环。当前版本已经修复了这个问题。 - Nick Frolov
这个是转义还是反转义?& 没有被解码。只有 & 被添加到映射中,所以它只能单向工作? - mjs
3
StringWriter 内部使用了一个使用锁的 StringBuffer。直接使用 StringBuilder 应该更快。 - Axel Dörfler
在上述代码中发现了一个错误,当遇到 "=" 即等于号时。应该将 writer.write(entityValue); 改为 writer.write(Character.toString((char)entityValue));。- Stevko 4小时前 - Stevko
4
@NickFrolov,你的评论似乎有点混乱。例如,auml代表的是ä而不是д - aioobe
显示剩余3条评论

17
以下的库也可用于Java中的HTML转义:unbescape
可以通过以下方式取消转义HTML:
final String unescapedText = HtmlEscape.unescapeHtml(escapedText); 

2
它对此没有任何作用:%3Chtml%3E%0D%0A%3Chead%3E%0D%0A%3Ctitle%3Etest%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%3E%0D%0Atest%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E - user1191027
45
您的文本并非HTML编码,而是URL编码。 - Mikhail Batcer

16

Spring框架HtmlUtils

如果您已经在使用Spring框架,请使用以下方法:

import static org.springframework.web.util.HtmlUtils.htmlUnescape;

...

String result = htmlUnescape(source);


12

这对我来说完成了工作。

import org.apache.commons.lang.StringEscapeUtils;
...
String decodedXML = StringEscapeUtils.unescapeHtml(encodedXML);

或者

import org.apache.commons.lang3.StringEscapeUtils;
...
String decodedXML = StringEscapeUtils.unescapeHtml4(encodedXML);

我想显然使用lang3总是更好的选择。


4
一个非常简单但效率低下且没有使用任何外部库的解决方案是:
public static String unescapeHtml3(String str) {
    try {
        HTMLDocument doc = new HTMLDocument();
        new HTMLEditorKit().read(new StringReader("<html><body>" + str), doc, 0);
        return doc.getText(1, doc.getLength());
    } catch(Exception ex) {
        return str;
    }
}

只有在需要解码的字符串数量很少时才应使用此方法。


1
非常接近,但不完全正确 - 它将 "qwAS12ƷƸDžǚǪǼȌ" 转换为 "qwAS12ƷƸDžǚǪǼȌ\n"。 - Greg

3
最可靠的方法是使用 标签。
String cleanedString = StringEscapeUtils.unescapeHtml4(originalString);

来自org.apache.commons.lang3.StringEscapeUtils
并且可以对空格进行转义。
cleanedString = cleanedString.trim();

这将确保在Web表单中因复制和粘贴而产生的空格不会在数据库中保存。

1

0
在我的情况下,我使用替换方法来测试每个变量中的每个实体。我的代码如下:
text = text.replace("&Ccedil;", "Ç");
text = text.replace("&ccedil;", "ç");
text = text.replace("&Aacute;", "Á");
text = text.replace("&Acirc;", "Â");
text = text.replace("&Atilde;", "Ã");
text = text.replace("&Eacute;", "É");
text = text.replace("&Ecirc;", "Ê");
text = text.replace("&Iacute;", "Í");
text = text.replace("&Ocirc;", "Ô");
text = text.replace("&Otilde;", "Õ");
text = text.replace("&Oacute;", "Ó");
text = text.replace("&Uacute;", "Ú");
text = text.replace("&aacute;", "á");
text = text.replace("&acirc;", "â");
text = text.replace("&atilde;", "ã");
text = text.replace("&eacute;", "é");
text = text.replace("&ecirc;", "ê");
text = text.replace("&iacute;", "í");
text = text.replace("&ocirc;", "ô");
text = text.replace("&otilde;", "õ");
text = text.replace("&oacute;", "ó");
text = text.replace("&uacute;", "ú");

在我的情况下,这个很有效。

2
这不是一个特殊的实体。甚至在问题中提到的两个也没有出现。 - Sandy Gifford
1
这样做不会很好地扩展。 - denov

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