Usuń znaki diakrytyczne (ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ) ze znaków Unicode


88

Patrzę na algorytmu, który można mapować między postaciami ze znakami diakrytycznymi ( tylda , daszkiem , daszek , umlaut , Caron ) i ich „prosty” charakter.

Na przykład:

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

Itp.

  1. Chcę to zrobić w Javie, chociaż podejrzewam, że powinno to być coś w standardzie Unicode-y i powinno być możliwe do wykonania w miarę łatwo w dowolnym języku.

  2. Cel: umożliwienie łatwego wyszukiwania słów ze znakami diakrytycznymi. Na przykład, jeśli mam bazę danych tenisistów i wpisano Björn_Borg, zachowam również Bjorn_Borg, aby móc go znaleźć, jeśli ktoś wejdzie do Bjorn, a nie Björn.


Zależy to od środowiska, w którym programujesz, chociaż prawdopodobnie będziesz musiał ręcznie obsługiwać jakąś tabelę mapowania. Więc jakiego języka używasz?
Thorarin

15
Uwaga: niektóre litery, takie jak ñ en.wikipedia.org/wiki/%C3%91, nie powinny być usuwane ze znaków diakrytycznych do celów wyszukiwania. Google poprawnie rozróżnia hiszpańskie „ano” (odbyt) i „año” (rok). Jeśli więc naprawdę chcesz mieć dobrą wyszukiwarkę, nie możesz polegać na prostym usuwaniu znaków diakrytycznych.
Eduardo

@Eduardo: W danym kontekście może to nie mieć znaczenia. Korzystając z przykładu podanego przez OP, wyszukując imię i nazwisko osoby w kontekście międzynarodowym, w rzeczywistości chcesz, aby wyszukiwanie nie było zbyt dokładne.
Amir Abiri,

(Przypadkowo wysłane poprzednie) Jest jednak miejsce na odwzorowanie znaków diakrytycznych na ich fonetyczne odpowiedniki, aby usprawnić wyszukiwanie fonetyczne. tj. ñ => ni przyniesie lepsze wyniki, jeśli bazowa wyszukiwarka obsługuje wyszukiwanie oparte na fonetyce (np. soundex)
Amir Abiri

Przypadek użycia, w którym zmiana año na ano itp. Polega na usunięciu znaków innych niż base64 z adresów URL, identyfikatorów itp.
Ondra Žižka

Odpowiedzi:


82

Ostatnio zrobiłem to w Javie:

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;
}

Zrobi to, jak określiłeś:

stripDiacritics("Björn")  = Bjorn

ale to się nie uda na przykład na Białymstoku, bo łpostać nie jest diakrytyczna.

Jeśli chcesz mieć pełne uproszczenie ciągów znaków, będziesz potrzebować drugiej rundy porządkowania, aby uzyskać więcej znaków specjalnych, które nie są znakami diakrytycznymi. Czy to ta mapa, dołączyłem najczęściej używane znaki specjalne, które pojawiają się w nazwach naszych klientów. Nie jest to pełna lista, ale da ci pomysł, jak ją rozszerzyć. ImmutableMap to po prostu prosta klasa z kolekcji 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;
    }
}

a co z postaciami takimi jak ╨?
mickthompson

zostaną przekazane - chociaż. podobnie wszystkie japońskie postacie itp.
Andreas Petersson,

dzięki Andreas. Czy jest sposób, aby je usunąć? Znaki takie jak ら が な を 覚 男 (lub inne) zostaną uwzględnione w generowanym ciągu i zasadniczo spowodują przerwanie wyniku. Próbuję użyć danych wyjściowych simplifiedString jako generatora adresów URL, tak jak robi to StackOverflow dla adresów URL swoich pytań.
mickthompson

2
Jak powiedziałem w komentarzu do pytania. Nie możesz polegać na prostym usuwaniu znaków diakrytycznych, jeśli chcesz mieć dobrą wyszukiwarkę.
Eduardo

