Equivalentes Unicode para \ w e \ b em expressões regulares Java?

126

Muitas implementações modernas de regex interpretam a \wabreviação da classe de caracteres como "qualquer letra, dígito ou pontuação de conexão" (geralmente: sublinhado). Dessa forma, uma regex como \w+jogos de palavras como hello, élève, GOÄ_432ou gefräßig.

Infelizmente, Java não. Em Java, \wé limitado a [A-Za-z0-9_]. Isso dificulta palavras correspondentes como as mencionadas acima, entre outros problemas.

Parece também que o \bseparador de palavras corresponde a locais onde não deveria.

Qual seria o equivalente correto de um .NET, compatível com Unicode \wou \bem Java? Quais outros atalhos precisam ser "reescritos" para torná-los compatíveis com Unicode?

Tim Pietzcker
fonte
3
A história curta, Tim, é que todos eles precisam escrever para alinhá-los ao Unicode. Ainda não vejo sinais de que o Java 1.7 faça algo mais com as propriedades Unicode do que finalmente adicionar suporte a scripts, mas é isso. Há algumas coisas que você realmente não pode fazer sem um melhor acesso ao conjunto completo de propriedades Unicode. Se você ainda não tem meus uniprops e unichars scripts (e uninames ), eles são eye-openers deslumbrante para tudo isso.
tchrist
Pode-se considerar adicionar marcas à classe de palavras. Desde que por exemplo o & auml; pode ser representado no Unicode como \ u0061 \ u0308 ou \ u00E4.
Mostowski Collapse
3
Ei, Tim, confira minha atualização. Eles adicionaram uma bandeira para fazer tudo funcionar. Viva!
Tcrist

Respostas:

240

Código fonte

O código fonte das funções de reescrita que discuto abaixo está disponível aqui .

Atualização em Java 7

A Patternclasse atualizada da Sun para o JDK7 possui uma maravilhosa nova flag UNICODE_CHARACTER_CLASS, que faz tudo funcionar novamente. Está disponível como um incorporável (?U)para dentro do padrão, para que você também possa usá-lo com os Stringinvólucros da classe. Também possui definições corrigidas para várias outras propriedades. Agora ele rastreia o Padrão Unicode, tanto no RL1.2 quanto no RL1.2a do UTS # 18: Expressões regulares do Unicode . Esta é uma melhoria emocionante e dramática, e a equipe de desenvolvimento deve ser elogiada por esse importante esforço.


Problemas de Unicode Regex do Java

O problema com Java expressões regulares é que os Perl 1.0 escapes charclass - o que significa \w, \b, \s, \de seus complementos - não estão em Java estendido para trabalhar com Unicode. Sozinho entre estes, \bgoza de certos semântica prolongados, mas estes mapa nem para \w, nem para identificadores Unicode , nem para Unicode propriedades de quebra de linha .

Além disso, as propriedades POSIX em Java são acessadas desta maneira:

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}

Esta é uma verdadeira bagunça, porque isso significa que as coisas gosto Alpha, Lowere Spacefazer não no mapa Java para o Unicode Alphabetic, Lowercaseou Whitespacepropriedades. Isso é extremamente irritante. O suporte à propriedade Unicode do Java é estritamente antemilenista , com o que quero dizer que ele não suporta nenhuma propriedade Unicode lançada na última década.

Não poder falar sobre espaço em branco corretamente é super irritante. Considere a seguinte tabela. Para cada um desses pontos de código, existe uma coluna de resultados J para Java e uma coluna de resultados P para Perl ou qualquer outro mecanismo de regex baseado em 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  -

Está vendo isso?

Praticamente todos esses resultados de espaço em branco do Java são gerados de acordo com o Unicode. É realmente um grande problema. Java é apenas uma bagunça, dando respostas "erradas" de acordo com a prática existente e também de acordo com o Unicode. Além disso, o Java nem lhe dá acesso às propriedades reais do Unicode! De fato, o Java não suporta nenhuma propriedade que corresponda ao espaço em branco Unicode.


