有没有一种方法可以去除重音符号并将整个字符串转换为普通字母?

323

除了使用String.replaceAll()方法并逐个替换字母外,是否有更好的方法来去除重音并使这些字母规范化?

例如:

输入:orčpžsíáýd

输出:orcpzsiayd

它不需要包括所有带重音符号的字母,如俄语或中文字母。

15个回答

456

使用 java.text.Normalizer 来处理这个问题。

string = Normalizer.normalize(string, Normalizer.Form.NFD);
// or Normalizer.Form.NFKD for a more "compatible" deconstruction 

这将从字符中分离出所有的重音符号。然后,您只需要将每个字符与字母进行比较并丢弃不是字母的字符。

string = string.replaceAll("[^\\p{ASCII}]", "");

如果您的文本是Unicode编码,则应改用此方法:

string = string.replaceAll("\\p{M}", "");

对于Unicode,\\P{M}匹配基本字形,而\\p{M}(小写)匹配每个重音符号。
感谢GarretWilson的指引和regular-expressions.info提供的出色Unicode指南。

7
如果您只需要使用一次正则表达式,每次都编译它也没关系。但如果您需要对大量文本进行操作,则预编译正则表达式更有效。请注意不要改变原意。 - David Conrad
3
请注意,并非所有基于拉丁字母的字母都可以分解为ASCII码和重音符号。这会导致一些问题,比如在波兰语中使用的“带划线拉丁大/小写字母l”。 - Michał Politowski
12
这是一个不错的方法,但删除所有非 ASCII 字符可能有些过头了,并且可能会删除一些你不希望删除的内容,正如其他人所指出的那样。最好删除所有 Unicode "标记",包括非间隔标记、间隔/组合标记和封闭标记。您可以使用string.replaceAll("\\p{M}", "")来完成此操作。有关更多信息,请参见 http://www.regular-expressions.info/unicode.html。 - Garret Wilson
5
你可能需要使用Normalizer.Form.NFKD而不是NFD - NFKD会将连字号之类的字符转换为ASCII字符(例如,将fi转换为fi),而NFD则不会这样做。 - chesterm8
4
有趣的是,NFKD将"fi"转换为"fi",但它没有将"Æ"转换为"AE"。我想我会查阅Unicode数据来找出原因,但这不是我预期的结果。 - Garret Wilson
显示剩余3条评论

203

从2011年开始,你可以使用Apache Commons StringUtils.stripAccents(input) (自3.0版本起):

    String input = StringUtils.stripAccents("Tĥïŝ ĩš â fůňķŷ Šťŕĭńġ");
    System.out.println(input);
    // Prints "This is a funky String"

注意:

被接受的答案(Erick Robertson的)不能处理 Ø 或 Ł。Apache Commons 3.5也不能处理 Ø,但是它可以处理Ł。阅读了关于Ø的维基百科文章后,我不确定是否应该用“O”替换它:在挪威和丹麦,Ø是一个单独的字母,在“z”之后排列。这是“去除重音”方法的限制很好的例子。


2
如果您不想包含该库,可以轻松地从源代码中获取涉及该功能的两种方法,网址为https://commons.apache.org/proper/commons-lang/apidocs/src-html/org/apache/commons/lang3/StringUtils.html。 - lujop
3
作为丹麦人,丹麦/挪威语中的ø、法语中的œ以及德语/瑞典语/匈牙利语/爱沙尼亚语等中的ö都源自于简写“oe”的方式。因此,根据您的目的,这可能是您想要的替代方案。 - Ole V.V.

64

@virgo47的解决方案非常快,但是是近似的。被接受的答案使用了Normalizer和正则表达式。我想知道Normalizer和正则表达式哪一部分占用了时间,因为可以不使用正则表达式来删除所有非ASCII字符:

import java.text.Normalizer;

public class Strip {
    public static String flattenToAscii(String string) {
        StringBuilder sb = new StringBuilder(string.length());
        string = Normalizer.normalize(string, Normalizer.Form.NFD);
        for (char c : string.toCharArray()) {
            if (c <= '\u007F') sb.append(c);
        }
        return sb.toString();
    }
}

