有没有一种方法可以消除口音并将整个字符串转换为常规字母?


263

除了使用String.replaceAll()方法和逐个替换字母之外,还有没有更好的方法来消除重音并使这些字母规则化?例:

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

输出: orcpzsiayd

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

Answers:


387

使用java.text.Normalizer来处理这个给你。

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

这会将所有重音符号与字符分开。然后,您只需要比较每个字符和一个字母,然后扔掉那些不是字母。

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

如果您的文本使用unicode,则应改用以下代码:

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

对于unicode,\\P{M}匹配基本字形,\\p{M}(小写)匹配每个重音。

由于GarretWilson的指针和regular-expressions.info为伟大的Unicode指南。


7
每次都会编译正则表达式,如果您只需要一次,就可以了,但是如果您需要使用大量文本来执行此操作,则预编译正则表达式是一个成功。
戴维·康拉德

3
请注意,并非所有基于拉丁语的字母都分解为ASCII +重音符号。这将杀死。波兰语中使用的“带笔画的拉丁字母{大写,小}字母l”。
米哈尔Politowski

12
这是一个很好的方法,但是删除所有非ASCII字符是过大的,并且可能会删除其他不需要的内容。最好删除所有Unicode“标记”;包括非间距标记,空格/组合标记和封闭标记。您可以使用string.replaceAll("\\p{M}", "")。有关更多信息,请参见regular-expressions.info/unicode.html
Garret Wilson

4
您可能要使用Normalizer.Form.NFKD而不是NFD-NFKD会将连字之类的内容转换为ascii字符(例如,将fin转换为fi),而NFD不会这样做。
chesterm8 '17

2
@ chesterm8,有趣的是NFKD会将“ fi”转换为“ fi”,但并未将“Æ”转换为“ AE”。我想我必须调出Unicode数据来找出原因,但这不是我所期望的。
加勒·威尔逊

136

从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's)不适用于Ø或Ł。Apache Commons 3.5也不适用于Ø,但适用于Ł。阅读ØWikipedia文章后,我不确定是否应将其替换为“ O”:它是挪威语和丹麦语中的单独字母,在“ z”之后按字母顺序排列。这是“条带重音”方法局限性的一个很好的例子。


2
我看到有一个针对Ł @KarolS的错误报告。有人提交了请求请求,但该请求未通过某些测试,自去年7月以来未进行过更新。
DavidS

1
5天前已进行了更新,并且合并了合并请求。
EpicPandaForce's

6
Commons Lang 3.5是几天前发布的。我确认现在可以在works上使用。在Ø上不起作用。阅读Ø的Wiki文章时,我不确定是否应将其替换为“ O”:它是挪威语和丹麦语中的单独字母,并在“ z”之后按字母顺序排列。这是“条带重音”方法局限性的一个很好的例子。
DavidS

2
如果您不想包含该库,则可以从commons.apache.org/proper/commons-lang/apidocs/src-html/org/…的
lujop

2
作为丹麦人,丹麦语/挪威语ø就像法国语-以及德国/瑞典/匈牙利语/爱沙尼亚语等。ö是写oe的一种简短方法。因此,根据您的目的,这可能是您想要的替代品。
Ole VV

57

@ 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倍)。


2
outj在构造字符串对象之前,必须调整大小以匹配有效字符的数量。
Lefteris E

4
我对这个解决方案有异议。想象一下输入“æøåá”。当前flattenToAscii会创建结果“ aa ..”,其中点表示\ u0000。这是不好的。第一个问题是-如何表示“无法标准化”的字符?假设它是?,或者我们可以在此处保留NULL char,但是无论如何我们都必须保留它们的正确位置(就像regex解决方案一样)。为此,if循环中的内容必须类似于:if (c <= '\u007F') out[j++] = c; else if (Character.isLetter(c)) out[j++] = '?';它将使速度变慢,但首先必须正确。;-)
virgo47

在我的最后一条评论中(很遗憾,他们不能再写更长的时间了)-也许正数(isLetter)不是正确的选择,但我没有发现更好的选择。我不是Unicode专家,所以我不知道如何更好地识别替换原始字符的单个字符的类。对于大多数应用程序/用法,字母都可以正常工作。
virgo47

1
您可能要使用Normalizer.Form.NFKD而不是NFD-NFKD会将连字之类的内容转换为ascii字符(例如,将fin转换为fi),而NFD不会这样做。
chesterm8 '17