3
Dzięki Andreas, działa jak urok! (testowane na 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

Podstawowy pakiet java.text został zaprojektowany, aby rozwiązać ten przypadek użycia (dopasowywanie ciągów znaków bez zwracania uwagi na znaki diakrytyczne, wielkość liter itp.).

Skonfiguruj Collatorsortowanie według PRIMARYróżnic w znakach. Dzięki temu utwórz CollationKeydla każdego ciągu. Jeśli cały kod jest w Javie, możesz użyć CollationKeybezpośrednio. Jeśli potrzebujesz przechowywać klucze w bazie danych lub w innym indeksie, możesz przekonwertować je na tablicę bajtów .

Klasy te używają standardowych danych składanych wielkości liter Unicode, aby określić, które znaki są równoważne, i obsługują różne strategie dekompozycji .

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"

Zwróć uwagę, że kolatory są specyficzne dla lokalizacji. Dzieje się tak, ponieważ „kolejność alfabetyczna” różni się w zależności od regionu (a nawet w czasie, jak miało to miejsce w przypadku hiszpańskiego). CollatorKlasa uwalnia użytkownika od konieczności śledzenia wszystkich tych zasad i zachować je na bieżąco.


brzmi interesująco, ale czy możesz przeszukać swój klucz sortowania w bazie danych za pomocą funkcji select * od osoby, dla której collated_name, np. „bjo%”?
Andreas Petersson

bardzo miło, nie wiedziałem o tym. spróbuje tego.
Andreas Petersson

W systemie Android nie można używać CollationKeys jako prefiksów do przeszukiwania bazy danych. Klucz sortowania ciągu azamienia się w bajty 41, 1, 5, 1, 5, 0, ale łańcuch abzamienia się w bajty 41, 43, 1, 6, 1, 6, 0. Te sekwencje bajtów nie pojawiają się tak, jak są w pełnych słów (tablica bajtów dla klucza sortowania anie pojawiają się w tablicy bajtów do klucza sortowania dla ab)
Grzegorz Adam Hankiewicz

1
@GrzegorzAdamHankiewicz Po kilku testach widzę, że tablice bajtów można porównać, ale nie tworzą przedrostków, jak zauważyłeś. Tak więc, aby wykonać zapytanie typu prefiks bjo%, musiałbyś wykonać zapytanie o zakres, w którym kolatory to> = bjoi < bjp(lub jakikolwiek następny symbol byłby w tym ustawieniu narodowym i nie ma programowego sposobu, aby to ustalić).
erickson

16

Jest częścią Apache Commons Lang od wer. 3.1.

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

zwroty An


1
Dla Ø daje to ponownie Ø
Mike Argyriou

2
Dzięki Mike za wskazanie tego. Metoda obsługuje tylko akcenty. Wynik „ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ” to „nnnnnnnnn ɲ ƞ ᶇ ɳ ȵ”
Kenston Choi

12

Możesz użyć klasy Normalizer z java.text:

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

Ale wciąż jest trochę do zrobienia, ponieważ Java robi dziwne rzeczy z niemożliwymi do konwersji znakami Unicode (nie ignoruje ich i nie zgłasza wyjątku). Ale myślę, że możesz to wykorzystać jako punkt wyjścia.


3
to nie zadziała w przypadku znaków diakrytycznych innych niż ascii, takich jak w języku rosyjskim, mają też znaki diakrytyczne, a ponadto rzeźbi wszystkie azjatyckie struny. nie używaj. zamiast konwertować na ascii, użyj \\ p {InCombiningDiacriticalMarks} regexp jak w answer stackoverflow.com/questions/1453171/ ...
Andreas Petersson


5

Należy pamiętać, że nie wszystkie te znaki są po prostu „znakami” na jakimś „normalnym” znaku, który można usunąć bez zmiany znaczenia.

W języku szwedzkim å ä i ö są prawdziwymi i właściwymi znakami pierwszej klasy, a nie jakimś „wariantem” innego znaku. Brzmią inaczej niż wszystkie inne znaki, inaczej sortują i powodują, że słowa zmieniają znaczenie („mätt” i „matt” to dwa różne słowa).


4
Chociaż jest to poprawne, jest to bardziej komentarz niż odpowiedź na pytanie.
Simon Forsberg,

2

Unicode ma określone znaki diatryczne (które są znakami złożonymi) i można przekształcić ciąg, tak aby znak i diatryki były rozdzielone. Następnie możesz po prostu usunąć diatricki ze sznurka i gotowe.

Aby uzyskać więcej informacji na temat normalizacji, dekompozycji i równoważności, zobacz Standard Unicode na stronie głównej Unicode .

Jednak sposób, w jaki możesz to osiągnąć, zależy od frameworka / systemu operacyjnego / ... nad którym pracujesz. Jeśli używasz platformy .NET, możesz użyć metody String.Normalize akceptującej wyliczenie System.Text.NormalizationForm .


2
Jest to metoda, której używam w .NET, chociaż nadal muszę ręcznie mapować niektóre znaki. To nie znaki diakrytyczne, ale dwuznaki. Jednak podobny problem.
Thorarin

1
Konwertuj do postaci normalizacyjnej „D” (tj. Zdekomponowanej) i weź znak podstawowy.
Richard

2

Najłatwiejszym sposobem (dla mnie) byłoby po prostu utrzymywanie rzadkiej tablicy mapowania, która po prostu zmienia twoje punkty kodowe Unicode na wyświetlane ciągi.

Jak na przykład:

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"
}
: : :

