Rimuovi i segni diacritici (ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ) dai caratteri Unicode


88

Sto cercando un algoritmo in grado di mappare tra i caratteri con segni diacritici ( tilde , circonflesso , accento circonflesso , dieresi , caron ) e il loro carattere "semplice".

Per esempio:

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

Eccetera.

  1. Voglio farlo in Java, anche se sospetto che dovrebbe essere qualcosa di Unicode-y e dovrebbe essere fattibile ragionevolmente facilmente in qualsiasi lingua.

  2. Scopo: per consentire una facile ricerca di parole con segni diacritici. Ad esempio, se ho un database di giocatori di tennis e viene inserito Björn_Borg, terrò anche Bjorn_Borg in modo da poterlo trovare se qualcuno inserisce Bjorn e non Björn.


Dipende dall'ambiente in cui stai programmando, anche se probabilmente dovrai mantenere manualmente una sorta di tabella di mappatura. Allora, quale lingua stai usando?
Thorarin,

15
Per favore fai attenzione che alcune lettere come ñ en.wikipedia.org/wiki/%C3%91 non dovrebbero essere rimosse dai segni diacritici per scopi di ricerca. Google distingue correttamente tra spagnolo "ano" (anus) e "año" (anno). Quindi, se vuoi davvero un buon motore di ricerca, non puoi fare affidamento sulla rimozione dei segni diacritici di base.
Eduardo

@ Eduardo: in un dato contesto potrebbe non avere importanza. Utilizzando l'esempio fornito dal PO, cercando il nome di una persona in un contesto multinazionale, in realtà si desidera che la ricerca non sia troppo accurata.
Amir Abiri

(Accidentalmente inviato in precedenza) C'è spazio però per mappare i segni diacritici ai loro equivalenti fonetici per migliorare la ricerca fonetica. ad esempio ñ => ni produrrà risultati migliori se il motore di ricerca sottostante supporta la ricerca basata sulla fonetica (ad esempio soundex)
Amir Abiri

Un caso d'uso in cui cambiare año in ano ecc. Sta eliminando i caratteri non in base64 per URL, ID, ecc.
Ondra Žižka

Risposte:


82

L'ho fatto di recente in 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;
}

Questo farà come hai specificato:

stripDiacritics("Björn")  = Bjorn

ma fallirà, ad esempio, su Białystok, perché il łcarattere non è diacritico.

Se vuoi avere un semplificatore di stringhe in piena regola, avrai bisogno di un secondo round di pulizia, per alcuni caratteri più speciali che non sono segni diacritici. È questa mappa, ho incluso i caratteri speciali più comuni che compaiono nei nomi dei nostri clienti. Non è un elenco completo, ma ti darà l'idea di come estenderlo. ImmutableMap è solo una semplice classe di 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;
    }
}

che dire di personaggi come ╨?
mickthompson

saranno passati, però. allo stesso modo tutti i personaggi giapponesi ecc.
Andreas Petersson

grazie Andreas. C'è un modo per rimuoverli? Caratteri come ら が な を 覚 男 (o altri) saranno inclusi nella stringa generata e questi sostanzialmente interromperanno l'output. Sto provando a utilizzare l'output di SimplifiedString come generatore di URL come fa StackOverflow per gli URL delle sue domande.
mickthompson

2
Come ho detto nella domanda commento. Non puoi fare affidamento sulla rimozione dei segni diacritici di base se desideri un buon motore di ricerca.
Eduardo

3
Grazie Andreas, funziona alla grande! (testato su 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

Il pacchetto principale java.text è stato progettato per affrontare questo caso d'uso (corrispondenza di stringhe senza preoccuparsi di segni diacritici, maiuscole e minuscole, ecc.).

Configura a Collatorper ordinare le PRIMARYdifferenze nei caratteri. Con questo, crea un CollationKeyper ogni stringa. Se tutto il codice è in Java, puoi utilizzare CollationKeydirettamente il file. Se è necessario memorizzare le chiavi in ​​un database o in un altro tipo di indice, è possibile convertirlo in un array di byte .

Queste classi utilizzano i dati di piegatura dei casi standard Unicode per determinare quali caratteri sono equivalenti e supportano varie strategie di scomposizione .

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"

Notare che i raccoglitori sono specifici della locale. Questo perché "l'ordine alfabetico" è diverso tra le lingue (e anche nel tempo, come nel caso dello spagnolo). La Collatorclasse ti solleva dal dover tenere traccia di tutte queste regole e tenerle aggiornate.


sembra interessante, ma puoi cercare la tua chiave di confronto nel database con select * from person where collated_name like 'bjo%' ??
Andreas Petersson,

molto bello, non lo sapevo. lo proverò.
Andreas Petersson,

Su Android le CollationKeys non possono essere utilizzate come prefissi per le ricerche nel database. Una chiave di confronto della stringa asi trasforma in byte 41, 1, 5, 1, 5, 0, ma la stringa absi trasforma in byte 41, 43, 1, 6, 1, 6, 0. Queste sequenze di byte non vengono visualizzate come sono in parole complete (l'array di byte per la chiave di confronto anon appare nell'array di byte per la chiave di confronto per ab)
Grzegorz Adam Hankiewicz

1
@GrzegorzAdamHankiewicz Dopo alcuni test, vedo che gli array di byte possono essere confrontati, ma non formano prefissi, come hai notato. Quindi, per eseguire una query di prefisso come bjo%, è necessario eseguire una query di intervallo in cui i raccoglitori sono> = bjoe < bjp(o qualunque sia il simbolo successivo in quella lingua, e non esiste un modo programmatico per determinarlo).
erickson

16

Fa parte di Apache Commons Lang a partire dalla ver. 3.1.

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

ritorna An


1
Per Ø dà di nuovo Ø
Mike Argyriou

2
Grazie Mike per averlo fatto notare. Il metodo gestisce solo gli accenti. Il risultato di "ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ" è "nnnnnnnnn ɲ ƞ ᶇ ɳ ȵ"
Kenston Choi

12

Puoi usare la classe Normalizer da java.text:

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

Ma c'è ancora del lavoro da fare, poiché Java fa cose strane con caratteri Unicode non convertibili (non li ignora e non genera un'eccezione). Ma penso che potresti usarlo come punto di partenza.


