¿Hay alguna manera de deshacerse de los acentos y convertir una cadena completa en letras regulares?

263

¿Hay una mejor manera de deshacerse de los acentos y hacer que esas letras sean regulares, aparte de usar el String.replaceAll()método y reemplazar las letras una por una? Ejemplo:

Entrada: orčpžsíáýd

Salida: orcpzsiayd

No necesita incluir todas las letras con acentos como el alfabeto ruso o el chino.

Martín
fuente

Respuestas:

387

Úselo java.text.Normalizerpara manejar esto por usted.

string = Normalizer.normalize(string, Normalizer.Form.NFD);
// or Normalizer.Form.NFKD for a more "compatable" deconstruction 

Esto separará todas las marcas de acento de los personajes. Luego, solo necesita comparar cada carácter con una letra y desechar los que no lo son.

string = string.replaceAll("[^\\p{ASCII}]", "");

Si su texto está en Unicode, debe usar esto en su lugar:

string = string.replaceAll("\\p{M}", "");

Para Unicode, \\P{M}coincide con el glifo base y \\p{M}(en minúsculas) coincide con cada acento.

Gracias a GarretWilson por el puntero y regular-expressions.info por la gran guía Unicode.

Erick Robertson
fuente
77
Esto compila la expresión regular cada vez, lo cual está bien si solo lo necesita una vez, pero si necesita hacerlo con mucho texto, compilar previamente la expresión regular es una victoria.
David Conrad
3
Tenga en cuenta que no todas las letras en latín se descomponen en acentos ASCII +. Esto matará, por ejemplo. "Letra latina {mayúscula, pequeña} l con trazo" utilizada en polaco.
Michał Politowski
12
Este es un buen enfoque, pero eliminar todos los caracteres que no son ASCII es excesivo y probablemente eliminará cosas que no desea, como lo han indicado otros. Sería mejor eliminar todas las "marcas" Unicode; incluidas las marcas sin espaciado, las marcas de espaciado / combinación y las marcas de cierre. Puedes hacer esto con string.replaceAll("\\p{M}", ""). Consulte regular-expressions.info/unicode.html para obtener más información.
Garret Wilson el
44
Probablemente desee utilizar Normalizer.Form.NFKD en lugar de NFD: NFKD convertirá cosas como las ligaduras en caracteres ASCII (p. Ej., Fi a fi), NFD no lo hará.
chesterm8
2
@ chesterm8, curiosamente, NFKD está convirtiendo "fi" en "fi", pero no está convirtiendo "Æ" en "AE". Supongo que tendré que mostrar los datos de Unicode para averiguar por qué, pero no era lo que esperaba.
Garret Wilson
136

A partir de 2011, puede usar Apache Commons StringUtils.stripAccents (input) (desde 3.0):

    String input = StringUtils.stripAccents("Tĥïŝ ĩš â fůňķŷ Šťŕĭńġ");
    System.out.println(input);
    // Prints "This is a funky String"

Nota:

La respuesta aceptada (Erick Robertson) no funciona para Ø o Ł. Apache Commons 3.5 tampoco funciona para Ø, pero funciona para Ł. Después de leer el artículo de Wikipedia para Ø , no estoy seguro de que deba reemplazarse con "O": es una letra separada en noruego y danés, alfabetizada después de "z". Es un buen ejemplo de las limitaciones del enfoque de "acentos de tira".