A solução para todos esses problemas e muito mais

Para lidar com esse e muitos outros problemas relacionados, ontem escrevi uma função Java para reescrever uma cadeia de caracteres padrão que reescreve essas 14 fugas de classe:

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

substituindo-os por coisas que realmente funcionam para corresponder ao Unicode de maneira previsível e consistente. É apenas um protótipo alfa de uma única sessão de invasão, mas é completamente funcional.

A história curta é que meu código reescreve esses 14 da seguinte maneira:

\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*)

Algumas coisas a considerar ...

  • Isso usa para sua \Xdefinição o que o Unicode agora se refere como um cluster de grafema herdado , não um cluster de grafema estendido , pois o último é um pouco mais complicado. O próprio Perl agora usa a versão mais sofisticada, mas a versão antiga ainda é perfeitamente viável para as situações mais comuns. EDIT: Veja adendo na parte inferior.

  • O que fazer \ddepende da sua intenção, mas o padrão é a definição Uniode. Eu posso ver as pessoas nem sempre querendo \p{Nd}, mas às vezes [0-9]ou \pN.

  • As duas definições de limite \be \Bsão especificamente escritas para usar a \wdefinição.

  • Essa \wdefinição é excessivamente ampla, porque pega as letras parenned e não apenas as circuladas. A Other_Alphabeticpropriedade Unicode não está disponível até o JDK7, portanto é o melhor que você pode fazer.


Explorando limites

Os limites têm sido um problema desde que Larry Wall cunhou a sintaxe \be o nome \Bdeles para falar sobre eles para o Perl 1.0 em 1987. A chave para entender como \be \Bo trabalho deles é dissipar dois mitos difundidos sobre eles:

  1. Eles são sempre apenas olhando para \wcaracteres de palavra, não para caracteres não-palavra.
  2. Eles não procuram especificamente a borda da string.

Um \blimite significa:

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

E esses são todos perfeitamente definidos como:

  • segue a palavra é (?<=\w).
  • precede a palavra é (?=\w).
  • não segue a palavra é (?<!\w).
  • não precede a palavra é (?!\w).

Portanto, uma vez que IF-THENé codificado como um and ed-juntos ABem regexes, um oré X|Y, e porque o andmaior tem precedência or, isso é simplesmente AB|CD. Portanto, tudo o \bque significa que um limite pode ser substituído com segurança por:

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

com o \wdefinido da maneira apropriada.

(Você pode achar estranho que os componentes Ae Csejam opostos. Em um mundo perfeito, você deve escrever isso AB|D, mas por um tempo eu estava perseguindo contradições de exclusão mútua nas propriedades Unicode - das quais acho que já cuidei. , mas deixei a condição dupla no limite, apenas por precaução. Além disso, torna-se mais extensível se você receber idéias extras posteriormente.)

Para os \Bnão limites, a lógica é:

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

Permitindo que todas as instâncias \Bsejam substituídas por:

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

É realmente assim \be \Bse comporta. Padrões equivalentes para eles são

  • \busando a ((IF)THEN|ELSE)construção é(?(?<=\w)(?!\w)|(?=\w))
  • \Busando a ((IF)THEN|ELSE)construção é(?(?=\w)(?<=\w)|(?<!\w))

Mas as versões com apenas AB|CDsão boas, especialmente se você não tiver padrões condicionais em sua linguagem regex - como Java. ☹

Eu já verifiquei o comportamento dos limites usando todas as três definições equivalentes com um conjunto de testes que verifica 110.385.408 correspondências por execução e que eu executei em uma dúzia de configurações de dados diferentes de acordo com:

     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)

No entanto, as pessoas geralmente querem um tipo diferente de limite. Eles querem algo com espaço em branco e com conhecimento de borda de cadeia:

  • borda esquerda como(?:(?<=^)|(?<=\s))
  • borda direita como(?=$|\s)

Corrigindo Java com Java

