89

我正在研究一种算法,该算法可以在带有变音符号(波浪号抑扬符、插入符号变音符号、卡隆)的字符与其“简单”字符之间进行映射。

例如:

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

等等。

  1. 我想在 Java 中做到这一点,尽管我怀疑它应该是 Unicode-y 并且应该可以在任何语言中相当容易地实现。

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

4

12 回答 12

86

我最近在 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) {
        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 -> 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;
    }
}
于 2009-09-21T07:43:40.420 回答
25

核心 java.text 包旨在解决这个用例(匹配字符串而不关心变音符号、大小写等)。

配置 a以对字符的差异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课程使您不必跟踪所有这些规则并使其保持最新状态。

于 2009-09-21T14:32:03.977 回答
17

从版本开始,它是Apache Commons Lang的一部分。3.1。

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

返回An

于 2012-10-14T10:22:13.343 回答
12

您可以使用以下Normalizerjava.text

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

但是还有一些工作要做,因为 Java 用不可转换的 Unicode 字符做一些奇怪的事情(它不会忽略它们,也不会抛出异常)。但我认为你可以以此为起点。

于 2009-09-21T07:31:38.307 回答
10

unicode 网站上有一个关于字符折叠的报告草稿,其中有很多相关材料。具体参见第 4.1 节。“折叠算法”。

这是使用 Perl 进行变音符号删除的讨论和实现。

这些现有的 SO 问题是相关的:

于 2009-09-21T07:13:14.507 回答
5

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

在瑞典语中,å ä 和 ö 是真实而恰当的一等字符,而不是某些其他字符的“变体”。它们听起来与所有其他字符不同,它们排序不同,并且它们使单词改变含义(“mätt”和“matt”是两个不同的词)。

于 2010-03-01T15:46:15.453 回答
2

Unicode 具有特定的diatric 字符(它们是复合字符),并且可以转换字符串,以便将字符和diatrics 分开。然后,您可以从字符串中删除 diatricts,您基本上就完成了。

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

但是,您如何实际实现这一点取决于您正在使用的框架/操作系统/...。如果您使用的是 .NET,则可以使用接受System.Text.NormalizationForm枚举的String.Normalize方法。

于 2009-09-21T07:10:10.383 回答
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)。

这是一个与语言无关的答案,因此,如果您考虑到特定的语言,那么会有更好的方法(尽管无论如何它们都可能会归结为最低级别)。

于 2009-09-21T07:41:05.110 回答
2

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

尝试使用字符串编码。

于 2009-09-21T14:41:36.567 回答
2

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

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

于 2009-09-21T14:58:09.313 回答
2

在德语的情况下,它不想从变音符号(ä、ö、ü)中删除变音符号。相反,它们被替换为两个字母组合(ae、oe、ue)。例如,Björn 应该写成 Bjoern(而不是 Bjorn)才能有正确的发音。

为此,我宁愿使用硬编码映射,您可以在其中为每个特殊字符组单独定义替换规则。

于 2013-02-08T10:16:12.773 回答
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");
}
于 2009-09-26T17:06:42.270 回答