DavidS
fuente
2
Veo que hay un informe de error abierto para Ł , @KarolS. Alguien envió una solicitud de extracción, pero falló algunas pruebas y no se ha actualizado desde julio del año pasado.
DavidS
1
Hubo una actualización hace 5 días y la solicitud de extracción se fusionó.
EpicPandaForce
66
Commons Lang 3.5 fue lanzado hace varios días. Confirmé que funciona en Ł ahora. No funciona en Ø. Al leer el artículo de Wiki para Ø , no estoy seguro de que deba reemplazarse con "O": es una letra separada en noruego y danés, alfabetizada después de "z". Es un buen ejemplo de las limitaciones del enfoque de "acentos de tira".
DavidS
2
Si no desea incluir la biblioteca, puede tomar los dos métodos involucrados en esa característica fácilmente desde la fuente en commons.apache.org/proper/commons-lang/apidocs/src-html/org/…
lujop
2
Como danés, el danés / noruego ø al igual que el francés œ y el alemán / sueco / húngaro / estonio, etc. ö se origina como una forma corta de escribir oe. Entonces, dependiendo de su propósito, esta puede ser la sustitución que desee.
Ole VV
57

La solución de @ virgo47 es muy rápida, pero aproximada. La respuesta aceptada usa Normalizer y una expresión regular. Me preguntaba qué parte del tiempo tomó Normalizer versus la expresión regular, ya que la eliminación de todos los caracteres no ASCII se puede hacer sin una expresión regular:

import java.text.Normalizer;

public class Strip {
    public static String flattenToAscii(String string) {
        StringBuilder sb = new StringBuilder(string.length());
        string = Normalizer.normalize(string, Normalizer.Form.NFD);
        for (char c : string.toCharArray()) {
            if (c <= '\u007F') sb.append(c);
        }
        return sb.toString();
    }
}

Se pueden obtener pequeñas aceleraciones adicionales escribiendo en un char [] y no llamando aCharArray (), aunque no estoy seguro de que la disminución en la claridad del código lo merezca:

public static String flattenToAscii(String string) {
    char[] out = new char[string.length()];
    string = Normalizer.normalize(string, Normalizer.Form.NFD);
    int j = 0;
    for (int i = 0, n = string.length(); i < n; ++i) {
        char c = string.charAt(i);
        if (c <= '\u007F') out[j++] = c;
    }
    return new String(out);
}

Esta variación tiene la ventaja de la corrección de la que usa Normalizer y algo de la velocidad de la que usa una tabla. En mi máquina, esta es aproximadamente 4 veces más rápida que la respuesta aceptada, y 6.6x a 7x más lenta que @ virgo47 (la respuesta aceptada es aproximadamente 26 veces más lenta que @ virgo47 en mi máquina).

David Conrad
fuente
2
outdebe redimensionarse para que coincida con el número de caracteres válidos jantes de que se use para construir el objeto de cadena.
Lefteris E
44
Tengo una objeción a esta solución. Imagine la entrada "æøåá". Current flattenToAsciicrea el resultado "aa .." donde los puntos representan \ u0000. Eso no es bueno. La primera pregunta es: ¿cómo representar los caracteres "no normalizables"? Digamos que será?, O podemos dejar NULL char allí, pero en cualquier caso tenemos que preservar la posición correcta de estos (al igual que lo hace la solución regex). Para esto, el if en el bucle debe ser algo así como: if (c <= '\u007F') out[j++] = c; else if (Character.isLetter(c)) out[j++] = '?';lo ralentizará un poco, pero debe ser correcto en primer lugar. ;-)
virgo47
Ad mi último comentario (lástima que no puedan durar más) - tal vez la toma positiva ( isLetter) no es la correcta, pero no encontré nada mejor. No soy un experto en Unicode, por lo que no sé cómo identificar mejor la clase del carácter único que reemplaza al carácter original. Las letras funcionan bien para la mayoría de las aplicaciones / usos.
virgo47
1
Probablemente desee utilizar Normalizer.Form.NFKD en lugar de NFD: NFKD convertirá cosas como las ligaduras en caracteres ASCII (p. Ej., Fi a fi), NFD no lo hará.
chesterm8
2
Para nosotros queríamos eliminar el personaje por completo. Para asegurar que no hubiera caracteres nulos finales, los eliminé con un constructor de cadenas alternativo: return new String (out, 0, j);
Mike Samaras
30

