¿Equivalentes Unicode para \ w y \ b en expresiones regulares de Java?

126

Muchas implementaciones modernas de expresiones regulares interpretan la \wtaquigrafía de la clase de caracteres como "cualquier letra, dígito o puntuación de conexión" (generalmente: subrayado). De esta manera, una expresión regular como \w+los partidos palabras como hello, élève, GOÄ_432o gefräßig.

Desafortunadamente, Java no lo hace. En Java, \wse limita a [A-Za-z0-9_]. Esto hace que las palabras coincidentes como las mencionadas anteriormente sean difíciles, entre otros problemas.

También parece que el \bseparador de palabras coincide en lugares donde no debería.

¿Cuál sería el equivalente correcto de un tipo .NET, compatible con Unicode \wo \ben Java? ¿Qué otros atajos necesitan "reescribirse" para que sean compatibles con Unicode?

Tim Pietzcker
fuente
3
La historia corta, Tim, es que todos necesitan escribir para alinearlos con Unicode. Todavía no veo señales de que Java 1.7 haga algo más con las propiedades Unicode que finalmente agregar soporte para scripts, pero eso es todo. Hay algunas cosas que realmente no puede hacer sin un mejor acceso al complemento completo de las propiedades Unicode. Si aún no tienes mis scripts uniprops y unichars (y uninames ), son asombrosos los descubrimientos de todo esto.
tchrist
Uno podría considerar agregar marcas a la clase de palabras. Como por ejemplo & auml; se puede representar en Unicode como \ u0061 \ u0308 o \ u00E4.
Mostowski Collapse
3
Hola Tim, mira mi ACTUALIZACIÓN. Han agregado una bandera para que todo funcione. ¡Viva!
tchrist

Respuestas:

240

Código fuente

El código fuente para las funciones de reescritura que analizo a continuación está disponible aquí .

Actualización en Java 7

La Patternclase actualizada de Sun para JDK7 tiene una nueva y maravillosa bandera UNICODE_CHARACTER_CLASS, que hace que todo vuelva a funcionar correctamente. Está disponible como incrustable (?U)para dentro del patrón, por lo que también puede usarlo con los Stringenvoltorios de la clase. También tiene definiciones corregidas para varias otras propiedades, también. Ahora rastrea The Unicode Standard, tanto en RL1.2 como en RL1.2a de UTS # 18: Expresiones regulares de Unicode . Esta es una mejora emocionante y dramática, y el equipo de desarrollo debe ser elogiado por este importante esfuerzo.


Problemas Unicode de expresiones regulares de Java

El problema con Java expresiones regulares es que los escapes Perl 1.0 charclass - es decir \w, \b, \s, \dy sus complementos - no son en Java extenderse a trabajar con Unicode. Sólo entre estos, \bgoza de cierta semántica extendidos, pero éstos mapa ni a \w, ni a los identificadores de Unicode , ni a Unicode propiedades de salto de línea .

Además, se accede a las propiedades POSIX en Java de esta manera:

POSIX syntax    Java syntax

[[:Lower:]]     \p{Lower}
[[:Upper:]]     \p{Upper}
[[:ASCII:]]     \p{ASCII}
[[:Alpha:]]     \p{Alpha}
[[:Digit:]]     \p{Digit}
[[:Alnum:]]     \p{Alnum}
[[:Punct:]]     \p{Punct}
[[:Graph:]]     \p{Graph}
[[:Print:]]     \p{Print}
[[:Blank:]]     \p{Blank}
[[:Cntrl:]]     \p{Cntrl}
[[:XDigit:]]    \p{XDigit}
[[:Space:]]     \p{Space}

Este es un verdadero desastre, porque significa que las cosas les gusta Alpha, Lowery Spacelo hacen no en el mapa de Java para el Unicode Alphabetic, Lowercaseo Whitespacepropiedades. Esto es extremadamente molesto. El soporte de propiedad Unicode de Java es estrictamente antemilenial , lo que significa que no admite ninguna propiedad Unicode que haya surgido en la última década.

No poder hablar sobre los espacios en blanco correctamente es súper molesto. Considere la siguiente tabla. Para cada uno de esos puntos de código, hay una columna de resultados J para Java y una columna de resultados P para Perl o cualquier otro motor de expresiones regulares basado en PCRE:

             Regex    001A    0085    00A0    2029
                      J  P    J  P    J  P    J  P
                \s    1  1    0  1    0  1    0  1
               \pZ    0  0    0  0    1  1    1  1
            \p{Zs}    0  0    0  0    1  1    0  0
         \p{Space}    1  1    0  1    0  1    0  1
         \p{Blank}    0  0    0  0    0  1    0  0
    \p{Whitespace}    -  1    -  1    -  1    -  1
