从Unicode字符中删除变音符号(ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ)。

91

我正在研究一种算法,能够将带有变音符号(波浪符抑扬符插入符分音符舌音符)和它们的“简单”字符相互映射。

例如:

ń  ǹ  ň  ñ  ṅ  ņ  ṇ  ṋ  ṉ  ̈  ɲ  ƞ ᶇ ɳ ȵ  --> n
á --> a
ä --> a
ấ --> a
ṏ --> o

等等。

  1. 虽然我怀疑它应该与Unicode有关,并且在任何语言中都应该相对容易实现,但我想要用Java来实现这个功能。

  2. 目的:允许轻松搜索带有变音符号的单词。例如,如果我有一个网球选手数据库,并输入Björn_Borg,则也会保留Bjorn_Borg,以便我可以在某人输入Bjorn而不是Björn时找到它。


这取决于你正在编程的环境,尽管你可能需要手动维护某种映射表。那么,你使用的是哪种语言? - Thorarin
15
请注意,像ñ这样的字母 http://en.wikipedia.org/wiki/%C3%91 不应该去掉它们的变音符号以进行搜索。Google可以正确地区分西班牙语中的“ano”(肛门)和“año”(年)。因此,如果你真的想要一个好的搜索引擎,就不能仅仅依赖于基本的变音符号去除。 - Eduardo
1
@Eduardo:在特定的情境下,这可能并不重要。以OP提供的例子为例,在跨国环境中搜索一个人的名字,实际上你不希望搜索结果过于准确。 - Amir Abiri
然而,可以将变音符号映射到它们的语音等效物以改善语音搜索。例如,如果底层搜索引擎支持基于语音的(例如soundex)搜索,则ñ => ni将产生更好的结果。 - Amir Abiri
一种使用情况是将“año”更改为“ano”等,以便于URL、ID等的非Base64字符剥离。 - Ondra Žižka
Apache.commons库中的StringUtils类有一个stripAccents方法,它的效果非常好。 https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/StringUtils.html - Guilherme Guini
12个回答

87
我最近用Java完成了这个任务。
public static final Pattern DIACRITICS_AND_FRIENDS
    = Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}\\u0591-\\u05C7]+");

private static String stripDiacritics(String str) {
    str = Normalizer.normalize(str, Normalizer.Form.NFD);
    str = DIACRITICS_AND_FRIENDS.matcher(str).replaceAll("");
    return str;
}

这样做就按照你的要求来处理:
stripDiacritics("Björn")  = Bjorn

但是它在例如Białystok这样的情况下会失败,因为字符ł不是附加符号。
如果你想要一个完整的字符串简化器,你需要进行第二轮清理,处理一些不是附加符号的特殊字符。在这个映射中,我已经包含了出现在我们客户名称中最常见的特殊字符。这不是一个完整的列表,但它会给你一个扩展的思路。immutableMap只是来自google-collections的一个简单类。
public class StringSimplifier {
    public static final char DEFAULT_REPLACE_CHAR = '-';
    public static final String DEFAULT_REPLACE = String.valueOf(DEFAULT_REPLACE_CHAR);
    private static final ImmutableMap<String, String> NONDIACRITICS = ImmutableMap.<String, String>builder()

        //Remove crap strings with no sematics
        .put(".", "")
        .put("\"", "")
        .put("'", "")

        //Keep relevant characters as seperation
        .put(" ", DEFAULT_REPLACE)
        .put("]", DEFAULT_REPLACE)
        .put("[", DEFAULT_REPLACE)
        .put(")", DEFAULT_REPLACE)
        .put("(", DEFAULT_REPLACE)
        .put("=", DEFAULT_REPLACE)
        .put("!", DEFAULT_REPLACE)
        .put("/", DEFAULT_REPLACE)
        .put("\\", DEFAULT_REPLACE)
        .put("&", DEFAULT_REPLACE)
        .put(",", DEFAULT_REPLACE)
        .put("?", DEFAULT_REPLACE)
        .put("°", DEFAULT_REPLACE) //Remove ?? is diacritic?
        .put("|", DEFAULT_REPLACE)
        .put("<", DEFAULT_REPLACE)
        .put(">", DEFAULT_REPLACE)
        .put(";", DEFAULT_REPLACE)
        .put(":", DEFAULT_REPLACE)
        .put("_", DEFAULT_REPLACE)
        .put("#", DEFAULT_REPLACE)
        .put("~", DEFAULT_REPLACE)
        .put("+", DEFAULT_REPLACE)
        .put("*", DEFAULT_REPLACE)

        //Replace non-diacritics as their equivalent characters
        .put("\u0141", "l") // BiaLystock
        .put("\u0142", "l") // Bialystock
        .put("ß", "ss")
        .put("æ", "ae")
        .put("ø", "o")
        .put("©", "c")
        .put("\u00D0", "d") // All Ð ð from http://de.wikipedia.org/wiki/%C3%90
        .put("\u00F0", "d")
        .put("\u0110", "d")
        .put("\u0111", "d")
        .put("\u0189", "d")
        .put("\u0256", "d")
        .put("\u00DE", "th") // thorn Þ
        .put("\u00FE", "th") // thorn þ
        .build();