EDITAR: Si no está atascado con Java <6 y la velocidad no es crítica y / o la tabla de traducción es demasiado limitante, use la respuesta de David. El punto es usar Normalizer(introducido en Java 6) en lugar de la tabla de traducción dentro del bucle.

Si bien esta no es una solución "perfecta", funciona bien cuando conoce el rango (en nuestro caso Latin1,2), funcionó antes de Java 6 (aunque no es un problema real) y es mucho más rápido que la versión más sugerida (puede o puede no sea un problema):

    /**
 * Mirror of the unicode table from 00c0 to 017f without diacritics.
 */
private static final String tab00c0 = "AAAAAAACEEEEIIII" +
    "DNOOOOO\u00d7\u00d8UUUUYI\u00df" +
    "aaaaaaaceeeeiiii" +
    "\u00f0nooooo\u00f7\u00f8uuuuy\u00fey" +
    "AaAaAaCcCcCcCcDd" +
    "DdEeEeEeEeEeGgGg" +
    "GgGgHhHhIiIiIiIi" +
    "IiJjJjKkkLlLlLlL" +
    "lLlNnNnNnnNnOoOo" +
    "OoOoRrRrRrSsSsSs" +
    "SsTtTtTtUuUuUuUu" +
    "UuUuWwYyYZzZzZzF";

/**
 * Returns string without diacritics - 7 bit approximation.
 *
 * @param source string to convert
 * @return corresponding string without diacritics
 */
public static String removeDiacritic(String source) {
    char[] vysl = new char[source.length()];
    char one;
    for (int i = 0; i < source.length(); i++) {
        one = source.charAt(i);
        if (one >= '\u00c0' && one <= '\u017f') {
            one = tab00c0.charAt((int) one - '\u00c0');
        }
        vysl[i] = one;
    }
    return new String(vysl);
}

Las pruebas en mi HW con JDK de 32 bits muestran que esto realiza la conversión de àèéľšťč89FDČ a aeelstc89FDC 1 millón de veces en ~ 100 ms, mientras que el modo Normalizador lo hace en 3.7 s (37 veces más lento). En caso de que sus necesidades estén relacionadas con el rendimiento y conozca el rango de entrada, esto puede ser para usted.

Disfruta :-)