\p{javaWhitespace}    1  -    0  -    0  -    1  -
 \p{javaSpaceChar}    0  -    0  -    1  -    1  -

¿Mira eso?

Prácticamente cada uno de esos resultados de espacios en blanco de Java es ̲w̲r̲o̲n̲g̲ según Unicode. Es un gran problema. Java simplemente está en mal estado, dando respuestas que están "mal" de acuerdo con la práctica existente y también de acuerdo con Unicode. ¡Además, Java ni siquiera te da acceso a las propiedades reales de Unicode! De hecho, Java no admite ninguna propiedad que corresponda al espacio en blanco Unicode.


La solución a todos esos problemas y más

Para tratar con este y muchos otros problemas relacionados, ayer escribí una función Java para reescribir una cadena de patrón que reescribe estos 14 escapes de charclass:

\w \W \s \S \v \V \h \H \d \D \b \B \X \R

reemplazándolos con cosas que realmente funcionan para que coincida con Unicode de una manera predecible y consistente. Es solo un prototipo alfa de una sola sesión de pirateo, pero es completamente funcional.

La historia corta es que mi código reescribe esos 14 de la siguiente manera:

\s => [\u0009-\u000D\u0020\u0085\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]
\S => [^\u0009-\u000D\u0020\u0085\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]

\v => [\u000A-\u000D\u0085\u2028\u2029]
\V => [^\u000A-\u000D\u0085\u2028\u2029]

\h => [\u0009\u0020\u00A0\u1680\u180E\u2000-\u200A\u202F\u205F\u3000]
\H => [^\u0009\u0020\u00A0\u1680\u180E\u2000\u2001-\u200A\u202F\u205F\u3000]

\w => [\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]
\W => [^\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]

\b => (?:(?<=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])|(?<![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]))
\B => (?:(?<=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])|(?<![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]))

\d => \p{Nd}
\D => \P{Nd}

\R => (?:(?>\u000D\u000A)|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])

\X => (?>\PM\pM*)

Algunas cosas a considerar ...

  • Eso utiliza para su \Xdefinición lo que Unicode ahora se refiere como un grupo de grafemas heredados , no un grupo de grafemas extendido , ya que este último es bastante más complicado. Perl ahora usa la versión más elegante, pero la versión anterior sigue siendo perfectamente viable para las situaciones más comunes. EDITAR: Ver anexo en la parte inferior.

  • Lo que debe hacer \ddepende de su intención, pero el valor predeterminado es la definición de Uniode. Puedo ver la gente no siempre querer \p{Nd}, pero a veces, ya sea [0-9]o \pN.

  • Las dos definiciones de límites, \by \B, están escritas específicamente para usar la \wdefinición.

  • Esa \wdefinición es demasiado amplia, porque toma las letras parenizadas, no solo las encerradas en un círculo. La Other_Alphabeticpropiedad Unicode no está disponible hasta JDK7, por lo que es lo mejor que puede hacer.


Explorando límites

Los límites han sido un problema desde que Larry Wall acuñado por primera vez el \by \Bsintaxis para hablar de ellos para Perl 1.0 en 1987. La clave para entender cómo \by \Btanto el trabajo es disipar dos mitos generalizados sobre ellos:

  1. Ellos son sólo alguna vez en busca de \wcaracteres de palabra, no para caracteres no de palabras.
  2. No buscan específicamente el borde de la cuerda.

Un \blímite significa:

    IF does follow word
        THEN doesn't precede word
    ELSIF doesn't follow word
        THEN does precede word

Y todos estos se definen perfectamente de manera directa como:

  • sigue la palabra es (?<=\w).
  • precede a la palabra es (?=\w).
  • no sigue la palabra es (?<!\w).
  • no precede a la palabra es (?!\w).

Por lo tanto, dado que IF-THENestá codificado como un and ed-together ABen expresiones regulares, an ores X|Y, y porque andes mayor en prioridad que or, eso es simplemente AB|CD. Entonces, todo \beso significa que un límite se puede reemplazar de forma segura con:

    (?:(?<=\w)(?!\w)|(?<!\w)(?=\w))

con lo \wdefinido de la manera adecuada.

(Puede que te parezca extraño que los componentes Ay Csean opuestos. En un mundo perfecto, deberías poder escribir eso AB|D, pero durante un tiempo estuve persiguiendo contradicciones de exclusión mutua en las propiedades Unicode, que creo que me he ocupado de , pero dejé la doble condición en el límite por si acaso. Además, esto lo hace más extensible si tienes ideas adicionales más adelante).