通过将数据写入char []而不调用toCharArray()可以获得小的额外加速,尽管我不确定代码清晰度的降低是否值得:

public static String flattenToAscii(String string) {
    char[] out = new char[string.length()];
    string = Normalizer.normalize(string, Normalizer.Form.NFD);
    int j = 0;
    for (int i = 0, n = string.length(); i < n; ++i) {
        char c = string.charAt(i);
        if (c <= '\u007F') out[j++] = c;
    }
    return new String(out);
}

这种变体的优点是使用Normalizer的正确性和使用表格的一些速度。在我的机器上,这个版本比被接受的答案快4倍左右,比@virgo47的慢6.6倍到7倍(被接受的答案在我的机器上比@virgo47的慢26倍左右)。


3
在使用 out 构建字符串对象之前,必须将其重新调整大小以匹配有效字符数 j - Lefteris E
4
我对这个解决方案有异议。想象一下输入“æøåá”。当前的flattenToAscii创建了结果“aa..”,其中点表示\u0000。这不好。第一个问题是如何表示“无法规范化”的字符?假设它将是“?”或者我们可以在那里留下NULL字符,但无论如何,我们都必须保留这些字符的正确位置(就像正则表达式解决方案所做的那样)。为此,在循环中的if语句必须像这样:if (c <= '\u007F') out[j++] = c; else if (Character.isLetter(c)) out[j++] = '?'; 它会使它变慢一些,但首先必须是正确的。;-) - virgo47
以我的最后一条评论为例(太糟糕了,它们不能更长)-也许正面的想法(isLetter)不是正确的选择,但我没有找到更好的。我不是Unicode专家,所以我不知道如何更好地识别代替原始字符的单个字符的类别。对于大多数应用/用途,字母运作良好。 - virgo47
最后,这个解决方案(带修复)与正则表达式版本不会产生相同的输出。这是因为正则表达式版本将这种字符(如ø)保留为原样。从这个意义上说,即使在这些边角情况下,这个答案至少不会留下任何非ASCII字符(这是预期的结果)。因此,最终这似乎是最正确的解决方案。当然,应用了我的建议的修复程序,所以字母的位置是正确的,无论替换字符(?)是什么。 - virgo47
2
你可能想使用Normalizer.Form.NFKD而不是NFD - NFKD会将像连字号这样的东西转换为ASCII字符(例如fi到fi),而NFD则不会这样做。 - chesterm8
2
对我们来说,我们希望完全删除该字符。为确保没有尾随的空字符,我使用另一种String构造函数将它们移除: 返回 new String(out, 0, j); - Mike Samaras

33

编辑:如果你不受限于Java <6且速度不是关键,或者翻译表太受限制,请使用David的答案。关键是在循环内部使用Java 6中引入的Normalizer而不是翻译表。

虽然这不是“完美”的解决方案,但在你知道范围(在我们的情况下是Latin1、2)时效果很好,在Java 6之前也适用(尽管不是真正的问题),并且比大多数建议的版本要快得多(这可能是一个问题,也可能不是)。

    /**
 * Mirror of the unicode table from 00c0 to 017f without diacritics.
 */
private static final String tab00c0 = "AAAAAAACEEEEIIII" +
    "DNOOOOO\u00d7\u00d8UUUUYI\u00df" +
    "aaaaaaaceeeeiiii" +
    "\u00f0nooooo\u00f7\u00f8uuuuy\u00fey" +
    "AaAaAaCcCcCcCcDd" +
    "DdEeEeEeEeEeGgGg" +
    "GgGgHhHhIiIiIiIi" +
    "IiJjJjKkkLlLlLlL" +
    "lLlNnNnNnnNnOoOo" +
    "OoOoRrRrRrSsSsSs" +
    "SsTtTtTtUuUuUuUu" +
    "UuUuWwYyYZzZzZzF";