virgo47
fuente
1
Gran parte de la lentitud de la versión sugerida se debe a la expresión regular, no al Normalizador. Usar Normalizer pero eliminar los caracteres no ASCII 'a mano' es más rápido, aunque aún no tan rápido como su versión. Pero funciona para todo Unicode en lugar de solo latin1 y latin2.
David Conrad
Extendí esto para trabajar con más caracteres, pastebin.com/FAAm6a2j , tenga en cuenta que no funcionará correctamente con caracteres multichar como DŽ (DZ). Solo producirá 1 personaje a partir de él. Además, mi función usa char en lugar de cadenas, que es más rápido SI está manejando char de todos modos, por lo que no tiene que convertir.
James T
Oye, no entiendo qué significan esas letras en el campo tab00c0. por ejemplo "AAAAAAACEEEEIIII" o "lLlNnNnNnnNnOoOo", etc. Nunca los había visto antes. ¿Dónde los encontraste? Además, ¿por qué no usas los códigos correspondientes?
ThanosFisherman
@ThanosF solo intenta pasar por el código (con depurador si es necesario). Lo que esto hace es para cada carácter en una cadena: "¿Es este carácter entre \ u00c0 y \ u017f? Si es así, reemplácelo con un carácter ASCII de 7 bits de la tabla". La tabla solo cubre dos páginas de codificación (latín 1 y 2) con sus equivalentes de 7 bits. Entonces, si es un carácter con el código \ u00e0 (à) tomará su aproximación de 7 bits desde la posición 32 de la tabla (e0-c0 = 32), eso es "a". Algunos caracteres no son letras, se dejan allí con su código.
virgo47
Gracias por tu explicación. ¿Dónde puedo encontrar esas páginas de codificación para poder extender esta variable a mi idioma? (Griego) La respuesta aceptada ya hace el trabajo reemplazando las letras griegas acentuadas, pero también quería probar su método y ejecutar algunos puntos de referencia :)
ThanosFisherman
22
System.out.println(Normalizer.normalize("àèé", Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", ""));

trabajó para mi. La salida del fragmento anterior da "aee", que es lo que quería, pero

System.out.println(Normalizer.normalize("àèé", Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", ""));

no hizo ninguna sustitución.

Nico
fuente
1
Confirmando esto ... normalmente ASCII funciona bien, pero encontré este problema en Linux (64b) con JRockit (1.6.0_29 64b). No puedo confirmarlo con ninguna otra configuración, no puedo confirmar esa correlación, pero puedo confirmar que la otra solución sugerida funcionó y por eso voté por esta. :-) (Por cierto: reemplazó un poco, pero no lo suficiente, cambió Ú a U por ejemplo, pero no á a a.)
virgo47
1
Probablemente desee utilizar Normalizer.Form.NFKD en lugar de NFD: NFKD convertirá cosas como las ligaduras en caracteres ASCII (p. Ej., Fi a fi), NFD no lo hará.
chesterm8
@KarolS no veo a ninguno de ellos contiene ningún acentos
EIS
@eis Una barra diagonal en una letra cuenta como un signo diacrítico: en.wikipedia.org/wiki/Diacritic Y si sigue una definición más estricta de un "acento" como en esa página de Wikipedia, entonces la diaeresis no es un acento, así que la respuesta de Nico Todavía está mal.
Karol S
6

Dependiendo del idioma, puede que no se consideren acentos (que cambian el sonido de la letra), sino signos diacríticos.

https://en.wikipedia.org/wiki/Diacritic#Languages_with_letters_containing_diacritics

"Bosnio y croata tienen los símbolos č, ć, đ, š y ž, que se consideran letras separadas y se enumeran como tales en los diccionarios y otros contextos en los que las palabras se enumeran según el orden alfabético".

Eliminarlos podría estar cambiando inherentemente el significado de la palabra, o cambiar las letras a letras completamente diferentes.

NinjaCat
fuente
55
Convenido. Por ejemplo en sueco: "höra" (escuchar) -> "hora" (puta)
Christoffer Hammarström
14
No importa lo que signifiquen. La pregunta es cómo eliminarlos.
Erick Robertson
77
Erick: Importa cómo se llaman. Si la pregunta pregunta cómo eliminar los acentos, y si esos no son acentos, entonces la respuesta puede no ser solo cómo eliminar todas esas cosas que parecen acentos. Aunque esto probablemente debería ser un comentario y no una respuesta.
Smig
44
Creo que el caso de uso normal para esto es la búsqueda, particularmente la búsqueda de idiomas mixtos, a menudo con un teclado en inglés como entrada, en cuyo caso es mejor obtener falsos positivos que falsos negativos.
nilskp
3

Me he enfrentado al mismo problema relacionado con la verificación de igualdad de cadenas. Una de las cadenas de comparación tiene el código de caracteres ASCII 128-255 .

es decir, Espacio sin ruptura - [Hex - A0] Espacio [Hex - 20]. Para mostrar espacio sin interrupciones sobre HTML. He usado lo siguiente spacing entities. Su carácter y sus bytes son como&emsp is very wide space[ ]{-30, -128, -125}, &ensp is somewhat wide space[ ]{-30, -128, -126}, &thinsp is narrow space[ ]{32} , Non HTML Space {}

String s1 = "My Sample Space Data", s2 = "My Sample Space Data";
System.out.format("S1: %s\n", java.util.Arrays.toString(s1.getBytes()));
System.out.format("S2: %s\n", java.util.Arrays.toString(s2.getBytes()));

Salida en bytes:

S1: [77, 121, 3283, 97, 109, 112, 108, 101, 3283, 112, 97, 99, 101 32, 68, 97, 116, 97] S2: [77, 121 -30, -128, -125, 83, 97, 109, 112, 108, 101, -30, -128, -12583, 112, 97, 99, 101 -30, -128, -125, 68, 97, 116, 97]

Use el siguiente código para diferentes espacios y sus códigos de bytes: wiki for List_of_Unicode_characters

String spacing_entities = "very wide space,narrow space,regular space,invisible separator";
System.out.println("Space String :"+ spacing_entities);
byte[] byteArray = 
    // spacing_entities.getBytes( Charset.forName("UTF-8") );
    // Charset.forName("UTF-8").encode( s2 ).array();
    {-30, -128, -125, 44, -30, -128, -126, 44, 32, 44, -62, -96};
System.out.println("Bytes:"+ Arrays.toString( byteArray ) );
try {
    System.out.format("Bytes to String[%S] \n ", new String(byteArray, "UTF-8"));
} catch (UnsupportedEncodingException e) {
    e.printStackTrace();
}
  • ➩ Transliteraciones ASCII de cadena Unicode para Java. unidecode

    String initials = Unidecode.decode( s2 );
  • ➩ utilizando Guava: Google Core Libraries for Java.

    String replaceFrom = CharMatcher.WHITESPACE.replaceFrom( s2, " " );

    Para la codificación de URL para el espacio, use Guava laibrary.

    String encodedString = UrlEscapers.urlFragmentEscaper().escape(inputString);
  • Overcome Para superar este problema utilizado String.replaceAll()con algunos RegularExpression.

    // \p{Z} or \p{Separator}: any kind of whitespace or invisible separator.
    s2 = s2.replaceAll("\\p{Zs}", " ");
    
    
    s2 = s2.replaceAll("[^\\p{ASCII}]", " ");
    s2 = s2.replaceAll(" ", " ");
  • Usando java.text.Normalizer.Form . Esta enumeración proporciona constantes de las cuatro formas de normalización Unicode que se describen en el Anexo estándar Unicode # 15 - Formularios de normalización Unicode y dos métodos para acceder a ellos.

    ingrese la descripción de la imagen aquí

    s2 = Normalizer.normalize(s2, Normalizer.Form.NFKC);

Prueba de cadenas y salidas en diferentes enfoques como ➩ Unidecode, Normalizer, StringUtils .

String strUni = "Tĥïŝ ĩš â fůňķŷ Šťŕĭńġ Æ,Ø,Ð,ß";

// This is a funky String AE,O,D,ss
String initials = Unidecode.decode( strUni );

// Following Produce this o/p: Tĥïŝ ĩš â fůňķŷ Šťŕĭńġ Æ,Ø,Ð,ß
String temp = Normalizer.normalize(strUni, Normalizer.Form.NFD);
Pattern pattern = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");
temp = pattern.matcher(temp).replaceAll("");

String input = org.apache.commons.lang3.StringUtils.stripAccents( strUni );

El uso de Unidecode es el best choice, Mi código final que se muestra a continuación.

public static void main(String[] args) {
    String s1 = "My Sample Space Data", s2 = "My Sample Space Data";
    String initials = Unidecode.decode( s2 );
    if( s1.equals(s2)) { //[ , ] %A0 - %2C - %20 « http://www.ascii-code.com/
        System.out.println("Equal Unicode Strings");
    } else if( s1.equals( initials ) ) {
        System.out.println("Equal Non Unicode Strings");
    } else {
        System.out.println("Not Equal");
    }

}
Yash
fuente
3

Sugiero Junidecode . Manejará no solo 'Ł' y 'Ø', sino que también funciona bien para la transcripción de otros alfabetos, como el chino, al alfabeto latino.

OlgaMaciaszek
fuente
1
Parece prometedor, pero desearía que este fuera un proyecto más activo / mantenido y disponible en Maven.
Phil
2

La solución @David Conrad es la más rápida que intenté usar el Normalizador, pero tiene un error. Básicamente, elimina los caracteres que no son acentos, por ejemplo, los caracteres chinos y otras letras como æ, se eliminan todos. Los caracteres que queremos eliminar son marcas sin espacios, caracteres que no ocupan un ancho adicional en la cadena final. Estos caracteres de ancho cero básicamente terminan combinados en algún otro carácter. Si puede verlos aislados como un personaje, por ejemplo así `, supongo que se combina con el carácter de espacio.

public static String flattenToAscii(String string) {
    char[] out = new char[string.length()];
    String norm = Normalizer.normalize(string, Normalizer.Form.NFD);

    int j = 0;
    for (int i = 0, n = norm.length(); i < n; ++i) {
        char c = norm.charAt(i);
        int type = Character.getType(c);

        //Log.d(TAG,""+c);
        //by Ricardo, modified the character check for accents, ref: http://stackoverflow.com/a/5697575/689223
        if (type != Character.NON_SPACING_MARK){
            out[j] = c;
            j++;
        }
    }
    //Log.d(TAG,"normalized string:"+norm+"/"+new String(out));
    return new String(out);
}
Ricardo Freitas
fuente
1

Una de las mejores maneras de usar regex y Normalizer si no tiene una biblioteca es:

    public String flattenToAscii(String s) {
                if(s == null || s.trim().length() == 0)
                        return "";
                return Normalizer.normalize(s, Normalizer.Form.NFD).replaceAll("[\u0300-\u036F]", "");
}

Esto es más eficiente que replaceAll ("[^ \ p {ASCII}]", "")) y si no necesita signos diacríticos (como su ejemplo).

De lo contrario, debe usar el patrón p {ASCII}.

Saludos.

Zhar
fuente
0

Creo que la mejor solución es convertir cada carácter a HEX y reemplazarlo con otro HEX. Es porque hay 2 tipos de Unicode:

Composite Unicode
Precomposed Unicode

Por ejemplo, "Ồ" escrito por Unicode compuesto es diferente de "Ồ" escrito por Unicode precompuesto. Puede copiar mis caracteres de muestra y convertirlos para ver la diferencia.

In Composite Unicode, "Ồ" is combined from 2 char: Ô (U+00d4) and ̀ (U+0300)
In Precomposed Unicode, "Ồ" is single char (U+1ED2)

Desarrollé esta función para que algunos bancos conviertan la información antes de enviarla al banco central (por lo general, no son compatibles con Unicode) y me enfrenté a este problema cuando los usuarios finales usan la tipificación Unicode múltiple para ingresar los datos. Entonces, creo, convertir a HEX y reemplazarlo es la forma más confiable.

Hoang Tran
fuente
-1

En caso de que alguien esté luchando por hacer esto en Kotlin, este código funciona de maravilla. Para evitar inconsistencias, también uso .toUpperCase y Trim (). entonces lanzo esta función:

   fun stripAccents(s: String):String{

   if (s == null) {
      return "";
   }

val chars: CharArray = s.toCharArray()

var sb = StringBuilder(s)
var cont: Int = 0

while (chars.size > cont) {
    var c: kotlin.Char
    c = chars[cont]
    var c2:String = c.toString()
   //these are my needs, in case you need to convert other accents just Add new entries aqui
    c2 = c2.replace("Ã", "A")
    c2 = c2.replace("Õ", "O")
    c2 = c2.replace("Ç", "C")
    c2 = c2.replace("Á", "A")
    c2 = c2.replace("Ó", "O")
    c2 = c2.replace("Ê", "E")
    c2 = c2.replace("É", "E")
    c2 = c2.replace("Ú", "U")

    c = c2.single()
    sb.setCharAt(cont, c)
    cont++

}

return sb.toString()

}

para usar estos divertidos, lanza el código de esta manera:

     var str: String
     str = editText.text.toString() //get the text from EditText
     str = str.toUpperCase().trim()

     str = stripAccents(str) //call the function
Thiago Silva
fuente