O código que publiquei em minha outra resposta fornece essa e várias outras conveniências. Isso inclui definições para palavras em linguagem natural, traços, hífens e apóstrofos, além de um pouco mais.

Também permite especificar caracteres Unicode em pontos de código lógico, não em substitutos idiotas do UTF-16. É difícil enfatizar o quanto isso é importante! E isso é apenas para a expansão de strings.

Para a substituição de classe de classe regex que faz com que a classe em suas expressões regulares em Java finalmente funcione em Unicode e funcione corretamente, pegue a fonte completa a partir daqui . Você pode fazer o que quiser, é claro. Se você corrigir isso, eu adoraria ouvir, mas você não precisa. É bem curto. A essência da principal função de regravação de regex é 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;

Enfim, esse código é apenas uma versão alfa, coisa que eu hackeei no fim de semana. Não vai ficar assim.

Para a versão beta, pretendo:

  • dobre a duplicação de código

  • forneça uma interface mais clara sobre escapes de string sem escape versus aumento de escapes de regex

  • fornecer alguma flexibilidade na \dexpansão, e talvez o\b

  • forneça métodos de conveniência que tratam de virar e chamar Pattern.compile ou String.matches ou outros enfeites para você

Para liberação de produção, ele deve ter javadoc e um conjunto de testes JUnit. Posso incluir meu gigatester, mas não está escrito como testes JUnit.


Termo aditivo

Eu tenho boas e más notícias.

A boa notícia é que agora eu tenho uma aproximação muito próxima de um cluster de grafema estendido para usar para melhorar \X.

A má notícia ☺ é que esse padrão é:

(?:(?:\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 em Java você escreveria 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
fonte
10
Isso é incrível. Muito obrigado.
Tim Pietzcker
9
Cristo, essa é uma resposta esclarecedora. Só não entendo a referência de Jon Skeet. O que ele tem a ver com isso?
BalusC
12
@BalusC: É uma referência a Jon dizendo anteriormente que ele me deixaria responder à pergunta. Mas por favor, não deixe o t@tchrist. Pode ir à minha cabeça. :)
tchrist
3
Você já pensou em adicionar isso ao OpenJDK?
Martijn Verburg
2
@ Martijn: Eu não tinha, não; Eu não sabia que era tão "aberto". :) Mas pensei em liberá-lo em um sentido mais formal; outros no meu departamento desejam que isso seja feito (com algum tipo de licença de código aberto, provavelmente BSD ou ASL). Provavelmente vou mudar a API do que é neste protótipo alfa, limpar o código etc. Mas isso nos ajuda tremendamente, e achamos que ajudará outras pessoas também. Eu realmente gostaria que a Sun fizesse algo com a biblioteca deles, mas a Oracle não inspira confiança.
tchrist
15

É realmente lamentável que \wnão funcione. A solução proposta \p{Alpha}também não funciona para mim.

Parece [\p{L}]pegar todas as letras Unicode. Portanto, o equivalente Unicode de \wdeveria ser [\p{L}\p{Digit}_].