/**
 * Returns string without diacritics - 7 bit approximation.
 *
 * @param source string to convert
 * @return corresponding string without diacritics
 */
public static String removeDiacritic(String source) {
    char[] vysl = new char[source.length()];
    char one;
    for (int i = 0; i < source.length(); i++) {
        one = source.charAt(i);
        if (one >= '\u00c0' && one <= '\u017f') {
            one = tab00c0.charAt((int) one - '\u00c0');
        }
        vysl[i] = one;
    }
    return new String(vysl);
}

我的硬件上使用32位JDK进行测试表明,该方法将从àèéľšťč89FDČ转换为aeelstc89FDC,在1百万次内约需要100毫秒,而Normalizer方式则需要3.7秒(慢37倍)。如果您的需求是关于性能,并且您了解输入范围,那么这个方法可能适合您。

祝您愉快 :-)


1
建议版本的许多缓慢是由于正则表达式而不是规范化器。使用规范化器但手动删除非ASCII字符会更快,尽管仍然不如您的版本快。但它适用于Unicode的所有内容,而不仅仅是Latin1和Latin2。 - David Conrad
1
我扩展了这个程序以处理更多字符,http://pastebin.com/FAAm6a2j。请注意,它无法正确处理多字符(如DŽ(DZ)),只会生成一个字符。此外,我的函数使用char而不是字符串,在处理char时更快,因此您不必进行转换。 - James T
此解决方案无法将字符“Ệệ”转换为“Ee”。 - thuanle
@thuanle 这些是Latin 1还是2?看起来这些是U+1ec6/7,这已经超出了我的解决方案的范围。所以是的,它不支持这些字符,并且我在答案中明确说明了这一点。 - virgo47
@virgo47 感谢您的回答。 它是0x1EFF,在范围0x1E00 - 0x1EFF内:拉丁扩展附加。 那么,我们能否进行任何修改以使其与拉丁扩展附加一起工作? - thuanle
显示剩余6条评论

