Remova as marcas diacríticas (ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ) dos caracteres Unicode


88

Estou olhando um algoritmo que pode mapear entre caracteres com diacríticos ( til , circunflexo , circunflexo , trema , caron ) e seu caractere "simples".

Por exemplo:

ń  ǹ  ň  ñ    ņ        ̈  ɲ  ƞ  ɳ ȵ  --> n
á --> a
ä --> a
 --> a
 --> o

Etc.

  1. Quero fazer isso em Java, embora suspeite que deva ser algo Unicode-y e possa ser feito com razoável facilidade em qualquer linguagem.

  2. Objetivo: permitir a busca fácil por palavras com sinais diacríticos. Por exemplo, se eu tiver um banco de dados de jogadores de tênis e Björn_Borg estiver inserido, também irei manter Bjorn_Borg para que possa localizá-lo se alguém inserir Bjorn e não Björn.


Depende do ambiente em que você está programando, embora provavelmente você precise manter algum tipo de tabela de mapeamento manualmente. Então, qual idioma você está usando?
Thorarin,

15
Tenha em atenção que algumas letras como ñ en.wikipedia.org/wiki/%C3%91 não devem ter os seus diacríticos removidos para fins de pesquisa. O Google diferencia corretamente entre o espanhol "ano" (ânus) e "año" (ano). Portanto, se você realmente deseja um bom mecanismo de pesquisa, não pode contar com a remoção básica de marcas diacríticas.
Eduardo

@Eduardo: Em um determinado contexto, isso pode não importar. Usando o exemplo dado pelo OP, ao pesquisar o nome de uma pessoa em um contexto multinacional, você realmente deseja que a pesquisa não seja muito precisa.
Amir Abiri

(Enviado acidentalmente anterior) No entanto, há espaço para mapear os diacríticos para seus equivalentes fonéticos para melhorar a pesquisa fonética. ou seja, ñ => ni produzirá melhores resultados se o mecanismo de pesquisa subjacente suportar pesquisa baseada em fonética (por exemplo, soundex)
Amir Abiri

Um caso de uso em que alterar año para ano etc. está removendo caracteres não-base64 para URLs, IDs etc.
Ondra Žižka

Respostas:


82

Eu fiz isso recentemente em 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;
}

Isso fará como você especificou:

stripDiacritics("Björn")  = Bjorn

mas falhará, por exemplo, em Białystok, porque o łcaractere não é diacrítico.

Se você quiser um simplificador de string completo, precisará de uma segunda rodada de limpeza para mais alguns caracteres especiais que não sejam diacríticos. É este mapa, incluí os caracteres especiais mais comuns que aparecem nos nomes de nossos clientes. Não é uma lista completa, mas lhe dará a idéia de como estendê-la. O immutableMap é apenas uma classe simples de coleções do 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;
    }
}

e personagens como ╨?
mickthompson,

eles serão ultrapassados. da mesma forma todos os caracteres japoneses etc.
Andreas Petersson,

obrigado Andreas. Existe uma maneira de removê-los? Caracteres como ら が な を 覚 男 (ou outros) serão incluídos na string gerada e basicamente quebrarão a saída. Estou tentando usar a saída simplifiedString como um gerador de URL como StackOverflow faz para seus URLs de perguntas.
mickthompson

2
Como eu disse no comentário da pergunta. Você não pode contar com a remoção básica de marcas diacríticas se quiser um bom mecanismo de pesquisa.
Eduardo

