probar - ¿Cómo agregar características que faltan en la implementación de expresiones regulares de Java?
extraer cadenas con expresiones regulares java (4)
Algunas de las fallas API mencionadas en la respuesta de @christ se arreglaron en Kotlin .
Soy nuevo en Java. Como desarrollador de .Net, estoy muy acostumbrado a la clase Regex
en .Net. La implementación de Java de Regex
(expresiones regulares) no está mal, pero le faltan algunas características clave.
Quería crear mi propia clase de ayuda para Java pero pensé que quizás ya había una disponible. Entonces, ¿hay algún producto gratuito y fácil de usar disponible para Regex en Java o debería crear uno yo mismo?
Si escribiera mi propia clase, ¿dónde cree que debería compartirla para que otros la usen?
[Editar]
Hubo quejas de que no estaba abordando el problema con la clase Regex
actual. Trataré de aclarar mi pregunta.
En .Net, el uso de una expresión regular es más fácil que en Java. Como ambos lenguajes están orientados a objetos y muy similares en muchos aspectos, espero tener una experiencia similar con el uso de expresiones regulares en ambos idiomas. Desafortunadamente ese no es el caso.
Aquí hay un pequeño código comparado en Java y C #. El primero es C # y el segundo es Java:
Cª#:
string source = "The colour of my bag matches the color of my shirt!";
string pattern = "colou?r";
foreach(Match match in Regex.Matches(source, pattern))
{
Console.WriteLine(match.Value);
}
En Java:
String source = "The colour of my bag matches the color of my shirt!";
String pattern = "colou?r";
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(source);
while(m.find())
{
System.out.println(source.substring(m.start(), m.end()));
}
Traté de ser justo con ambos idiomas en el código de muestra anterior.
Lo primero que notará aquí es el miembro .Value
de la clase Match
(en comparación con el uso de .start()
y .end()
en Java).
¿Por qué debería crear dos objetos cuando puedo llamar a una función estática como Regex.Matches
o Regex.Match
, etc.?
En usos más avanzados, la diferencia se muestra mucho más. Mire el método Groups
, longitud del diccionario, Capture
, Index
, Length
, Success
, etc. Todas estas son características muy necesarias que en mi opinión deberían estar disponibles también para Java.
Por supuesto, todas estas características se pueden agregar manualmente mediante una clase proxy (auxiliar) personalizada. Esta es la razón principal por la que hice esta pregunta. No tenemos la brisa de Regex
en Perl, pero al menos podemos utilizar el enfoque .Net para Regex
que creo que está muy bien diseñado.
Chico, ¿te escucho en ese Alireza? Los Regex son lo suficientemente confusos sin que haya tantas variaciones de sintaxis entre ellos. Yo también hago mucho más C # que la programación Java y tuve el mismo problema.
Encontré esto muy útil: http://www.tusker.org/regex/regex_benchmark.html - es una lista de implementaciones alternativas de expresiones regulares para Java, comparadas.
De su ejemplo editado, ahora puedo ver lo que le gustaría. Y también tienes mis simpatías en esto. Las expresiones regulares de Java están muy, muy, muy lejos de la comodidad que se encuentra en los lenguajes de programación de nivel superior como Ruby o Perl. Y casi siempre lo serán; esto no se puede arreglar, así que estamos atrapados con este lío para siempre, al menos en Java. Otros lenguajes JVM hacen un mejor trabajo en esto, especialmente Groovy. Pero aún sufren algunos de los defectos inherentes, y solo pueden ir tan lejos.
¿Dónde empezar? Existen los llamados métodos de conveniencia de la clase String: matches
, replaceAll
, replaceFirst
y split
. En ocasiones, estos programas pueden estar bien en programas pequeños, dependiendo de cómo los use. Sin embargo, sí tienen varios problemas, que parece que has descubierto. Aquí hay una lista parcial de esos problemas, y qué se puede y no se puede hacer con ellos.
El método de la inconveniencia se denomina "coincidencias" de manera muy extraña, pero requiere rellenar tu expresión regular en ambos lados para que coincida con toda la cadena. Este sentido contrario a la intuición es contrario a cualquier sentido de la combinación de palabras como se usa en cualquier lenguaje anterior, y constantemente muerde a las personas. Los patrones pasados a los otros 3 métodos de inconvenientes funcionan muy diferente a este, porque en los otros 3, funcionan como patrones normales en todas partes; simplemente no en los
matches
. Esto significa que no puedes simplemente copiar tus patrones, incluso dentro de los métodos en la misma clase maldita por amor de Dios. Y no hay método defind
conveniencia para hacer lo que hacen todos los demás competidores del mundo. El método dematches
debería haberse llamado algo así comoFullMatch
, y debería haber sido un métodoPartialMatch
ofind
agregado a la clase String.No hay API que le permita pasar indicadores de
Pattern.compile
junto con las cadenas que utiliza para los 4 métodos de conveniencia relacionados con los patrones de la clase String. Eso significa que puede confiar en versiones de cadenas como(?i)
y(?x)
, pero esas no existen para todos los posibles indicadores de compilación de patrones. Esto es muy inconveniente por decir lo menos.El método de
split
no devuelve el mismo resultado en casos de borde que los retornossplit
en los idiomas divididos por Java. Este es un poco astuto gotcha. ¿Cuántos elementos crees que deberías volver a la lista de devoluciones si dividiste la cadena vacía, eh? Los fabricantes de Java tienen un elemento de devolución falso donde debería haber uno, lo que significa que no se puede distinguir entre los resultados legítimos y los falsos. Es un error de diseño serio que se divida en un":"
, no se puede distinguir entre las entradas de""
vs de":"
. Aw, ¡gee! ¿La gente nunca prueba esto? Y, de nuevo, el comportamiento roto y fundamentalmente no confiable no se puede corregir: nunca debes cambiar las cosas, incluso las quebrantadas. No está bien romper cosas rotas en Java de la misma manera que en cualquier otro lado. Broken está para siempre aquí.La notación de barra diagonal inversa de expresiones regulares entra en conflicto con la notación de barra invertida utilizada en cadenas. Esto lo hace superdupe incomodo y propenso a errores, también, porque tiene que agregar constantemente muchas barras diagonales inversas a todo, y es muy fácil olvidar uno y no recibir ni advertencia ni éxito. Los patrones simples como
/b/w+/b
convierten en pesadillas en exceso tipográfico:"//b//w+//b"
. Buena suerte al leer eso. Algunas personas usan una función de barra diagonal en sus patrones para que puedan escribir eso como"/b/w+/b"
lugar. Además de leer en sus patrones de una cadena, no hay forma de construir su patrón en una forma WYSIWYG literal; siempre está cargado de barras invertidas. ¿Los obtuviste a todos, y suficiente, y en los lugares correctos? Si es así, es realmente muy difícil de leer. Si no es así, probablemente no los hayas conseguido todos. Al menos los lenguajes JVM como Groovy han encontrado la respuesta correcta aquí: dale a la gente expresiones regulares de primera clase para que no te vuelvas loco. Aquí hay una buena colección de ejemplos de expresiones regulares de Groovy que muestran lo simple que puede y debe ser.El modo
(?x)
es muy defectuoso. No toma comentarios en el estilo Java de// COMMENT
sino en el estilo de shell de# COMMENT
. No funciona con cadenas multilínea. No acepta literales como literales, lo que obliga a los problemas de barra invertida enumerados anteriormente, lo que fundamentalmente compromete cualquier intento de alinear las cosas, como tener todos los comentarios comienzan en la misma columna. Debido a las barras diagonales inversas, puede hacer que comiencen en la misma columna en la cadena del código fuente y atornillarlas si las imprime, o viceversa. ¡Tanto para la legibilidad!Es increíblemente difícil, y de hecho, fundamentalmente unfijably roto, para ingresar caracteres Unicode en una expresión regular. No hay soporte para caracteres simbólicos como
/N{QUOTATION MARK}
,/N{LATIN SMALL LETTER E WITH GRAVE}
, o/N{MATHEMATICAL BOLD CAPITAL C}
. Eso significa que estás atrapado con números mágicos no mantenibles. Y tampoco puedes ingresarlos por punto de código. No puede usar/u0022
para el primero porque el preprocesador de Java lo convierte en un error de sintaxis. Entonces, se mueve a//u0022
, que funciona hasta que llegue al siguiente,//u00E8
, que no se puede ingresar de esa manera o romperá el indicadorCANON_EQ
. Y el último es una pesadilla pura: su punto de código es U + 1D402, pero Java no admite el conjunto Unicode completo usando sus números de punto de código en expresiones regulares, lo que le obliga a sacar su calculadora para descubrir que eso es/uD835/uDC02
o//uD835//uDC02
(pero no//uD835/uDC02
), lo suficientemente loco. Pero no puede usarlos en las clases de caracteres debido a un error de diseño, por lo que es imposible hacer coincidir, por ejemplo,[/N{MATHEMATICAL BOLD CAPITAL A}-/N{MATHEMATICAL BOLD CAPITAL Z}]
porque el compilador de expresiones regulares se estropea en el UTF- dieciséis. De nuevo, esto nunca se puede arreglar o cambiará los programas anteriores. Ni siquiera puede evitar el error utilizando la solución normal a los problemas de Unicode-en-código-fuente dejava -encoding UTF-8
compilando conjava -encoding UTF-8
, porque lo estúpido almacena las cadenas como desagradable UTF-16, lo que necesariamente las rompe en clases de personajes ¡OOPS!Muchas de las expresiones regulares de las que hemos llegado a depender en otros idiomas faltan en Java. No hay grupos nombrados para ejemplos, ni siquiera los relativamente numerados. Esto hace que la construcción de patrones más grandes a partir de los más pequeños sea fundamentalmente propensa a errores. Hay una biblioteca de aplicaciones para el usuario que le permite tener grupos con nombres simples, y de hecho esto finalmente llegará a la producción JDK7. Pero aun así no hay un mecanismo para qué hacer con más de un grupo con el mismo nombre. Y todavía no tienes buffers numerados relativamente. Volvemos a los Bad Old Days nuevamente, cosas que fueron resueltas hace eones.
No se admite una secuencia de salto de línea, que es una de las dos únicas partes "muy recomendadas" del estándar, lo que sugiere que se use
/R
para tal. Esto es incómodo de emular debido a su naturaleza de longitud variable y la falta de soporte de Java para grafemas.Los escapes de clase de caracteres no funcionan en el juego de caracteres nativo de Java. Sí, eso es correcto: cosas de rutina como
/w
y/s
(o más bien,"//w"
y"//b"
) no funcionan en Unicode en Java. Este no es el tipo de retro cool. Para empeorar las cosas, Java''s/b
(make that"//b"
, que no es lo mismo que"/b"
) tiene algo de sensibilidad Unicode, aunque no es lo que el estándar dice que debe tener. Entonces, por ejemplo, una cuerda como"élève"
nunca coincidirá en Java con el patrón/b/w+/b
, y no solo en su totalidad porPattern.matches
, sino que en ningún punto en absoluto puede obtenerse dePattern.find
. Esto es tan jodido como para creer en el mendigo. ¡Han roto la conexión inherente entre/w
y/b
, y luego los han mal definido para arrancar! Ni siquiera sabe qué son los puntos del código alfabético Unicode. Esto está extremadamente roto, y nunca lo pueden arreglar porque eso cambiaría el comportamiento del código existente, que está estrictamente prohibido en el Universo Java. Lo mejor que puede hacer es crear una biblioteca de reescritura que actúe como una interfaz antes de llegar a la fase de compilación; de esa forma, puedes migrar forzosamente tus patrones desde la década de 1960 hasta el siglo XXI del procesamiento de texto.Las únicas dos propiedades Unicode admitidas son las categorías generales y las propiedades del bloque. Las propiedades generales de la categoría solo admiten las abreviaturas como
/p{Sk}
, contrariamente a los estándares Strong Recommendation para permitir también/p{Modifier Symbol}
,/p{Modifier_Symbol}
, etc. Ni siquiera obtiene los alias necesarios como estándar dice que deberías. Eso hace que tu código sea aún más ilegible e imposible de mantener. Finalmente obtendrá soporte para la propiedad Script en la producción JDK7, pero eso aún es muy poco para el conjunto mínimo de 11 propiedades esenciales que el estándar dice que debe proporcionar incluso para el nivel mínimo de soporte Unicode.Algunas de las escasas propiedades que proporciona Java son faux amis : tienen los mismos nombres que los nombres de propiedad oficiales de Unicode, pero hacen algo totalmente diferente . Por ejemplo, Unicode requiere que
/p{alpha}
sea lo mismo que/p{Alphabetic}
, pero Java lo convierte en el/p{Alphabetic}
arcaico y no más pintoresco de 7 bits solamente, que es más de 4 órdenes de magnitud muy pocos. El espacio en blanco es otro defecto, ya que usa la versión de Java que se enmascara como espacio en blanco Unicode, sus analizadores UTF-8 se romperán debido a sus puntos de código NO-BREAK SPACE, lo que Unicode requiere normativamente como espacio en blanco, pero Java ignora ese requisito, por lo que tu analizador.No hay soporte para grafemas, de la manera que
/X
normalmente proporciona. Eso hace imposible innumerablemente muchas tareas comunes que necesita y desea hacer con expresiones regulares. Los grupos de grafemas extendidos no solo están fuera de tu alcance, porque Java no admite casi ninguna de las propiedades Unicode, ni siquiera puedes aproximar los antiguos clústeres de grafemas heredados usando el estándar(?:/p{Grapheme_Base}/p{Grapheme_Extend}]*)
. No poder trabajar con grafemas hace que incluso los tipos más simples de procesamiento de texto Unicode sean imposibles. Por ejemplo, no puede hacer coincidir una vocal independientemente de diacrítico en Java. La forma en que haces esto en un idioma con compatibilidad con grafema varía, pero al menos deberías ser capaz de lanzarlo a NFD y hacer coincidir(?:(?=[aeiou])/X)
. En Java, no puedes hacer mucho: los grafemas están fuera de tu alcance. Y eso significa que Java ni siquiera puede manejar su propio conjunto de caracteres nativos. Te da Unicode y luego hace que sea imposible trabajar con él.Los métodos de conveniencia en la clase String no almacenan en caché la expresión regular compilada. De hecho, no existe un patrón en tiempo de compilación que se comprueba con sintaxis en tiempo de compilación, que es cuando se supone que se debe realizar la comprobación de sintaxis. Eso significa que su programa, que utiliza expresiones regulares constantes en el momento de la compilación, explotará con una excepción en el medio de su ejecución si olvida una pequeña barra invertida aquí o allá como suele suceder debido a los defectos discutidos anteriormente. . Incluso Groovy entiende bien esta parte. Regexes son construcciones de muy alto nivel a las que se las puede enfrentar el desagradable modelo posventa de Java, atornillado en el lateral, y son demasiado importantes para que el proceso de texto rutinario sea ignorado. Java es un lenguaje de muy bajo nivel para este tipo de cosas, y no proporciona los mecanismos simples de los que puedes construir lo que necesitas: no puedes llegar allí desde aquí.
Las clases
String
yPattern
están marcadas comofinal
en Java. Eso elimina por completo cualquier posibilidad de utilizar un diseño OO apropiado para extender esas clases. No puede crear una mejor versión de un método dematches
subclases y reemplazos. Diablos, ¡ni siquiera puedes subclase! Final no es una solución; final es una sentencia de muerte de la cual no hay apelación.
Finalmente, para mostrarle cuán dañadas por el cerebro son las expresiones auténticas de Java, considere este patrón de líneas múltiples, que muestra muchos de los defectos ya descritos:
String rx =
"(?= ^ //p{Lu} [_//pL//pM//d//-] + /$)/n"
. " # next is a big can''t-have set /n"
. "(?! ^ .* /n"
. " (?: ^ //d+ $ /n"
. " | ^ //p{Lu} - //p{Lu} $ /n"
. " | Invitrogen /n"
. " | Clontech /n"
. " | L-L-X-X # dashes ok /n"
. " | Sarstedt /n"
. " | Roche /n"
. " | Beckman /n"
. " | Bayer /n"
. " ) # end alternatives /n"
. " //b # only on a word boundary /n"
. ") # end negated lookahead /n"
;
¿Ves lo antinatural que es eso? Tienes que poner nuevas líneas literales en tus cadenas; tienes que usar comentarios que no sean de Java; no se puede alinear nada debido a las barras invertidas adicionales; tienes que usar definiciones de cosas que no funcionan bien en Unicode. Hay muchos más problemas más allá de eso.
No solo no hay planes para corregir casi ninguno de estos graves defectos, sino que es imposible arreglar casi ninguno de ellos, porque cambias los programas anteriores. Incluso las herramientas normales del diseño OO están prohibidas para usted, ya que está bloqueado con la finalidad de una sentencia de muerte, y no se puede arreglar.
Entonces, Alireza Noori, si siente que las torpes expresiones fagetas de Java son demasiado flexibles para que el procesamiento de expresiones regulares sea confiable y conveniente en Java, no puedo negarlo. Lo siento, pero así son las cosas.
"¡Reparado en la próxima versión!"
El hecho de que algunas cosas nunca se puedan arreglar no significa que nunca se pueda arreglar nada. Simplemente tiene que hacerse con mucho cuidado. Aquí están las cosas que sé que ya están corregidas en las versiones actuales de JDK7 o JDK8 propuestas:
La propiedad de secuencia de comandos Unicode ahora es compatible. Puede usar cualquiera de las formas equivalentes
/p{Script=Greek}
,/p{sc=Greek}
,/p{IsGreek}
, o/p{Greek}
. Esto es intrínsecamente superior a las antiguas propiedades de bloque torpe. Significa que puedes hacer cosas como[/p{Latin}/p{Common}/p{Inherited}]
, lo cual es bastante importante.El error UTF-16 tiene una solución. Ahora puede especificar cualquier punto de código Unicode por su número usando la notación
/x{⋯}
, como/x{1D402}
. Esto funciona incluso dentro de las clases de caracteres, lo que finalmente permite que[/x{1D400}-/x{1D419}]
funcione correctamente. Sin embargo, aún debe doblar la barra invertida, y solo funciona en regexex, no con cadenas en general como debería.Los grupos con nombre ahora son compatibles a través de la notación estándar
(?<NAME>⋯)
para crearlo y/k<NAME>
para retroalimentarlo. Estos aún contribuyen a los números numéricos de grupo, también. Sin embargo, no puede obtener más de uno de ellos en el mismo patrón, ni puede usarlos para la recursión.Una nueva bandera de compilación de Patrones,
Pattern.UNICODE_CHARACTER_CLASSES
y el interruptor incrustable asociado,(?U)
, ahora intercambiará todas las definiciones de cosas como/w
,/b
,/p{alpha}
y/p{punct}
, de modo que ahora se ajustan a las definiciones de esas cosas requeridas por el estándar Unicode .Las propiedades binarias
/p{IsLowercase}
o mal/p{IsLowercase}
,/p{IsUppercase}
y/p{IsAlphabetic}
ahora serán compatibles, y corresponden a los métodos de la claseCharacter
. Esto es importante porque Unicode hace una distinción significativa y generalizada entre meras letras y puntos codificados en mayúsculas o en letras alfabéticas. Estas propiedades clave se encuentran entre esas 11 propiedades esenciales que son absolutamente necesarias para el cumplimiento del Nivel 1 con UTS # 18, "Expresiones Regulares Unicode" , sin las cuales no puede trabajar con Unicode.
Estas mejoras y correcciones son muy importantes para finalmente tener, por lo que estoy contento, incluso emocionado, por tenerlas.
Pero para la fuerza industrial, el regex de última generación y / o el trabajo Unicode, no usaré Java. Faltan muchas cosas en el modelo Unicode de Java, todavía fragmentario después de 20 años, para hacer un trabajo real si te atreves a utilizar el juego de caracteres que proporciona Java. Y el modelo atornillado en el lateral nunca funciona, que son todos los regex de Java. Tienes que empezar de nuevo desde los primeros principios, como lo hizo Groovy.
Claro, podría funcionar para aplicaciones muy limitadas, cuya pequeña base de clientes se limita a los monoglots de habla inglesa de las zonas rurales de Iowa sin interacciones externas ni necesidad de caracteres más allá de lo que podría enviar un telégrafo antiguo. Pero, ¿para cuántos proyectos es eso realmente cierto? Menos aún lo que piensas, resulta.
Es por esta razón que cierto (y obvio) multimillonario dólar canceló recientemente el despliegue internacional de una aplicación importante. El soporte de Unicode de Java, no solo en expresiones regulares, sino en todas partes, demostró ser demasiado débil para que la internacionalización necesaria se realice de manera confiable en Java. Debido a esto, se han visto obligados a retroceder desde su despliegue original planificado originalmente a un despliegue meramente estadounidense. Es positivamente parroquial. Y no, hay Nᴏᴛ Hᴀᴘᴘʏ; ¿Serias?
Java ha tenido 20 años para hacerlo bien, y es evidente que no lo han hecho hasta ahora, así que no aguantaría la respiración. O tirar un buen dinero después de malo; la lección aquí es ignorar la exageración y, en su lugar, aplicar la diligencia debida para asegurarse de que todo el soporte de infraestructura necesario esté allí antes de invertir demasiado. De lo contrario, usted también puede quedarse atascado sin ninguna opción real una vez que esté demasiado adentro para rescatar su proyecto.
Caveat Emptor
Uno puede despotricar, o simplemente escribir:
public class Regex {
/**
* @param source
* the string to scan
* @param pattern
* the regular expression to scan for
* @return the matched
*/
public static Iterable<String> matches(final String source, final String pattern) {
final Pattern p = Pattern.compile(pattern);
final Matcher m = p.matcher(source);
return new Iterable<String>() {
@Override
public Iterator<String> iterator() {
return new Iterator<String>() {
@Override
public boolean hasNext() {
return m.find();
}
@Override
public String next() {
return source.substring(m.start(), m.end());
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
};
}
}
Usado como lo desee:
public class RegexTest {
@Test
public void test() {
String source = "The colour of my bag matches the color of my shirt!";
String pattern = "colou?r";
for (String match : Regex.matches(source, pattern)) {
System.out.println(match);
}
}
}