    public static String simplifiedString(String orig) {
        String str = orig;
        if (str == null) {
            return null;
        }
        str = stripDiacritics(str);
        str = stripNonDiacritics(str);
        if (str.length() == 0) {
            // Ugly special case to work around non-existing empty strings
            // in Oracle. Store original crapstring as simplified.
            // It would return an empty string if Oracle could store it.
            return orig;
        }
        return str.toLowerCase();
    }

    private static String stripNonDiacritics(String orig) {
        StringBuilder ret = new StringBuilder
        String lastchar = null;
        for (int i = 0; i < orig.length(); i++) {
            String source = orig.substring(i, i + 1);
            String replace = NONDIACRITICS.get(source);
            String toReplace = replace == null ? String.valueOf(source) : replace;
            if (DEFAULT_REPLACE.equals(lastchar) && DEFAULT_REPLACE.equals(toReplace)) {
                toReplace = "";
            } else {
                lastchar = toReplace;
            }
            ret.append(toReplace);
        }
        if (ret.length() > 0 && DEFAULT_REPLACE_CHAR == ret.charAt(ret.length() - 1)) {
            ret.deleteCharAt(ret.length() - 1);
        }
        return ret.toString();
    }

/*
    Special regular expression character ranges relevant for simplification:
    - InCombiningDiacriticalMarks: diacritic marks used in many languages
    - IsLm: Letter, Modifier (see http://www.fileformat.info/info/unicode/category/Lm/list.htm)
    - IsSk: Symbol, Modifier (see http://www.fileformat.info/info/unicode/category/Sk/list.htm)
    - U+0591 to U+05C7: Range for Hebrew diacritics (niqqud) 
      (see official Unicode chart: https://www.unicode.org/charts/PDF/U0590.pdf)
*/
public static final Pattern DIACRITICS_AND_FRIENDS = Pattern.compile(
    "[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}\\u0591-\\u05C7]+"
);


    private static String stripDiacritics(String str) {
        str = Normalizer.normalize(str, Normalizer.Form.NFD);
        str = DIACRITICS_AND_FRIENDS.matcher(str).replaceAll("");
        return str;
    }
}

2
就像我在问题评论中所说的那样。如果你想要一个好的搜索引擎,就不能依赖于基本的变音符号去除。 - Eduardo
3
谢谢 Andreas,非常好用!(在 r̀r̂r̃r̈rʼŕřt̀t̂ẗţỳỹẙyʼy̎ýÿŷp̂p̈s̀s̃s̈s̊sʼs̸śŝŞşšd̂d̃d̈ďdʼḑf̈f̸g̀g̃g̈gʼģq́ĝǧḧĥj̈jʼḱk̂k̈k̸ǩl̂l̃l̈Łłẅẍc̃c̈c̊cʼc̸Çççćĉčv̂v̈vʼv̸b́b̧ǹn̂n̈n̊nʼńņňñm̀m̂m̃m̈m̊m̌ǵß):-) - Fortega
太好了,谢谢,非常有用,但对我来说只有这种方式有效:"(\p{InCombiningDiacriticalMarks}+);" 保留其他括号会导致崩溃!但对我来说解决了问题,再次感谢。 - Alexandre
请注意,Unicode 的任何规范化形式(NFC、NFKC、NFD、NFKD)都无法帮助转写“Bjørn”,因为LATIN SMALL LETTER O WITH STROKE字符(U+00F8)不被视为组合。为此,您可能需要一个真正的转写器,例如ICU - 200_success
这对于希伯来语不起作用:https://en.wikipedia.org/wiki/Diacritic#Hebrew 例如:"בְּרֵאשִׁית" 不会变成 "בראשית"。它仍然保持完全相同。即使它转换了,我也不知道你该如何处理,因为在这里它被视为显示的更多字符(长度为11而不是6)。 - android developer
显示剩余4条评论

25

核心的 java.text 包旨在解决这种情况(匹配字符串但不考虑变音符号、大小写等)。

配置一个 Collator 以按字符中的 PRIMARY 差异排序。然后为每个字符串创建一个 CollationKey。如果您的所有代码都是 Java,可以直接使用 CollationKey。如果需要将键存储在数据库或其他类型的索引中,可以将其转换为字节数组

这些类使用 Unicode 标准大小写折叠数据来确定哪些字符是等效的,并支持各种分解策略

Collator c = Collator.getInstance();
c.setStrength(Collator.PRIMARY);
Map<CollationKey, String> dictionary = new TreeMap<CollationKey, String>();
dictionary.put(c.getCollationKey("Björn"), "Björn");
...
CollationKey query = c.getCollationKey("bjorn");
System.out.println(dictionary.get(query)); // --> "Björn"

请注意,排序规则会因为所在地域不同而有所不同。这是因为“按字母顺序排列”在不同的地区(甚至随着时间的推移,如西班牙语)有所差异。 Collator 类能够帮助您避免跟踪所有这些规则并使其保持最新状态。


