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

Muchas implementaciones de expresiones regulares modernas interpretan la taquigrafía de la clase de caracteres \w como “cualquier letra, dígito o puntuación de conexión” (por lo general: subrayado). De esta forma, una expresión regular como \w+ coincide con palabras como hello , élève , GOÄ_432 o gefräßig .

Desafortunadamente, Java no. En Java, \w está limitado 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 separador de palabras \b coincide en lugares donde no debería.

¿Cuál sería el equivalente correcto de .NET, Unicode-aware \w o \b en Java? ¿Qué otros atajos necesitan “volver a escribir” para que sean compatibles con Unicode?

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 clase de Pattern actualizada de Sun para JDK7 tiene una nueva bandera maravillosa, UNICODE_CHARACTER_CLASS , que hace que todo vuelva a funcionar bien. Está disponible como incrustable (?U) para el interior del patrón, por lo que puedes usarlo también con los envoltorios de la clase String . También tiene definiciones corregidas para otras propiedades. Ahora rastrea El Estándar Unicode, tanto en RL1.2 como en RL1.2a desde UTS # 18: Expresiones Regulares Unicode . Esta es una mejora emocionante y dramática, y el equipo de desarrollo es digno de elogio por este importante esfuerzo.


Problemas Unicode Regex de Java

El problema con las expresiones regulares de Java es que las clases de caracteres Perl 1.0 escapan, es decir, \w , \b , \s , \d y sus complementos, no están extendidas en Java para funcionar con Unicode. Solo entre ellos, \b goza de cierta semántica extendida, pero estos no se asignan a \w , ni a los identificadores Unicode , ni a las propiedades de salto de línea de Unicode .

Además, las propiedades POSIX en Java se acceden 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} 

Esto es un verdadero desastre, porque significa que cosas como Alpha , Lower y Space no se asignan en Java a las propiedades Unicode Alphabetic , Lowercase o Whitespace . Esto es excesivamente molesto. El soporte de propiedades Unicode de Java es estrictamente antemillennial , lo que significa que no admite ninguna propiedad Unicode que haya aparecido en la última década.

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

  Regex 001A 0085 00A0 2029 JPJPJPJP \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 todos los resultados de espacio en blanco de Java son incorrectos de acuerdo con Unicode. Es un gran problema. Java simplemente está mal, 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 le da acceso a las propiedades reales de Unicode! De hecho, Java no admite ninguna propiedad que se corresponda con el espacio en blanco Unicode.


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

Para lidiar con este y muchos otros problemas relacionados, ayer escribí una función de Java para reescribir una cadena de patrones que reescribe estos 14 escapes de clase de caracteres:

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

reemplazándolos con cosas que realmente funcionen para que coincida con Unicode de manera predecible y consistente. Es solo un prototipo alfa de una sola sesión de hack, 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}]])|(? \p{Nd} \D => \P{Nd} \R => (?:(?>\u000D\u000A)|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029]) \X => (?>\PM\pM*) 

Algunas cosas para considerar …

  • Eso usa para su definición \X lo que Unicode ahora se refiere como un clúster de grafemas heredado , no como un clúster 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 todavía es perfectamente funcional para las situaciones más comunes. EDITAR: Ver apéndice en la parte inferior.

  • Qué hacer con \d depende de su intención, pero el valor predeterminado es la definición de Uniode. Puedo ver gente que no siempre quiere \p{Nd} , pero a veces [0-9] o \pN .

  • Las dos definiciones de límites, \b y \B , se escriben específicamente para usar la definición \w .

  • Esa definición es demasiado amplia, porque capta las letras apareadas no solo las que están circuladas. La propiedad Unicode Other_Alphabetic no está disponible hasta JDK7, por lo que es lo mejor que puede hacer.


Explorando los límites

Los límites han sido un problema desde que Larry Wall acuñó la syntax \b y \B para hablar sobre Perl 1.0 en 1987. La clave para entender cómo funcionan \b y \B es disipar dos mitos dominantes sobre ellos:

  1. Solo buscan caracteres \w Word, nunca para caracteres que no sean palabras.
  2. No buscan específicamente el borde de la cuerda.

A \b límite significa:

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

Y todos se definen perfectamente de la manera siguiente:

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

Por lo tanto, dado que IF-THEN está codificado como un AB and ed-together en expresiones regulares, an or es X|Y , y porque el and es mayor en precedencia que or , eso es simplemente AB|CD . Entonces, cada \b que signifique un límite se puede reemplazar de forma segura con:

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

con el \w definido de la manera apropiada.

(Puede pensar que es extraño que los componentes A y C sean opuestos. En un mundo perfecto, debería poder escribir AB|D , pero por un tiempo estuve persiguiendo contradicciones de exclusión mutua en propiedades Unicode, que creo que Me he ocupado, pero dejé la doble condición en el límite por las dudas. Además, esto hace que sea más extensible si obtiene ideas adicionales más adelante).

Para el \B non-boundaries, la lógica es:

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

Permitir que todas las instancias de \B sean reemplazadas por:

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

Esto realmente es cómo se comportan \b y \B Patrones equivalentes para ellos son

  • \b usando la construcción ((IF)THEN|ELSE) es (?(?<=\w)(?!\w)|(?=\w))
  • \B usando la construcción ((IF)THEN|ELSE) es (?(?=\w)(?<=\w)|(?

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

Ya he verificado el comportamiento de los límites utilizando las tres definiciones equivalentes con un conjunto de pruebas que comprueba 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, la gente a menudo quiere un tipo diferente de límite. Quieren algo que sea consciente del espacio en blanco y del 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 algunas otras comodidades. Esto incluye definiciones de palabras en lenguaje natural, guiones, guiones y apóstrofes, más un poco más.

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

Para la sustitución de la clase de expresiones regulares que hace que la clase de caracteres en sus expresiones regulares Java funcione finalmente en Unicode y funcione correctamente, obtenga la fuente completa desde aquí . Puede hacer con eso lo que quiera, por supuesto. Si le haces arreglos, me encantaría saberlo, pero no es necesario. Es bastante corto. Las agallas 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 un lanzamiento alfa, cosas que pirateé durante el fin de semana. No permanecerá de esa manera.

Para la versión beta, tengo la intención de:

  • doblar juntos la duplicación del código

  • Proporcione una interfaz más clara con respecto a escapes de cadenas sin escape frente a aumentos de escapes de expresiones regulares

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

  • Proporcione métodos de conveniencia que manejen la posibilidad de dar la vuelta y llamar a Pattern.compile o String.matches o lo que sea, por usted

Para la versión 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 grafemas extendido para usarlo para una \X mejorada.

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üß!

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

Parece que [\p{L}] capta todas las letras Unicode. Entonces, el equivalente Unicode de \w debería ser [\p{L}\p{Digit}_] .

En Java, \w y \d no son conscientes de Unicode; solo coinciden con los caracteres ASCII, [A-Za-z0-9_] y [0-9] . Lo mismo ocurre con \p{Alpha} y 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 quiere hacer coincidir los “caracteres de palabra” de Unicode, debe deletrearlo, por ejemplo [\pL\p{Mn}\p{Nd}\p{Pc}] , para letras, modificadores de no espaciado (acentos), dígitos decimales y puntuación de conexión.

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