Удалите диакритические знаки (ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ from) из символов Unicode


88

Я ищу алгоритм, который может отображать символы с диакритическими знаками ( тильда , циркумфлекс , каретка , умлаут , карон ) и их «простой» символ.

Например:

ń  ǹ  ň  ñ  ṅ  ņ  ṇ  ṋ  ṉ  ̈  ɲ  ƞ ᶇ ɳ ȵ  --> 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» (год). Поэтому, если вам действительно нужна хорошая поисковая система, вы не можете полагаться на простое удаление диакритических знаков.
Эдуардо

@ Эдуардо: В данном контексте это может не иметь значения. Используя пример, приведенный OP, при поиске имени человека в многонациональном контексте вы действительно хотите, чтобы поиск не был слишком точным.
Amir Abiri

(Случайно отправлено предыдущим) Тем не менее, есть место для сопоставления диакритических знаков с их фонетическими эквивалентами для улучшения фонетического поиска. ie ñ => ni даст лучшие результаты, если базовая поисковая система поддерживает поиск на основе фонетики (например, soundex)
Амир Абири

Пример использования, когда при изменении año на ano и т. Д. Удаляются символы, отличные от base64, для URL-адресов, идентификаторов и т. Д.
Ондра Жижка

Ответы:


83

Недавно я сделал это на 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

но это не удастся, например, в Белостоке, потому что łсимвол не является диакритическим.

Если вы хотите получить полноценный упроститель строк, вам понадобится второй раунд очистки для некоторых дополнительных специальных символов, не являющихся диакритическими знаками. На этой карте я включил наиболее распространенные специальные символы, которые встречаются в именах наших клиентов. Это не полный список, но он даст вам представление о том, как его расширить. ImmutableMap - это простой класс из google-коллекций.

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

они пройдут - правда. также все японские иероглифы и т. д.
Андреас Петерссон

спасибо Андреас. Есть ли способ их удалить? Такие символы, как ら が な を 覚 男 (или другие), будут включены в сгенерированную строку, что в основном нарушит вывод. Я пытаюсь использовать вывод simpleifiedString в качестве генератора URL-адресов, как это делает StackOverflow для URL-адресов своих вопросов.
mickthompson

2
Как я уже сказал в комментарии к вопросу. Вы не можете полагаться на простое удаление диакритических знаков, если вам нужна хорошая поисковая система.
Эдуардо

3
Спасибо Андреас, работает как шарм! (проверено на 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 был разработан для решения этого варианта использования (сопоставление строк без учета диакритических знаков, регистра и т. Д.).

Настройте 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, где collated_name, например 'bjo%' ??
Андреас Петерссон

очень красиво, не знал об этом. попробую это.
Андреас Петерссон

В Android CollationKeys нельзя использовать в качестве префиксов для поиска в базе данных. Ключ сопоставления строки aпревращается в байты 41, 1, 5, 1, 5, 0, но строка abпревращается в байты 41, 43, 1, 6, 1, 6, 0. Эти последовательности байтов не отображаются как есть полными словами (байтовый массив для ключа сопоставления aне отображается в ab
байтовом

1
@GrzegorzAdamHankiewicz После некоторого тестирования я вижу, что байтовые массивы можно сравнивать, но не формируют префиксы, как вы заметили. Итак, чтобы выполнить такой префиксный запрос bjo%, вам нужно будет выполнить запрос диапазона, в котором сопоставителями являются> = bjoи < bjp(или любой другой символ, который будет в этой локали, и нет программного способа определить это).
эриксон

16

Это часть Apache Commons Lang с версии ver. 3.1.

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

возвращается An


1
Для Ø снова
получается

2
Спасибо, Майк, что указал на это. Метод обрабатывает только акценты. Результатом «ń ǹ ň ñ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ» будет «nnnnnnnnn ɲ ƞ ᶇ ɳ»
Kenston Choi

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/…
Андреас Петерссон

10

Есть черновик отчета сайте Unicode о сворачивании символов, в котором есть много соответствующего материала. См. Конкретно раздел 4.1. «Алгоритм складывания».

Вот обсуждение и реализация удаления диакритического маркера с помощью Perl.

Эти существующие вопросы SO связаны:


5

Обратите внимание, что не все эти метки являются просто «метками» на каком-то «обычном» символе, которые можно удалить, не меняя значения.

В шведском å ä и ö - настоящие и правильные первоклассные символы, а не какой-то «вариант» какого-то другого символа. Они звучат иначе, чем все другие символы, они сортируются по-другому и заставляют слова менять значение («mätt» и «matt» - это два разных слова).


4
Хотя это и верно, это скорее комментарий, чем ответ на вопрос.
Саймон Форсберг

2

Unicode имеет определенные диатрические символы (которые являются составными символами), и строка может быть преобразована так, чтобы символ и диатрика были разделены. Затем вы можете просто удалить диатрику со строки, и все готово.

Для получения дополнительной информации о нормализации, декомпозиции и эквивалентности см. Стандарт Unicode на домашней странице Unicode. .

Однако то, как вы можете этого добиться, зависит от платформы / 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». Или заменив umlauted o на «o» или «oe». Любое решение, которое вы придумаете, в идеале, я думаю, должно включать и то, и другое.


2

В Windows и .NET я просто конвертирую, используя строковую кодировку. Таким образом я избегаю ручного сопоставления и кодирования.

Попробуйте поиграться со строковой кодировкой.


3
Можете ли вы подробнее рассказать о кодировке строк? Например, с примером кода.
Питер Мортенсен

2

В немецком языке не нужно удалять диакритические знаки из умляутов (ä, ö, ü). Вместо этого они заменяются комбинацией из двух букв (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");
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.