听起来很有趣,但是你能用 select * from person where collated_name like 'bjo%' 在数据库中搜索你的排序键吗? - Andreas Petersson
非常好,我不知道这个。我会试一试的。 - Andreas Petersson
在Android上,CollationKeys不能用作数据库搜索的前缀。字符串a的排序键转换为字节41、1、5、1、5、0,而字符串ab转换为字节41、43、1、6、1、6、0。这些字节序列不会完整地出现在单词中(排序键a的字节数组不会出现在排序键ab的字节数组中)。 - Grzegorz Adam Hankiewicz
1
经过一些测试,我发现字节数组可以进行比较,但不会形成前缀,正如您所指出的那样。因此,要执行类似于 bjo% 的前缀查询,您需要执行一个范围查询,其中排序器 >= bjo 且 < bjp(或该语言环境中下一个符号是什么,没有编程方法来确定)。 - erickson

18

自3.1版本起,它是Apache Commons Lang的一部分。

org.apache.commons.lang3.StringUtils.stripAccents("Añ");

返回 An


1
对于 Ø,它再次给出 Ø。 - Mike Argyriou

11

您可以使用java.text中的规范化类(Normalizer class)

System.out.println(new String(Normalizer.normalize("ń ǹ ň ñ ṅ ņ ṇ ṋ", Normalizer.Form.NFKD).getBytes("ascii"), "ascii"));

但是还有一些工作要做,因为Java对于无法转换的Unicode字符会产生奇怪的结果(它既不会忽略它们,也不会抛出异常)。不过我认为你可以以此作为起点。


3
这种方法无法处理非ASCII附加符号,例如俄语中也有附加符号,并且还会破坏所有亚洲字符串。不要使用这种方法。可以使用\p{InCombiningDiacriticalMarks}正则表达式,而不是转换为ASCII,具体方法参见以下答案:https://dev59.com/yHM_5IYBdhLWcg3wQQzw#1453284 - Andreas Petersson

10

6
请注意,并非所有这些符号只是一些“普通”字符上的“标记”,可以删除而不改变含义。
在瑞典语中,å、ä和ö是真正的一流字符,而不是其他字符的某种“变体”。它们的发音与所有其他字符不同,排序也不同,并且它们会使单词的含义发生变化(“mätt”和“matt”是两个不同的单词)。

4
虽然正确,但这更像是对问题的评论而不是答案。 - Simon Forsberg

3
在德语中,不希望从Umlauts(ä、ö、ü)中删除变音符号。相反,它们被替换为两个字母的组合(ae、oe、ue)。例如,Björn应该写成Bjoern(而不是Bjorn),以获得正确的发音。
为此,我更喜欢硬编码映射,您可以单独定义每个特殊字符组的替换规则。

2

对我来说,最简单的方法是维护一个稀疏映射数组,将Unicode代码点简单地转换为可显示的字符串。

例如:

start    = 0x00C0
size     = 23
mappings = {
    "A","A","A","A","A","A","AE","C",
    "E","E","E","E","I","I","I", "I",
    "D","N","O","O","O","O","O"
}
start    = 0x00D8
size     = 6
mappings = {
    "O","U","U","U","U","Y"
}
start    = 0x00E0
size     = 23
mappings = {
    "a","a","a","a","a","a","ae","c",
    "e","e","e","e","i","i","i", "i",
    "d","n","o","o","o","o","o"
}
start    = 0x00F8
size     = 6
mappings = {
    "o","u","u","u","u","y"
}
: : :

使用稀疏数组可以高效地表示替换,即使它们在Unicode表的广泛间隔部分中。字符串替换将允许任意序列替换您的变音符号(例如æ字形变成ae)。

这是一种与语言无关的答案,因此,如果您有特定的语言,请使用更好的方法(尽管它们最终都可能归结为这个方法)。


2
添加所有可能的奇怪字符并不是一件容易的任务。如果只针对少量字符进行此操作,则是一个好的解决方案。 - Simon Forsberg

2
Unicode具有特定的附加符号字符(即组合字符),可以将字符串转换为字符和附加符号分离的形式。然后,您只需从字符串中删除附加符号就可以完成操作。
有关规范化、分解和等价性的更多信息,请参阅Unicode标准,网址为Unicode主页
然而,如何实现这一点取决于您所使用的框架/操作系统等。如果您正在使用.NET,则可以使用接受System.Text.NormalizationForm枚举的String.Normalize方法。

2
这是我在.NET中使用的方法,尽管我仍然需要手动映射一些字符。它们不是变音符号,而是双字母组合。虽然问题类似。 - Thorarin
1
转换为规范化形式“D”(即分解形式),并取基本字符。 - Richard

2
在Windows和.NET中,我只需使用字符串编码进行转换。这样我就避免了手动映射和编码。尝试使用字符串编码。

3
可以详细介绍一下字符串编码吗?例如,附上一个示例代码。 - Peter Mortensen

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