3
questo non funzionerà per segni diacritici non ASCII, come in russo, hanno anche segni diacritici e inoltre macellano tutte le stringhe asiatiche. non usare. invece di convertire in ASCII, usa \\ p {InCombiningDiacriticalMarks} regexp come nella risposta stackoverflow.com/questions/1453171/…
Andreas Petersson,


5

Tieni presente che non tutti questi segni sono solo "segni" su un carattere "normale", che puoi rimuovere senza cambiare il significato.

In svedese, å ä e ö sono veri e propri caratteri di prima classe, non una "variante" di qualche altro carattere. Hanno un suono diverso da tutti gli altri personaggi, hanno un ordinamento diverso e fanno cambiare significato alle parole ("mätt" e "matt" sono due parole diverse).


4
Sebbene corretto, questo è più un commento che una risposta alla domanda.
Simon Forsberg

2

Unicode ha caratteri diatrici specifici (che sono caratteri compositi) e una stringa può essere convertita in modo che il carattere e il diatrico siano separati. Quindi, puoi semplicemente rimuovere i diatricts dalla stringa e il gioco è fatto.

Per ulteriori informazioni su normalizzazione, decomposizioni ed equivalenza, vedere The Unicode Standard nella home page di Unicode .

Tuttavia, il modo in cui puoi effettivamente ottenere ciò dipende dal framework / OS / ... su cui stai lavorando. Se stai usando .NET, puoi usare il metodo String.Normalize che accetta l' enumerazione System.Text.NormalizationForm .


2
Questo è il metodo che utilizzo in .NET, anche se devo ancora mappare alcuni caratteri manualmente. Non sono segni diacritici, ma digrafi. Problema simile però.
Thorarin,

1
Converti nella forma di normalizzazione "D" (cioè scomposta) e prendi il carattere di base.
Richard,

2

Il modo più semplice (per me) sarebbe semplicemente mantenere un array di mappatura sparsa che cambia semplicemente i punti del codice Unicode in stringhe visualizzabili.

Ad esempio:

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

L'uso di un array sparso consentirà di rappresentare in modo efficiente le sostituzioni anche quando si trovano in sezioni molto distanziate della tabella Unicode. Le sostituzioni di stringhe consentiranno a sequenze arbitrarie di sostituire i segni diacritici (come il ædivenire grafema ae).

Questa è una risposta indipendente dalla lingua, quindi, se hai in mente una lingua specifica, ci saranno modi migliori (anche se probabilmente arriveranno comunque a questo ai livelli più bassi).


Aggiungere tutti i possibili personaggi strani non è un compito facile. Quando lo fai solo per pochi personaggi, è una buona soluzione.
Simon Forsberg

2

Qualcosa da considerare: se segui la strada per cercare di ottenere una singola "traduzione" di ogni parola, potresti perdere alcune possibili alternative.

Ad esempio, in tedesco, quando si sostituisce la "s-set", alcune persone potrebbero usare "B", mentre altri potrebbero usare "ss". Oppure, sostituendo una o dieresi con "o" o "oe". Qualsiasi soluzione tu possa trovare, idealmente, penso che dovrebbe includere entrambi.


2

In Windows e .NET, converto semplicemente usando la codifica di stringa. In questo modo evito la mappatura e la codifica manuali.

Prova a giocare con la codifica delle stringhe.


3
Puoi approfondire la codifica delle stringhe? Ad esempio, con un esempio di codice.
Peter Mortensen

2

Nel caso del tedesco non si desidera rimuovere i segni diacritici dagli Umlauts (ä, ö, ü). Invece sono sostituiti da una combinazione di due lettere (ae, oe, ue). Ad esempio, Björn dovrebbe essere scritto come Bjoern (non Bjorn) per avere una pronuncia corretta.

Per questo avrei preferito una mappatura hardcoded, dove puoi definire la regola di sostituzione individualmente per ogni gruppo di caratteri speciali.


0

Per riferimento futuro, ecco un metodo di estensione C # che rimuove gli accenti.

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");
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.