28
System.out.println(Normalizer.normalize("àèé", Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", ""));

这对我有用。上面示例的输出给出了我想要的"aee",但是

System.out.println(Normalizer.normalize("àèé", Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", ""));

没有进行任何替换。


1
确认一下...通常ASCII工作得很好,但我在Linux(64b)上遇到了这个问题,使用JRockit(1.6.0_29 64b)。无法证实任何其他设置是否存在相似情况,但我可以确认另一个建议的解决方案起作用了,因此我赞成这个方案。 (顺便说一句:它确实进行了一些替换,但不够彻底,例如将Ú更改为U,但没有将á更改为a。) - virgo47
1
你可能想使用Normalizer.Form.NFKD而不是NFD - NFKD会将像连字号这样的字符转换为ASCII字符(例如fi到fi),而NFD则不会这样做。 - chesterm8
@KarolS,我没有看到它们中任何一个包含重音符号 - eis
@eis 字母上的斜杠被视为变音符号:https://en.wikipedia.org/wiki/Diacritic, 如果您按照维基百科页面上对“重音符”的更严格定义,那么分音符不是重音符,因此Nico的答案仍然是错误的。 - Karol S

6

根据语言的不同,这些可能不被视为重音(改变字母的发音),而是变音符号。

https://en.wikipedia.org/wiki/Diacritic#Languages_with_letters_containing_diacritics

"Bosnian and Croatian have the symbols č, ć, đ, š and ž, which are considered separate letters and are listed as such in dictionaries and other contexts in which words are listed according to alphabetical order."
"去除它们可能会本质上改变单词的含义,或将这些字母变成完全不同的字母。"

5
同意。例如在瑞典语中,“höra”(听)-> “hora”(妓女)。 - Christoffer Hammarström
21
不管它们意味着什么,问题是如何移除它们。 - Erick Robertson
9
Erick:它们的名称很重要。如果问题问如何去除重音符号,但如果那些不是重音符号,那么答案可能不仅仅是如何删除所有看起来像重音符号的东西。虽然这可能应该是评论而不是答案。 - Smig
6
我认为这个的正常使用情况是搜索,特别是搜索混合语言,通常使用英文键盘作为输入,在这种情况下,得到假阳性比得到假阴性更好。 - nilskp
@Smig不管它们被称为什么,Erick是正确的,因为它根本没有试图回答所问的问题,所以它与问题无关。应该作为评论。 - Hasen

4
我曾经遇到过与字符串相等性检查相关的问题,其中一个比较的字符串含有 ASCII字符代码128-255

i.e., Non-breaking space - [Hex - A0] Space [Hex - 20]. To show Non-breaking space over HTML. I have used the following spacing entities. Their character and its bytes are like &emsp is very wide space[ ]{-30, -128, -125}, &ensp is somewhat wide space[ ]{-30, -128, -126}, &thinsp is narrow space[ ]{32} , Non HTML Space {}

String s1 = "My Sample Space Data", s2 = "My Sample Space Data";
System.out.format("S1: %s\n", java.util.Arrays.toString(s1.getBytes()));
System.out.format("S2: %s\n", java.util.Arrays.toString(s2.getBytes()));

Output in Bytes:

S1: [77, 121, 32, 83, 97, 109, 112, 108, 101, 32, 83, 112, 97, 99, 101, 32, 68, 97, 116, 97] S2: [77, 121, -30, -128, -125, 83, 97, 109, 112, 108, 101, -30, -128, -125, 83, 112, 97, 99, 101, -30, -128, -125, 68, 97, 116, 97]

使用以下代码来获取不同空格及其字节码: wiki for List_of_Unicode_characters

String spacing_entities = "very wide space,narrow space,regular space,invisible separator";
System.out.println("Space String :"+ spacing_entities);
byte[] byteArray = 
    // spacing_entities.getBytes( Charset.forName("UTF-8") );
    // Charset.forName("UTF-8").encode( s2 ).array();
    {-30, -128, -125, 44, -30, -128, -126, 44, 32, 44, -62, -96};
System.out.println("Bytes:"+ Arrays.toString( byteArray ) );
try {
    System.out.format("Bytes to String[%S] \n ", new String(byteArray, "UTF-8"));
} catch (UnsupportedEncodingException e) {
    e.printStackTrace();
}
  • ➩ ASCII transliterations of Unicode string for Java. unidecode

    String initials = Unidecode.decode( s2 );
    
  • ➩ using Guava: Google Core Libraries for Java.

    String replaceFrom = CharMatcher.WHITESPACE.replaceFrom( s2, " " );
    

    For URL encode for the space use Guava laibrary.

    String encodedString = UrlEscapers.urlFragmentEscaper().escape(inputString);
    
  • ➩ To overcome this problem used String.replaceAll() with some RegularExpression.

    // \p{Z} or \p{Separator}: any kind of whitespace or invisible separator.
    s2 = s2.replaceAll("\\p{Zs}", " ");
    
    
    s2 = s2.replaceAll("[^\\p{ASCII}]", " ");
    s2 = s2.replaceAll(" ", " ");
    
  • ➩ Using java.text.Normalizer.Form. This enum provides constants of the four Unicode normalization forms that are described in Unicode Standard Annex #15 — Unicode Normalization Forms and two methods to access them.

    enter image description here

    s2 = Normalizer.normalize(s2, Normalizer.Form.NFKC);
    

测试字符串和不同方法的输出,如 ➩ Unidecode,正规化器,StringUtils

String strUni = "Tĥïŝ ĩš â fůňķŷ Šťŕĭńġ Æ,Ø,Ð,ß";

// This is a funky String AE,O,D,ss
String initials = Unidecode.decode( strUni );

// Following Produce this o/p: Tĥïŝ ĩš â fůňķŷ Šťŕĭńġ Æ,Ø,Ð,ß
String temp = Normalizer.normalize(strUni, Normalizer.Form.NFD);
Pattern pattern = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");
temp = pattern.matcher(temp).replaceAll("");

String input = org.apache.commons.lang3.StringUtils.stripAccents( strUni );

使用Unidecode最佳选择,下面是我的最终代码。

public static void main(String[] args) {
    String s1 = "My Sample Space Data", s2 = "My Sample Space Data";
    String initials = Unidecode.decode( s2 );
    if( s1.equals(s2)) { //[ , ] %A0 - %2C - %20 « http://www.ascii-code.com/
        System.out.println("Equal Unicode Strings");
    } else if( s1.equals( initials ) ) {
        System.out.println("Equal Non Unicode Strings");
    } else {
        System.out.println("Not Equal");
    }

}

4

由于该解决方案已经在Maven RepositoryStringUtils.stripAccents()中提供,并且像@DavidS所提到的那样对于Ł有效。

但我需要它可以处理Ø和Ł。 因此进行了修改,如下所示。 可能对他人也有帮助。

更新


这是StringUtils.stripAccents(String obj)的修改版本,包含旧功能以及处理Ø和Ł字符的功能。

public static String stripAccents(final String input) {
    if (input == null) {
        return null;
    }
    final StringBuilder decomposed = new StringBuilder(Normalizer.normalize(input, Normalizer.Form.NFD));
    for (int i = 0; i < decomposed.length(); i++) {
        if (decomposed.charAt(i) == '\u0141') {
            decomposed.setCharAt(i, 'L');
        } else if (decomposed.charAt(i) == '\u0142') {
            decomposed.setCharAt(i, 'l');
        }else if (decomposed.charAt(i) == '\u00D8') {
            decomposed.setCharAt(i, 'O');
        }else if (decomposed.charAt(i) == '\u00F8') {
            decomposed.setCharAt(i, 'o');
        }
    }
    // Note that this doesn't correctly remove ligatures...
    return Pattern.compile("\\p{InCombiningDiacriticalMarks}+").matcher(decomposed).replaceAll("");
}

输入字符串 Ł Tĥïŝ ĩš â fůňķŷ Šťŕĭńġ Ø ø
输出字符串 L This is a funky String O o


我得到了一个输出为 "̊� TĥïÅ� Ä©Å¡ â fůňķŷ Šťŕĭńġ Ø ø" 的结果。 - user812142
不确定您使用了哪些输入数据,可能是Normalizer.Form.NFC、NFKC、NFKD。您也可以尝试以下内容:例如https://docs.oracle.com/javase/7/docs/api/java/text/Normalizer.Form.html#NFC - Ashish
我应用了相同的输入 Ł Tĥïŝ ĩš â fůňķŷ Šťŕĭńġ Ø ø。 - user812142

4
我建议使用Junidecode。它不仅可以处理'Ł'和'Ø',而且对于从其他字母表(如中文)转录为拉丁字母表也非常有效。

1
看起来很有前途,但我希望这个项目能更加活跃/维护,并且在 Maven 上可用。 - Phil
感谢@OlgaMaciaszek分享这个很棒的库。 - baderkhane
@Phil,它也可以在Maven中使用 https://search.maven.org/artifact/net.gcardone.junidecode/junidecode/0.4.1/jar - baderkhane
@OlgaMaciaszek 我该如何将这个库包含或导入到Talend/Java中? - Alexander Dixon
@OlgaMaciaszek 将这个库导入到Talend作业中的方式是通过tLibraryLoad组件。将其连接到tJava组件后,在高级设置中添加以下行import static net.gcardone.junidecode.Junidecode.*;。从那里,您可以调用该方法来转换您的字符串。 - Alexander Dixon

3

如果你没有库的话,使用正则表达式和规范化是最好的方法之一:

    public String flattenToAscii(String s) {
                if(s == null || s.trim().length() == 0)
                        return "";
                return Normalizer.normalize(s, Normalizer.Form.NFD).replaceAll("[\u0300-\u036F]", "");
}

这比使用replaceAll("[^\p{ASCII}]", "")更有效,如果你不需要重音符号(就像你的例子一样)。
否则,你必须使用p{ASCII}模式。
祝好。

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