2
对于我们来说,我们希望完全删除该字符。为了确保没有尾随空字符,我使用了另一个String构造函数将其删除:return new String(out,0,j);
Mike Samaras

30

编辑:如果您不拘泥于Java <6,并且速度不是很严格,并且/或者转换表太有限,请使用David的答案。关键是要使用Normalizer(在Java 6中引入)而不是循环内的转换表。

虽然这不是“完美”的解决方案,但是当您知道范围(在我们的情况下为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在我的硬件上进行的测试表明,该程序可以在约100ms的时间内将àèéľšťč89FDČ转换为aeelstc89FDC 100万次,而Normalizer的转换速度为3.7s(慢37倍)。如果您的需求围绕性能,并且您知道输入范围,那么这可能适合您。

请享用 :-)


1
建议版本的许多速度慢是由于正则表达式引起的,而不是归因于Normalizer。使用规范化程序但手动删除非ASCII字符的速度更快,尽管仍不如您的版本快。但是它适用于所有Unicode,而不仅仅是latin1和latin2。
戴维·康拉德

我将其扩展为可以使用更多字符,pastebin.com/FAAm6a2j,请注意,它不适用于multi(DZ)等多字符字符。它将仅产生1个字符。而且我的函数使用char而不是字符串,如果无论如何都要处理char,这将更快,因此您不必进行转换。
James T

嘿,我不明白tab00c0字段上的那些字母代表什么?例如“ AAAAAAACEEEEIIII”或“ lLlNnNnNnnNnOooo”等。以前从未见过。你在哪里找到他们?另外,为什么不只使用核心响应代码?
ThanosFisherman 2014年

@ThanosF只是尝试遍历代码(如果需要,请使用调试器)。这是针对字符串中的每个字符执行的操作:“此字符在\ u00c0和\ u017f之间吗?如果是,则用表中的7位ASCII字符替换它。” 该表仅涵盖了两个编码页(拉丁1和2)及其7位等效项。因此,如果它是带有代码\ u00e0(à)的字符,它将从表的第32位(e0-c0 = 32)采用其7位近似值-即“ a”。有些字符不是字母,那些字符和代码留在那里。
virgo47,2014年

感谢您的解释。在哪里可以找到这些编码页面,以便可以将此变量扩展到我的语言?(希腊语)接受的答案已经可以代替希腊的带重音符号的字母,但我也想尝试您的方法并运行一些基准测试:)
ThanosFisherman 2014年

22
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可以正常工作,但是我在使用JRockit(1.6.0_29 64b)的Linux(64b)上遇到了此问题。无法通过任何其他设置进行确认,无法确认核对,但我可以确认其他建议的解决方案有效,对此我投了赞成票。:-)(顺便说一句(顺便说一句:它做了一些替换,但还不够,例如,将Ú更改为U,但没有将á更改为a。)
virgo47

1
您可能要使用Normalizer.Form.NFKD而不是NFD-NFKD会将连字之类的内容转换为ascii字符(例如,将fin转换为fi),而NFD不会这样做。
chesterm8 '17

@KarolS我没有看到他们的任何含有任何口音
EIS

@eis字母上的斜线算作变音符号:en.wikipedia.org/wiki/Diacritic如果您在Wikipedia页面上对“重音”进行更严格的定义,则变音符号不是重音,因此Nico的回答是还是错的。
Karol S

6

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

https://zh.wikipedia.org/wiki/Diacritic#Languages_with_letters_ contains_diacritics

“波斯尼亚语和克罗地亚语的符号分别是č,ć,đ,š和ž,它们被认为是单独的字母,并且在字典和其他上下文中也按字母顺序列出,因此也是如此。”

删除它们可能会从本质上改变单词的含义,或将字母更改为完全不同的字母。


5
同意 例如瑞典语:“höra”(听)->“ hora”(人)
ChristofferHammarström2010年

14
不管它们是什么意思。问题是如何删除它们。
艾里克·罗伯逊

7
埃里克:他们叫什么很重要。如果问题询问如何去除口音,而这些不是口音,那么答案可能不只是如何去除所有看起来像口音的东西。尽管这可能只是评论而不是答案。
Smig

4
我认为通常的用例是搜索,尤其是混合语言的搜索,通常使用英语键盘作为输入,在这种情况下,获得假肯定胜于假否定更好。
nilskp 2014年

3

我曾经遇到过与字符串相等性检查相关的相同问题,比较字符串之一具有 ASCII字符代码128-255

即,不间断空格-[Hex-A0]空间[Hex-20]。在HTML上显示不间断空间。我已经使用了以下内容spacing entities。他们的性格及其字节就像&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()));