Para los \Bno límites, la lógica es:

    IF does follow word
        THEN does precede word
    ELSIF doesn't follow word
        THEN doesn't precede word

Permitiendo que todas las instancias de \Bsean reemplazadas por:

    (?:(?<=\w)(?=\w)|(?<!\w)(?!\w))

Esto realmente es cómo \by \Bcomportarse. Patrones equivalentes para ellos son

  • \busando la ((IF)THEN|ELSE)construcción es(?(?<=\w)(?!\w)|(?=\w))
  • \Busando la ((IF)THEN|ELSE)construcción es(?(?=\w)(?<=\w)|(?<!\w))

Pero las versiones con just AB|CDestán bien, especialmente si carece de patrones condicionales en su lenguaje regex, como Java. ☹

Ya verifiqué el comportamiento de los límites utilizando las tres definiciones equivalentes con un conjunto de pruebas que verifica 110.385.408 coincidencias por ejecución, y que he ejecutado en una docena de configuraciones de datos diferentes de acuerdo con:

     0 ..     7F    the ASCII range
    80 ..     FF    the non-ASCII Latin1 range
   100 ..   FFFF    the non-Latin1 BMP (Basic Multilingual Plane) range
 10000 .. 10FFFF    the non-BMP portion of Unicode (the "astral" planes)

Sin embargo, las personas a menudo quieren un tipo diferente de límite. Quieren algo que tenga en cuenta el espacio en blanco y el borde de la cadena:

  • borde izquierdo como(?:(?<=^)|(?<=\s))
  • borde derecho como(?=$|\s)

Arreglando Java con Java

El código que publiqué en mi otra respuesta proporciona esto y muchas otras comodidades. Esto incluye definiciones de palabras, guiones, guiones y apóstrofes en lenguaje natural, y un poco más.

También le permite especificar caracteres Unicode en puntos de código lógico, no en sustitutos idiotas UTF-16. ¡Es difícil enfatizar lo importante que es eso! Y eso es solo para la expansión de la cadena.

Para la sustitución de chargelass regex que hace que la charclass en sus expresiones regulares de Java finalmente funcione en Unicode, y funcione correctamente, tome la fuente completa desde aquí . Puedes hacerlo con tu gusto, por supuesto. Si lo arreglas, me encantaría saberlo, pero no tienes que hacerlo. Es muy corto Las entrañas de la función principal de reescritura de expresiones regulares son simples:

switch (code_point) {

    case 'b':  newstr.append(boundary);
               break; /* switch */
    case 'B':  newstr.append(not_boundary);
               break; /* switch */

    case 'd':  newstr.append(digits_charclass);
               break; /* switch */
    case 'D':  newstr.append(not_digits_charclass);
               break; /* switch */

    case 'h':  newstr.append(horizontal_whitespace_charclass);
               break; /* switch */
    case 'H':  newstr.append(not_horizontal_whitespace_charclass);
               break; /* switch */

    case 'v':  newstr.append(vertical_whitespace_charclass);
               break; /* switch */
    case 'V':  newstr.append(not_vertical_whitespace_charclass);
               break; /* switch */

    case 'R':  newstr.append(linebreak);
               break; /* switch */

    case 's':  newstr.append(whitespace_charclass);
               break; /* switch */
    case 'S':  newstr.append(not_whitespace_charclass);
               break; /* switch */

    case 'w':  newstr.append(identifier_charclass);
               break; /* switch */
    case 'W':  newstr.append(not_identifier_charclass);
               break; /* switch */

    case 'X':  newstr.append(legacy_grapheme_cluster);
               break; /* switch */

    default:   newstr.append('\\');
               newstr.append(Character.toChars(code_point));
               break; /* switch */

}
saw_backslash = false;

De todos modos, ese código es solo una versión alfa, cosas que pirateé durante el fin de semana. No se quedará así.

Para la versión beta pretendo:

  • doblar juntos la duplicación de código

  • Proporcionar una interfaz más clara con respecto a los escapes de cadena de escape sin aumento de escapes de expresiones regulares

  • proporcionar cierta flexibilidad en la \dexpansión, y tal vez el\b

  • Proporcione métodos convenientes que se encarguen de dar la vuelta y llamar a Pattern.compile o String.matches o lo que sea para usted.

Para el lanzamiento de producción, debe tener javadoc y un conjunto de pruebas JUnit. Puedo incluir mi gigatester, pero no está escrito como pruebas JUnit.


Apéndice

Tengo buenas noticias y malas noticias.

La buena noticia es que ahora tengo una aproximación muy cercana a un clúster de grafema extendido para usar para mejorar \X.

