从Unicode字符中删除变音符(criticalñṅṇṋṉȵȵ)


88

我正在研究一种算法,该算法可以在带有变音符号的字符(tildecircumflexcaretumlautcaron)及其“简单”字符之间进行映射。

例如:

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

等等。

  1. 我想用Java做到这一点,尽管我怀疑它应该是Unicode-y,并且应该可以轻松地以任何语言进行操作。

  2. 目的:允许轻松搜索带有变音符号的单词。例如,如果我有一个网球运动员数据库,并且输入了Björn_Borg,我还将保留Bjorn_Borg,这样,如果有人进入Bjorn而不是Björn,我就可以找到它。


尽管您可能必须手动维护某种映射表,但这取决于您正在编程的环境。那么,您使用哪种语言?
Thorarin

15
请注意,请勿删除ñen.wikipedia.org/wiki/%C3%91之类的字母,以便进行搜索。Google正确区分了西班牙语的“ ano”(肛门)和“año”(年份)。因此,如果您真的想要一个好的搜索引擎,则不能依赖于基本的变音标记删除。
爱德华多(Eduardo)2010年

@Eduardo:在给定的上下文中这可能并不重要。使用OP给出的示例,在多国环境中搜索某人的姓名,您实际上希望该搜索不太准确。
阿米尔·阿比里

(无意间发送过)尽管有足够的空间将变音符号映射到其语音等效项,以改善语音搜索。即,如果基础搜索引擎支持基于语音的搜索(例如soundex),则ñ=> ni将产生更好的结果
Amir Abiri 2012年

其中改变AÑO到ANO等等用例是用于汽提的URL,ID的等非的base64字符
Ondra参观Žižka

Answers:


82

我最近在Java中完成了此操作:

public static final Pattern DIACRITICS_AND_FRIENDS
    = Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");

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) {
        StringBuffer ret = new StringBuffer();
        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 -> see http://docstore.mik.ua/orelly/perl/prog3/ch05_04.htm
    InCombiningDiacriticalMarks: special marks that are part of "normal" ä, ö, î etc..
        IsSk: Symbol, Modifier see http://www.fileformat.info/info/unicode/category/Sk/list.htm
        IsLm: Letter, Modifier see http://www.fileformat.info/info/unicode/category/Lm/list.htm
     */
    public static final Pattern DIACRITICS_AND_FRIENDS
        = Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");


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

╨这样的字符呢?
mickthompson

他们将通过。同样所有的日文字符等
安德烈亚斯·皮特森

谢谢安德里亚斯。有没有办法删除这些?诸如らがなを覚男(或其他角色)之类的字符将包含在生成的字符串中,并且这些字符基本上会破坏输出。我正在尝试使用简化字符串输出作为URL生成器,就像StackOverflow用于其Questions的URL一样。
mickthompson

2
正如我在问题评论中所说。如果您想要一个好的搜索引擎,则不能依靠基本的变音标记删除。
爱德华多

3
谢谢Andreas,作品像个魅力!(上rrrr̈r'ŕřttẗţỳỹẙy'yýÿŷpp̈sss̈s̊s's̸śŝŞşšddd̈ďd'ḑf̈f̸ggg̈g'ģqĝǧḧĥj̈j'ḱkk̈k̸ǩlll̈Łłẅẍcc̈c̊c'c̸Çççćĉčvv̈v'v̸bb̧ǹnn̈n̊n'ńņňñmmmm̈m̊m̌ǵß测试):-)
Fortega

25

核心java.text包旨在解决此用例(匹配字符串而无需关心变音符号,大小写等)。

配置aCollator以对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人来在数据库中搜索归类键吗?
Andreas Petersson,2009年

非常好,对此一无所知。将尝试一下。
Andreas Petersson,2009年

在Android上,Collat​​ionKeys不能用作数据库搜索的前缀。字符串的排序规则键a变成字节41、1、5、1、5、0,但是字符串ab变成字节41、43、1、6、1、6、0。这些字节序列不按原样显示用完整的单词表示(排序键的字节数组a未出现在用于排序键的字节数组中ab
Grzegorz Adam Hankiewicz

1
@GrzegorzAdamHankiewicz经过一些测试,我发现可以比较字节数组,但是不像您指出的那样形成前缀。因此,要执行像这样的前缀查询bjo%,您需要执行一个范围查询,其中的排序规则是> =bjo和< bjp(或该语言环境中的下一个符号,而没有编程的方法来确定)。
erickson


12

您可以使用来自的Normalizer类java.text

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

但是仍然有一些工作要做,因为Java使用无法转换的Unicode字符来处理奇怪的事情(它不会忽略它们,并且不会引发异常)。但是我认为您可以以此为起点。


3
这不适用于非ascii变音符号,例如俄语,他们也有变音符号,而且还屠宰了所有亚洲字符串。不使用。代替转换为ascii,使用\\ p {InCombiningDiacriticalMarks}正则表达式中的答案stackoverflow.com/questions/1453171/...
安德烈亚斯皮特森


5

请注意,并非所有这些标记都只是某些“正常”字符上的“标记”,您可以在不更改含义的情况下将其删除。

在瑞典语中,åä和ö是真实正确的一流字符,而不是其他某些字符的“变体”。它们听起来与所有其他字符都不同,它们的分类也不同,并且它们使单词改变意思(“mätt”和“ matt”是两个不同的单词)。


4
尽管是正确的,但这更多是评论,而不是问题的答案。
西蒙·福斯伯格

2

Unicode具有特定的直径字符(它们是复合字符),并且可以转换字符串,以便将字符和直径分开。然后,您只需从字符串中删除小写字母,就可以基本完成了。

有关规范化,分解和等效性的更多信息,请参见Unicode主页上的Unicode标准。

但是,如何实际实现此目标取决于您正在研究的Framework / OS /。如果使用的是.NET,则可以使用String.Normalize方法接受System.Text.NormalizationForm枚举。


2
这是我在.NET中使用的方法,尽管我仍然必须手动映射一些字符。他们不是变音符号,而是有向图。虽然类似的问题。
Thorarin

1
转换为规范化形式“ D”(即分解)并采用基本字符。
理查德

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

需要考虑的事情:如果您尝试每个单词都进行一次“翻译”,那么您可能会错过一些可能的替代词。

例如,用德语,当替换“ s-set”时,某些人可能会使用“ B”,而其他人可能会使用“ ss”。或者,用“ o”或“ oe”代替变位的o。理想情况下,我想您想出的任何解决方案都应包括两者。



2

如果是德语,则不希望从Umlauts(ä,ö,ü)中删除变音符号。而是用两个字母的组合(ae,oe,ue)代替。例如,Björn应该写为Bjoern(不是Bjorn)以具有正确的发音。

为此,我会有一个硬编码的映射,您可以在其中为每个特殊字符组分别定义替换规则。


0

供将来参考,这是一种消除重音符号的C#扩展方法。

public static class StringExtensions
{
    public static string RemoveDiacritics(this string str)
    {
        return new string(
            str.Normalize(NormalizationForm.FormD)
                .Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != 
                            UnicodeCategory.NonSpacingMark)
                .ToArray());
    }
}
static void Main()
{
    var input = "ŃŅŇ ÀÁÂÃÄÅ ŢŤţť Ĥĥ àáâãäå ńņň";
    var output = input.RemoveDiacritics();
    Debug.Assert(output == "NNN AAAAAA TTtt Hh aaaaaa nnn");
}
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.