输出以字节为单位:

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();
}
  • for Java的Unicode字符串的ASCII音译。 unidecode

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

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

    对于空间的 URL编码,请使用Guava图书馆。

    String encodedString = UrlEscapers.urlFragmentEscaper().escape(inputString);
  • ➩为了克服这个问题使用String.replaceAll()了一些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(" ", " ");
  • ➩使用java.text.Normalizer.Form。该枚举提供了Unicode标准附件#15 — Unicode规范化形式中描述的四种Unicode规范化形式的常量,以及访问它们的两种方法。

    在此处输入图片说明

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

使用不同的方法(如Unidecode,Normalizer,StringUtils)测试String和输出。

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

使用Unidecodebest choice我的最终代码,如下所示。

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

}


2

@David Conrad解决方案是我尝试使用Normalizer最快的解决方案,但确实存在错误。它基本上会去除不带重音符号的字符,例如汉字和其他字母(如æ)都被去除。我们要剥离的字符是非空格标记,这些字符在最终字符串中不会占用额外的宽度。这些零宽度字符基本上最终以其他字符组合在一起。如果您可以看到它们被孤立为一个字符(例如,像`这样的字符),那么我猜是它与空格字符结合在一起了。

public static String flattenToAscii(String string) {
    char[] out = new char[string.length()];
    String norm = Normalizer.normalize(string, Normalizer.Form.NFD);

    int j = 0;
    for (int i = 0, n = norm.length(); i < n; ++i) {
        char c = norm.charAt(i);
        int type = Character.getType(c);

        //Log.d(TAG,""+c);
        //by Ricardo, modified the character check for accents, ref: http://stackoverflow.com/a/5697575/689223
        if (type != Character.NON_SPACING_MARK){
            out[j] = c;
            j++;
        }
    }
    //Log.d(TAG,"normalized string:"+norm+"/"+new String(out));
    return new String(out);
}

1

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

    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}模式。

问候。


0

我认为最好的解决方案是将每个字符转换为十六进制并用另一个十六进制替换。这是因为有2种Unicode类型:

Composite Unicode
Precomposed Unicode

例如,复合Unicode编写的“Ồ”与预组合Unicode编写的“Ồ”不同。您可以复制我的示例字符并将其转换以查看区别。

In Composite Unicode, "Ồ" is combined from 2 char: Ô (U+00d4) and ̀ (U+0300)
In Precomposed Unicode, "Ồ" is single char (U+1ED2)

我已经为某些银行开发了此功能,以便在将信息发送到核心银行之前转换信息(通常不支持Unicode),并且当最终用户使用多种Unicode类型输入数据时会遇到此问题。因此,我认为转换为十六进制并替换它是最可靠的方法。


-1

万一有人在Kotlin中苦苦挣扎,此代码的工作原理就像一个魅力。为避免不一致,我还使用.toUpperCase和Trim()。然后我强制转换此功能:

   fun stripAccents(s: String):String{

   if (s == null) {
      return "";
   }

val chars: CharArray = s.toCharArray()

var sb = StringBuilder(s)
var cont: Int = 0

while (chars.size > cont) {
    var c: kotlin.Char
    c = chars[cont]
    var c2:String = c.toString()
   //these are my needs, in case you need to convert other accents just Add new entries aqui
    c2 = c2.replace("Ã", "A")
    c2 = c2.replace("Õ", "O")
    c2 = c2.replace("Ç", "C")
    c2 = c2.replace("Á", "A")
    c2 = c2.replace("Ó", "O")
    c2 = c2.replace("Ê", "E")
    c2 = c2.replace("É", "E")
    c2 = c2.replace("Ú", "U")

    c = c2.single()
    sb.setCharAt(cont, c)
    cont++

}

return sb.toString()

}

使用这些有趣的代码如下:

     var str: String
     str = editText.text.toString() //get the text from EditText
     str = str.toUpperCase().trim()

     str = stripAccents(str) //call the function
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.