La mala noticia ☺ es que ese patrón es:

(?:(?:\u000D\u000A)|(?:[\u0E40\u0E41\u0E42\u0E43\u0E44\u0EC0\u0EC1\u0EC2\u0EC3\u0EC4\uAAB5\uAAB6\uAAB9\uAABB\uAABC]*(?:[\u1100-\u115F\uA960-\uA97C]+|([\u1100-\u115F\uA960-\uA97C]*((?:[[\u1160-\u11A2\uD7B0-\uD7C6][\uAC00\uAC1C\uAC38]][\u1160-\u11A2\uD7B0-\uD7C6]*|[\uAC01\uAC02\uAC03\uAC04])[\u11A8-\u11F9\uD7CB-\uD7FB]*))|[\u11A8-\u11F9\uD7CB-\uD7FB]+|[^[\p{Zl}\p{Zp}\p{Cc}\p{Cf}&&[^\u000D\u000A\u200C\u200D]]\u000D\u000A])[[\p{Mn}\p{Me}\u200C\u200D\u0488\u0489\u20DD\u20DE\u20DF\u20E0\u20E2\u20E3\u20E4\uA670\uA671\uA672\uFF9E\uFF9F][\p{Mc}\u0E30\u0E32\u0E33\u0E45\u0EB0\u0EB2\u0EB3]]*)|(?s:.))

que en Java escribirías como:

String extended_grapheme_cluster = "(?:(?:\\u000D\\u000A)|(?:[\\u0E40\\u0E41\\u0E42\\u0E43\\u0E44\\u0EC0\\u0EC1\\u0EC2\\u0EC3\\u0EC4\\uAAB5\\uAAB6\\uAAB9\\uAABB\\uAABC]*(?:[\\u1100-\\u115F\\uA960-\\uA97C]+|([\\u1100-\\u115F\\uA960-\\uA97C]*((?:[[\\u1160-\\u11A2\\uD7B0-\\uD7C6][\\uAC00\\uAC1C\\uAC38]][\\u1160-\\u11A2\\uD7B0-\\uD7C6]*|[\\uAC01\\uAC02\\uAC03\\uAC04])[\\u11A8-\\u11F9\\uD7CB-\\uD7FB]*))|[\\u11A8-\\u11F9\\uD7CB-\\uD7FB]+|[^[\\p{Zl}\\p{Zp}\\p{Cc}\\p{Cf}&&[^\\u000D\\u000A\\u200C\\u200D]]\\u000D\\u000A])[[\\p{Mn}\\p{Me}\\u200C\\u200D\\u0488\\u0489\\u20DD\\u20DE\\u20DF\\u20E0\\u20E2\\u20E3\\u20E4\\uA670\\uA671\\uA672\\uFF9E\\uFF9F][\\p{Mc}\\u0E30\\u0E32\\u0E33\\u0E45\\u0EB0\\u0EB2\\u0EB3]]*)|(?s:.))";

¡Tschüß!

tchrist
fuente
10
Esto es increíble. Muchas gracias.
Tim Pietzcker
9
Cristo, esa es una respuesta iluminada. Solo que no obtengo la referencia de Jon Skeet. ¿Qué tiene él que ver con esto?
BalusC
12
@BalusC: Es una referencia a Jon antes diciendo que me había dejado responder la pregunta. Pero por favor, no deje caer ta @tchrist. Podría ir a mi cabeza. :)
tchrist
3
¿Has pensado en agregar esto a OpenJDK?
Martijn Verburg el
2
@ Martijn: no lo había hecho, no; No sabía que era tan "abierto". :) Pero he pensado en lanzarlo en un sentido más formal; otros en mi departamento desean verlo hecho (con algún tipo de licencia de código abierto, probablemente BSD o ASL). Probablemente voy a cambiar la API de lo que es en este prototipo alfa, limpiar el código, etc. Pero nos ayuda enormemente, y creemos que también ayudará a otros. Realmente desearía que Sun hiciera algo con su biblioteca, pero Oracle no inspira confianza.
tchrist el
15

Es realmente desafortunado que \wno funcione. La solución propuesta \p{Alpha}tampoco funciona para mí.

Parece que [\p{L}]atrapa todas las letras Unicode. Entonces el equivalente Unicode de \wdebería ser [\p{L}\p{Digit}_].

