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

@Eduardo: У даному контексті це може не мати значення. Використовуючи приклад, наданий ОП, шукаючи ім’я людини у багатонаціональному контексті, ви насправді хочете, щоб пошук не був надто точним.
Амір Абірі,

(Випадково надісланий попередній) Хоча є місце для відображення діакритичних значень до їх фонетичних еквівалентів для покращення фонетичного пошуку. тобто ñ => ni дасть кращі результати, якщо основна пошукова система підтримує фонетичний пошук (наприклад, soundex)
Амір Абірі,

Випадок використання, коли зміна año на ano тощо - це вилучення неосновних 64 символів для URL-адрес, ідентифікаторів тощо
Ondra Žižka

Відповіді:


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

але це не вдасться, наприклад, у Білостоці, бо łперсонаж не є діакритичним.

Якщо ви хочете мати повномасштабний спрощувач рядків, вам знадобиться другий раунд очищення для деяких спеціальніших символів, які не є діакритичними. Це карта, я включив найпоширеніші спеціальні символи, які з’являються в іменах наших клієнтів. Це не повний перелік, але він дасть вам уявлення, як його продовжити. Незмінна карта - це просто простий клас із колекцій 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

вони будуть передані - хоча. так само всі японські ієрогліфи тощо
Андреас Пітерссон

дякую Андреас. Чи є спосіб їх видалити? Такі символи, як ら が な を 覚 男 (або інші), будуть включені до згенерованого рядка, і це в основному порушить вихід. Я намагаюся використовувати висновок simplifiedString як генератор 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 був розроблений для вирішення цього випадку використання (узгодження рядків, не дбаючи про діакритику, регістр тощо).

Налаштуйте 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Клас позбавляє Вас від необхідності відстежувати всі ці правила і тримати їх в актуальному стані .


звучить цікаво, але чи можете ви шукати ваш ключ зіставлення в базі даних за допомогою select * від людини, де 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(або будь-який наступний символ буде в цій мові, і немає програмного способу визначити це).
erickson

16

Це частина Apache Commons Lang станом на версію. 3.1.

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

повертається An


1
Для Ø це знову дає Ø
Mike Argyriou

2
Дякуємо Майку, що вказав на це. Метод обробляє лише акценти. Результатом "ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ" є "nnnnnnnnn ɲ ƞ ᶇ ɳ ȵ"
Кенстон Чой,

12

Ви можете використовувати клас Normalizer з java.text:

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

Але є ще деяка робота, оскільки Java робить дивні речі з неконвертованими символами Unicode (вона їх не ігнорує і не створює винятку). Але я думаю, що ви могли б використати це як вихідну точку.


3
це не буде працювати для не-асіївських діакритиків, наприклад, російською мовою, у них теж є діакритики, і, крім того, ріжуть усі азіатські струни. не використовувати. замість перетворення на ascii, використовуйте регулярний вираз \\ p {InCombiningDiacriticalMarks} як у відповіді stackoverflow.com/questions/1453171/…
Андреас Петерссон,

10

На веб-сайті Unicode є проект звіту про згортання символів, який містить багато відповідних матеріалів. Див. Конкретно розділ 4.1. "Алгоритм складання".

Ось обговорення та реалізація видалення діакритичних маркерів за допомогою Perl.

Ці існуючі запитання щодо SO пов’язані:


5

Зверніть увагу, що не всі з цих позначок - це просто "позначки" на якомусь "звичайному" символі, які ви можете видалити, не змінюючи значення.

У шведській мові å ä та ö є справжніми і правильними першокласними персонажами, а не якимось "варіантом" якогось іншого символу. Вони звучать по-різному від усіх інших символів, вони сортуються по-різному, і вони змушують слова змінювати значення ("mätt" і "matt" - це два різні слова).


4
Хоча це правильно, це скоріше коментар, ніж відповідь на запитання.
Саймон Форсберг

2

Unicode має специфічні діатричні символи (які є складеними символами), і рядок може бути перетворений так, щоб символ та діаметри були розділені. Потім ви можете просто видалити діатрики з рядка, і в основному все готово.

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

Однак те, як ви насправді можете цього досягти, залежить від фреймворку / ОС / ..., над яким ви працюєте. Якщо ви використовуєте .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", деякі люди можуть використовувати "B", тоді як інші можуть використовувати "ss". Або, замінивши відмінне 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.