3
Obrigado Andreas, funciona como um encanto! (testado em 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

24

O pacote principal do java.text foi projetado para lidar com esse caso de uso (correspondendo strings sem se preocupar com diacríticos, maiúsculas e minúsculas, etc.).

Configure um Collator para classificar as PRIMARYdiferenças de caracteres. Com isso, crie um CollationKeypara cada string. Se todo o seu código estiver em Java, você pode usar o CollationKeydiretamente. Se você precisar armazenar as chaves em um banco de dados ou outro tipo de índice, poderá convertê-lo em uma matriz de bytes .

Essas classes usam os dados de dobragem de caso padrão Unicode para determinar quais caracteres são equivalentes e oferecem suporte a várias estratégias de decomposição .

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"

Observe que os alceadores são específicos do local. Isso ocorre porque a "ordem alfabética" difere entre os locais (e mesmo com o tempo, como foi o caso do espanhol). A Collatoraula evita que você tenha que rastrear todas essas regras e mantê-las atualizadas.


parece interessante, mas você pode pesquisar sua chave de agrupamento no banco de dados com select * from person where collated_name like 'bjo%' ??
Andreas Petersson

muito bom, não sabia disso. vai tentar isso.
Andreas Petersson

No Android, as CollationKeys não podem ser usadas como prefixos para pesquisas de banco de dados. Uma chave de agrupamento da string se atransforma em bytes 41, 1, 5, 1, 5, 0, mas a string se abtransforma em bytes 41, 43, 1, 6, 1, 6, 0. Essas sequências de bytes não aparecem como estão por extenso (a matriz de bytes da chave de agrupamento anão aparece na matriz de bytes da chave de agrupamento ab)
Grzegorz Adam Hankiewicz

1
@GrzegorzAdamHankiewicz Após alguns testes, vejo que as matrizes de bytes podem ser comparadas, mas não formam prefixos, como você observou. Portanto, para fazer uma consulta de prefixo como bjo%, você precisa realizar uma consulta de intervalo em que os agrupadores são> = bjoe < bjp(ou qualquer que seja o próximo símbolo nesse local, e não há maneira programática de determinar isso).
Erickson

16

Faz parte do Apache Commons Lang desde a versão. 3.1.

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

retorna An


1
Para Ø dá novamente Ø
Mike Argyriou

2
Obrigado Mike por apontar isso. O método lida apenas com acentos. O resultado de "ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ" é "nnnnnnnnn ɲ ƞ ᶇ ɳ ȵ"
Kenston Choi

12

Você pode usar a classe Normalizer de java.text:

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

Mas ainda há algum trabalho a ser feito, já que Java faz coisas estranhas com caracteres Unicode não convertíveis (ele não os ignora e não lança uma exceção). Mas acho que você pode usar isso como ponto de partida.


3
isso não funcionará para diacríticos não ascii, como em russo, eles também têm diacríticos e, além disso, eliminam todas as strings asiáticas. não use. em vez de converter para ascii, use \\ p {InCombiningDiacriticalMarks} regexp como em answer stackoverflow.com/questions/1453171/…
Andreas Petersson,


4

Observe que nem todas essas marcas são apenas "marcas" em algum caractere "normal", que você pode remover sem alterar o significado.

Em sueco, å ä e ö são verdadeiros e próprios caracteres de primeira classe, não uma "variante" de algum outro caractere. Eles soam diferentes de todos os outros caracteres, eles são classificados de forma diferente e fazem as palavras mudarem de significado ("mätt" e "matt" são duas palavras diferentes).


4
Embora correto, este é mais um comentário do que uma resposta à pergunta.
Simon Forsberg,

2

O Unicode possui caracteres diatricos específicos (que são caracteres compostos) e uma string pode ser convertida para que o caractere e os diatrics sejam separados. Então, você pode simplesmente remover os diatricts da string e está basicamente feito.

Para obter mais informações sobre normalização, decomposições e equivalência, consulte O Padrão Unicode na página inicial do Unicode .

No entanto, como você pode realmente conseguir isso depende da estrutura / OS / ... na qual você está trabalhando. Se estiver usando .NET, você pode usar o método String.Normalize aceitando a enumeração System.Text.NormalizationForm .


2
Este é o método que uso no .NET, embora ainda precise mapear alguns caracteres manualmente. Eles não são diacríticos, mas dígrafos. Porém, problema semelhante.
Thorarin,

1
Converta para a forma de normalização "D" (ou seja, decomposto) e use o caractere base.
Richard,

2

A maneira mais fácil (para mim) seria simplesmente manter um array de mapeamento esparso que simplesmente muda seus pontos de código Unicode em strings exibíveis.

Tal como:

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

O uso de uma matriz esparsa permitirá que você represente substituições com eficiência, mesmo quando elas em seções amplamente espaçadas da tabela Unicode. As substituições de cordas permitirão que sequências arbitrárias substituam seus diacríticos (como a ætransformação do grafema ae).

Esta é uma resposta independente de idioma, portanto, se você tiver um idioma específico em mente, haverá maneiras melhores (embora provavelmente todos cheguem a isso nos níveis mais baixos de qualquer maneira).


Adicionar todos os possíveis caracteres estranhos não é uma tarefa fácil. Ao fazer isso para apenas alguns personagens, é uma boa solução.
Simon Forsberg

2

Algo a considerar: se você tentar obter uma única "tradução" para cada palavra, poderá perder algumas alternativas possíveis.

Por exemplo, em alemão, ao substituir o "s-set", algumas pessoas podem usar "B", enquanto outras podem usar "ss". Ou substituindo um o com trema "o" ou "oe". Qualquer solução que você encontrar, idealmente, acho que deve incluir ambos.


2

No Windows e .NET, acabei de converter usando a codificação de string. Dessa forma, evito o mapeamento e a codificação manuais.

Tente brincar com a codificação de strings.


3
Você pode elaborar sobre codificação de string? Por exemplo, com um exemplo de código.
Peter Mortensen

2

No caso do alemão, não se deseja remover os sinais diacríticos dos Umlauts (ä, ö, ü). Em vez disso, eles são substituídos por combinações de duas letras (ae, oe, ue). Por exemplo, Björn deve ser escrito como Bjoern (não Bjorn) para ter a pronúncia correta.

Para isso, prefiro um mapeamento codificado, onde você pode definir a regra de substituição individualmente para cada grupo de caracteres especiais.


0

Para referência futura, aqui está um método de extensão C # que remove acentos.

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");
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.