Użycie rzadkiej tablicy pozwoli Ci efektywnie przedstawić zamienniki, nawet jeśli znajdują się one w szeroko rozstawionych sekcjach tabeli Unicode. Zamiana ciągów pozwoli dowolnym sekwencjom zastąpić znaki diakrytyczne (takie jak tworzenie ægrafemów ae).

Jest to odpowiedź niezależna od języka, więc jeśli masz na myśli konkretny język, będą lepsze sposoby (chociaż i tak wszystkie prawdopodobnie sprowadzą się do tego na najniższych poziomach).


Dodanie wszystkich możliwych dziwnych postaci nie jest łatwym zadaniem. Robiąc to tylko dla kilku postaci, jest to dobre rozwiązanie.
Simon Forsberg,

2

Coś do rozważenia: jeśli podejmiesz próbę uzyskania pojedynczego „tłumaczenia” każdego słowa, możesz przegapić niektóre możliwe alternatywy.

Na przykład, w języku niemieckim, podczas zastępowania „s-set”, niektórzy ludzie mogą używać „B”, podczas gdy inni mogą używać „ss”. Lub zastąpienie umlautowanego o przez „o” lub „oe”. Myślę, że każde rozwiązanie, które wymyślisz, powinno obejmować oba.


2

W Windows i .NET konwertuję po prostu przy użyciu kodowania ciągów. W ten sposób unikam ręcznego mapowania i kodowania.

Spróbuj bawić się kodowaniem ciągów.


3
Czy możesz rozwinąć kodowanie ciągów znaków? Na przykład z przykładem kodu.
Peter Mortensen

2

W przypadku języka niemieckiego nie ma potrzeby usuwania znaków diakrytycznych z Umlautów (ä, ö, ü). Zamiast tego są one zastępowane kombinacją dwuliterową (ae, oe, ue). Na przykład Björn należy zapisać jako Bjoern (nie Bjorn), aby mieć poprawną wymowę.

W tym celu wolałbym raczej zakodowane mapowanie, w którym można zdefiniować regułę zastępowania indywidualnie dla każdej grupy znaków specjalnych.


0

Na przyszłość, oto metoda rozszerzenia C #, która usuwa akcenty.

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");
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.