Eliminar las marcas diacríticas (ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ) de los caracteres Unicode


88

Estoy viendo un algoritmo que puede mapear entre caracteres con diacríticos ( tilde , circunflejo , signo de intercalación , diéresis , caron ) y su carácter "simple".

Por ejemplo:

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

Etc.

  1. Quiero hacer esto en Java, aunque sospecho que debería ser algo Unicode-y y debería ser razonablemente fácil en cualquier idioma.

  2. Finalidad: permitir la búsqueda sencilla de palabras con signos diacríticos. Por ejemplo, si tengo una base de datos de jugadores de tenis y se ingresa Björn_Borg, también conservaré Bjorn_Borg para poder encontrarlo si alguien ingresa a Bjorn y no a Björn.


Depende del entorno en el que esté programando, aunque probablemente tendrá que mantener algún tipo de tabla de mapeo manualmente. Entonces, ¿qué idioma estás usando?
Thorarin

15
Tenga en cuenta que algunas letras como ñ en.wikipedia.org/wiki/%C3%91 no deben eliminarse sus signos diacríticos para fines de búsqueda. Google diferencia correctamente entre el español "ano" (ano) y "año" (año). Entonces, si realmente desea un buen motor de búsqueda, no puede confiar en la eliminación básica de marcas diacríticas.
Eduardo

@Eduardo: En un contexto dado, eso podría no importar. Usando el ejemplo que dio el OP, al buscar el nombre de una persona en un contexto multinacional, realmente desea que la búsqueda no sea demasiado precisa.
Amir Abiri

(Enviado accidentalmente anterior) Sin embargo, hay espacio para mapear signos diacríticos a sus equivalentes fonéticos para mejorar la búsqueda fonética. es decir, ñ => ni producirá mejores resultados si el motor de búsqueda subyacente admite búsquedas basadas en fonética (por ejemplo, soundex)
Amir Abiri

Un caso de uso en el que cambiar año a ano, etc.es eliminar caracteres que no sean base64 para URL, ID, etc.
Ondra Žižka

Respuestas:


82

He hecho esto recientemente en 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;
}

Esto hará lo que especificó:

stripDiacritics("Björn")  = Bjorn

pero fallará, por ejemplo, en Białystok, porque el łcarácter no es diacrítico.

Si desea tener un simplificador de cadenas completo, necesitará una segunda ronda de limpieza, para algunos caracteres más especiales que no sean diacríticos. Es este mapa, he incluido los caracteres especiales más comunes que aparecen en los nombres de nuestros clientes. No es una lista completa, pero le dará una idea de cómo ampliarla. ImmutableMap es solo una clase simple de 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;
    }
}

¿qué pasa con personajes como ╨?
mickthompson

se pasarán sin embargo. también todos los caracteres japoneses, etc.
Andreas Petersson

gracias Andreas. ¿Hay alguna forma de eliminarlos? Los caracteres como ら が な を 覚 男 (u otros) se incluirán en la cadena generada y estos básicamente romperán la salida. Estoy tratando de usar la salida simplifiedString como un generador de URL como lo hace StackOverflow para las URL de sus preguntas.
mickthompson

2
Como dije en el comentario de la pregunta. No puede confiar en la eliminación básica de marcas diacríticas si desea un buen motor de búsqueda.
Eduardo