musiKk
fonte
Mas \wtambém corresponde a dígitos e muito mais. Eu acho que por apenas letras, \p{L}funcionaria.
Tim Pietzcker
Você está certo. \p{L}basta. Também pensei que apenas as letras eram o problema. [\p{L}\p{Digit}_]deve pegar todos os caracteres alfanuméricos, incluindo sublinhado.
musikk
@MusicKk: veja minha resposta para uma solução completa que permite escrever seus padrões normalmente, mas depois passar por uma função que corrige as lacunas abertas do Java para que funcione corretamente no Unicode.
tchrist
Não, \wé definido pelo Unicode como sendo muito mais amplo do que apenas \pLe os dígitos ASCII, de todas as coisas tolas. Você deve escrever [\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]se quiser um compatível com Unicode \wpara Java - ou você pode simplesmente usar minha unicode_charclassfunção a partir daqui . Desculpe!
tchrist
1
@ Tim, sim, pois as letras \pLfuncionam (você não precisa adotar adereços de uma letra). No entanto, você raramente deseja isso, porque é necessário ter cuidado para que sua correspondência não obtenha respostas diferentes, apenas porque seus dados estão no Formulário de Normalização D Unicode (também conhecido como NFD, que significa decomposição canônica ), em vez de estar em NFC (NFD seguido por canônico). composição ). Um exemplo é que o ponto de código U + E9 ( "é") é um \pLformato NFC, mas seu formato NFD se torna U + 65.301, correspondendo \pL\pM. Você pode tipo de contornar isso com \X: (?:(?=\pL)\X), mas você vai precisar da minha versão do que em Java. :(
tchrist
7

Em Java \we \dnão são compatíveis com Unicode; eles correspondem apenas aos caracteres ASCII [A-Za-z0-9_]e [0-9]. O mesmo vale para os \p{Alpha}amigos (as "classes de caracteres" POSIX nas quais elas se baseiam devem ser sensíveis ao código do idioma, mas em Java elas apenas correspondem aos caracteres ASCII). Se você deseja corresponder os "caracteres da palavra" Unicode, é necessário explicá-lo, por exemplo [\pL\p{Mn}\p{Nd}\p{Pc}], para letras, modificadores não espaçadores (acentos), dígitos decimais e pontuação de conexão.

No entanto, o Java \b é compatível com Unicode; ele usa Character.isLetterOrDigit(ch)e verifica também letras acentuadas, mas o único caractere de "pontuação de conexão" que reconhece é o sublinhado. EDIT: quando tento seu código de exemplo, ele é impresso ""e élève"como deveria ( veja em ideone.com ).

Alan Moore
fonte
Sinto muito, Alan, mas você realmente não pode dizer que o Java \bé habilitado para Unicode. Comete toneladas e toneladas de erros. "\u2163=", "\u24e7="E "\u0301="tudo não padrão combinado "\\b="em Java, mas são supostamente para - como perl -le 'print /\b=/ || 0 for "\x{2163}=", "\x{24e7}=", "\x{301}="'revela. No entanto, se (e somente se) você trocar na minha versão de um limite de palavras em vez do nativo \bem Java, todos eles também funcionarão em Java.
tchrist
@ tchrist: Eu não estava comentando a \bcorreção, apenas apontando que ele opera com caracteres Unicode (conforme implementado em Java), não apenas como \wamigos e amigos do ASCII . No entanto, ela não funciona corretamente com relação a \u0301quando esse personagem está emparelhado com um caractere base, como em e\u0301=. E não estou convencido de que Java esteja errado neste caso. Como uma marca combinada pode ser considerada um caractere de palavra, a menos que faça parte de um cluster de grafema com uma letra?
Alan Moore
3
@ Alan, isso foi esclarecido quando o Unicode esclareceu os clusters de grafema discutindo os clusters de grafema estendido versus legado. A definição antiga de um cluster de grafema, em que \Xsignifica uma não-marca seguida por qualquer número de marcas, é problemática, porque você deve ser capaz de descrever todos os arquivos como correspondências /^(\X*\R)*\R?$/, mas não pode se tiver uma \pMno início de o arquivo, ou mesmo de uma linha. Portanto, eles tentaram sempre corresponder a pelo menos um caractere. Sempre funcionou, mas agora faz o padrão acima funcionar. [... continuação ...]
tchrist 30/11/10
2
@ Alan, faz mais mal do que bem que o nativo do Java \bseja parcialmente compatível com Unicode. Considere combinar a sequência "élève"com o padrão \b(\w+)\b. Vê o problema?
tchrist
1
@ tchrist: Sim, sem os limites da palavra, \w+encontra duas correspondências: le ve, o que é ruim o suficiente. Mas, com os limites das palavras, não encontra nada, porque \breconhece ée ècomo caracteres das palavras. No mínimo, \be \wdeve concordar com o que é um caractere de palavra e o que não é.
Alan Moore