musiKk
fuente
Pero \wtambién coincide con dígitos y más. Creo que solo por letras, \p{L}funcionaría.
Tim Pietzcker
Tienes razón. \p{L}es suficiente. También pensé que solo las letras eran el problema. [\p{L}\p{Digit}_]debe capturar todos los caracteres alfanuméricos, incluido el guión bajo.
musiKk
@MusicKk: vea mi respuesta para obtener una solución completa que le permite escribir sus patrones normalmente, pero luego pasarla a través de una función que corrige las lagunas abiertas de Java para que funcione correctamente en Unicode.
tchrist
No, \wes definido por Unicode como mucho más amplio que solo \pLy los dígitos ASCII, de todas las cosas tontas. Debe escribir [\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]si desea un Unicode-aware \wpara Java, o simplemente puede usar mi unicode_charclassfunción desde aquí . ¡Lo siento!
tchrist
1
@Tim, sí, para las letras \pLfunciona (no es necesario adoptar accesorios de una letra). Sin embargo, rara vez lo desea, porque debe tener cuidado de que su coincidencia no obtenga respuestas diferentes solo porque sus datos están en el formulario de normalización Unicode D (también conocido como NFD, que significa descomposición canónica ) versus estar en NFC (NFD seguido de canónico) composición ). Un ejemplo es que el punto de código U + E9 ( "é") está \pLen forma NFC, pero su forma NFD se convierte en U + 65.301, por lo que coincide \pL\pM. Puede un poco de evitar esto con \X: (?:(?=\pL)\X), pero tendrá que mi versión de que para Java. :(
tchrist
7

En Java, \wy \dno son compatibles con Unicode; solo coinciden con los caracteres ASCII [A-Za-z0-9_]y [0-9]. Lo mismo ocurre con \p{Alpha}amigos (las "clases de caracteres" POSIX en las que se basan se supone que son sensibles a la configuración regional, pero en Java solo han coincidido con caracteres ASCII). Si desea hacer coincidir los "caracteres de palabras" de Unicode, debe deletrearlo, por ejemplo [\pL\p{Mn}\p{Nd}\p{Pc}], para letras, modificadores sin espaciado (acentos), dígitos decimales y puntuación de conexión.

Sin embargo, Java \b es unicode-savvy; también usa Character.isLetterOrDigit(ch)y verifica las letras acentuadas, pero el único carácter de "puntuación de conexión" que reconoce es el guión bajo. EDITAR: cuando pruebo su código de muestra, se imprime ""y élève"como debería ( ver en ideone.com ).

Alan Moore
fuente
Lo siento, Alan, pero realmente no puedes decir que Java \bes un experto en Unicode. Comete toneladas y toneladas de errores. "\u2163=", "\u24e7="y "\u0301="todos no coinciden con el patrón "\\b="en Java, pero se supone que lo hacen, como perl -le 'print /\b=/ || 0 for "\x{2163}=", "\x{24e7}=", "\x{301}="'revela. Sin embargo, si (y solo si) intercambias mi versión de un límite de palabra en lugar del nativo \ben Java, entonces todos funcionan también en Java.
tchrist
@tchrist: No estaba comentando sobre \bla corrección de las palabras, solo señalaba que funciona con caracteres Unicode (como se implementa en Java), no solo con los \wamigos y me gusta de ASCII . Sin embargo, funciona correctamente con respecto a \u0301cuándo ese personaje está emparejado con un personaje base, como en e\u0301=. Y no estoy convencido de que Java esté equivocado en este caso. ¿Cómo se puede considerar una marca de combinación como un carácter de palabra a menos que sea parte de un grupo de grafemas con una letra?
Alan Moore
3
@ Alan, esto es algo que se aclaró cuando Unicode aclaró los grupos de grafemas al discutir los grupos de grafemas extendidos frente a los legados. La antigua definición de un grupo de grafemas, en el que \Xsignifica una no marca seguida de cualquier número de marcas, es problemática, porque debería poder describir todos los archivos como coincidentes /^(\X*\R)*\R?$/, pero no puede hacerlo si tiene un \pMal comienzo de el archivo, o incluso de una línea. Así que lo han extendido para que siempre coincida con al menos un personaje. Siempre lo hizo, pero ahora hace que el patrón anterior funcione. [... continúa ...]
tchrist
2
@ Alan, hace más daño que bien que el nativo de Java \bsea ​​parcialmente consciente de Unicode. Considere hacer coincidir la cadena "élève"con el patrón \b(\w+)\b. ¿Ves el problema?
tchrist
1
@tchrist: Sí, sin los límites de la palabra, \w+encuentra dos coincidencias: ly ve, lo cual es bastante malo. Pero con los límites de las palabras no encuentra nada, porque \breconoce éy ècomo caracteres de palabras. Como mínimo, \by \wdebería ponerse de acuerdo sobre qué es un carácter de palabra y qué no lo es.
Alan Moore el