3
¡Gracias Andreas, funciona como un encanto! (probado en 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

El paquete central java.text fue diseñado para abordar este caso de uso (cadenas de coincidencia sin importar los signos diacríticos, mayúsculas y minúsculas, etc.).

Configure a Collatorpara clasificar las PRIMARYdiferencias en los caracteres. Con eso, crea un CollationKeypara cada cadena. Si todo su código está en Java, puede usar CollationKeydirectamente. Si necesita almacenar las claves en una base de datos u otro tipo de índice, puede convertirlo en una matriz de bytes .

Estas clases utilizan los datos de plegado de casos estándar de Unicode para determinar qué caracteres son equivalentes y admiten varias estrategias de descomposición .

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"

Tenga en cuenta que los clasificadores son específicos de la configuración regional. Esto se debe a que el "orden alfabético" difiere entre los lugares (e incluso a lo largo del tiempo, como ha sido el caso del español). La Collatorclase te libera de tener que hacer un seguimiento de todas estas reglas y mantenerlas actualizadas.


suena interesante, pero ¿puede buscar su clave de intercalación en la base de datos con seleccionar * de la persona donde collated_name como 'bjo%'?
Andreas Petersson

muy bonito, no sabía nada de eso. probará esto.
Andreas Petersson

En Android, las CollationKeys no se pueden usar como prefijos para búsquedas en bases de datos. Una clave de clasificación de la cadena se aconvierte en los bytes 41, 1, 5, 1, 5, 0, pero la cadena se abconvierte en los bytes 41, 43, 1, 6, 1, 6, 0. Estas secuencias de bytes no aparecen tal cual en palabras completas (la matriz de bytes para la clave de clasificación ano aparece en la matriz de bytes para la clave de clasificación para ab)
Grzegorz Adam Hankiewicz

1
@GrzegorzAdamHankiewicz Después de algunas pruebas, veo que las matrices de bytes se pueden comparar, pero no forman prefijos, como señaló. Entonces, para hacer una consulta de prefijo como bjo%, necesitaría realizar una consulta de rango donde los clasificadores son> = bjoy < bjp(o cualquiera que sea el siguiente símbolo en esa configuración regional, y no hay una forma programática de determinar eso).
erickson

16

Es parte de Apache Commons Lang a partir de la versión. 3.1.

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

devoluciones An


1
Para Ø da de nuevo Ø
Mike Argyriou

2
Gracias Mike por señalar eso. El método solo maneja acentos. El resultado de "ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ" es "nnnnnnnnn ɲ ƞ ᶇ ɳ ȵ"
Kenston Choi

12

Puede usar la clase Normalizador de java.text:

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

Pero aún queda trabajo por hacer, ya que Java hace cosas extrañas con caracteres Unicode inconvertibles (no los ignora y no lanza una excepción). Pero creo que podrías usar eso como punto de partida.


3
esto no funcionará para diacríticos que no sean ascii, como en ruso, también tienen diacríticos y, además, matan todas las cadenas asiáticas. no utilice. en lugar de convertir a ascii, use \\ p {InCombiningDiacriticalMarks} regexp como en answer stackoverflow.com/questions/1453171/…
Andreas Petersson


5

Tenga en cuenta que no todas estas marcas son sólo "marcas" en algún carácter "normal", que puede eliminar sin cambiar el significado.

En sueco, å ä y ö son verdaderos y propios caracteres de primera clase, no una "variante" de algún otro carácter. Suenan diferentes de todos los demás caracteres, se clasifican de manera diferente y hacen que las palabras cambien de significado ("mätt" y "mate" son dos palabras diferentes).


4
Aunque es correcto, esto es más un comentario que una respuesta a la pregunta.
Simon Forsberg

2

Unicode tiene caracteres diatricos específicos (que son caracteres compuestos) y una cadena se puede convertir para separar el carácter y la diatría. Luego, puede simplemente quitar los diatricts de la cuerda y básicamente está listo.

Para obtener más información sobre normalización, descomposición y equivalencia, consulte El estándar Unicode en la página de inicio de Unicode .

Sin embargo, cómo puede lograrlo depende del marco / sistema operativo / ... en el que esté trabajando. Si usa .NET, puede usar el método String.Normalize aceptando la enumeración System.Text.NormalizationForm .


2
Este es el método que utilizo en .NET, aunque todavía tengo que mapear algunos caracteres manualmente. No son diacríticos, sino dígrafos. Sin embargo, un problema similar.
Thorarin

1
Convierta a la forma de normalización "D" (es decir, descompuesta) y tome el carácter base.
Richard

2

La forma más fácil (para mí) sería simplemente mantener una matriz de mapeo dispersa que simplemente cambia sus puntos de código Unicode en cadenas visualizables.

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

El uso de una matriz dispersa le permitirá representar reemplazos de manera eficiente incluso cuando estén en secciones muy espaciadas de la tabla Unicode. Los reemplazos de cadenas permitirán que secuencias arbitrarias reemplacen sus signos diacríticos (como el ægrafema ae).

Esta es una respuesta independiente del idioma, por lo que, si tiene un idioma específico en mente, habrá mejores formas (aunque es probable que todas se reduzcan a esto en los niveles más bajos de todos modos).


Sumar todos los posibles personajes extraños no es tarea fácil. Al hacer esto solo para unos pocos caracteres, es una buena solución.
Simon Forsberg

2

Algo a tener en cuenta: si sigue la ruta de intentar obtener una única "traducción" de cada palabra, es posible que se pierda algunas alternativas posibles.

Por ejemplo, en alemán, al reemplazar el "s-set", algunas personas pueden usar "B", mientras que otras pueden usar "ss". O, reemplazando una o con diéresis por "o" u "oe". Cualquier solución que se te ocurra, idealmente, creo que debería incluir ambos.


2

En Windows y .NET, solo convierto usando codificación de cadena. De esa manera evito el mapeo y la codificación manuales.

Intenta jugar con la codificación de cadenas.


3
¿Puedes dar más detalles sobre la codificación de cadenas? Por ejemplo, con un ejemplo de código.
Peter Mortensen

2

En el caso del alemán, no se desea eliminar los signos diacríticos de las diéresis (ä, ö, ü). En su lugar, se reemplazan por una combinación de dos letras (ae, oe, ue). Por ejemplo, Björn debe escribirse como Bjoern (no como Bjorn) para tener una pronunciación correcta.

Para eso, preferiría un mapeo codificado, donde puede definir la regla de reemplazo individualmente para cada grupo de caracteres especiales.


0

Para referencia futura, aquí hay un método de extensión de C # que elimina los 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");
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.