Equivalenti Unicode per \ w e \ b nelle espressioni regolari Java?


126

Molte implementazioni regex moderne interpretano la \wscorciatoia della classe di caratteri come "qualsiasi lettera, cifra o punteggiatura di collegamento" (di solito: trattino basso). In questo modo, una regex come \w+partite parole come hello, élève, GOÄ_432o gefräßig.

Sfortunatamente, Java no. In Java, \wè limitato a [A-Za-z0-9_]. Questo rende difficile trovare parole come quelle sopra menzionate, tra gli altri problemi.

Sembra anche che la \bparola separatore corrisponda nei punti in cui non dovrebbe.

Quale sarebbe l'equivalente corretto di un .NET simile, compatibile con Unicode \wo \bin Java? Quali altre scorciatoie necessitano di "riscrittura" per renderle compatibili con Unicode?


3
Il racconto, Tim, è che tutti hanno bisogno di scrivere per metterli in linea con Unicode. Non vedo ancora alcun segno che Java 1.7 farà qualcosa di più con le proprietà Unicode che aggiungere infine il supporto per gli script, ma il gioco è fatto. Ci sono alcune cose che davvero non puoi fare senza un migliore accesso al complemento completo delle proprietà Unicode. Se non hai ancora le mie sceneggiature uniprops e unichars (e uniname ), sono sbalorditivi in ​​tutto questo.
tchrist,

Si potrebbe considerare di aggiungere segni alla classe di parole. Poiché ad esempio & auml; può essere rappresentato in Unicode come \ u0061 \ u0308 o \ u00E4.
Mostowski Collapse

3
Ehi Tim, dai un'occhiata al mio AGGIORNAMENTO. Hanno aggiunto una bandiera per far funzionare tutto. Evviva!
tchrist

Risposte:


240

Codice sorgente

Il codice sorgente per le funzioni di riscrittura che discuterò di seguito è disponibile qui .

Aggiornamento in Java 7

La Patternclasse aggiornata di Sun per JDK7 ha una nuova meravigliosa bandiera UNICODE_CHARACTER_CLASS, che fa tornare tutto a posto. È disponibile come embeddable (?U)per all'interno del modello, quindi puoi usarlo anche con i Stringwrapper della classe. Mette in mostra anche definizioni corrette per varie altre proprietà. Traccia ora lo standard Unicode, sia in RL1.2 che in RL1.2a da UTS # 18: Unicode Regular Expressions . Si tratta di un miglioramento entusiasmante e drammatico e il team di sviluppo deve essere lodato per questo importante sforzo.


Problemi Unicode Regex di Java

Il problema con Java regex è che i Perl 1.0 charclass fughe - il che significa \w, \b, \s, \de loro complementi - non sono in Java estesi a lavoro con Unicode. Solo tra questi, \bgode di una semantica estesa, ma questi non associano \wné a identificatori Unicode né a proprietà di interruzione di riga Unicode .

Inoltre, è possibile accedere alle proprietà POSIX in Java in questo modo:

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}

Si tratta di un vero e proprio pasticcio, perché vuol dire che le cose piace Alpha, Lowere Spacelo fanno non in carta Java per l'Unicode Alphabetic, Lowercaseo Whitespaceproprietà. Questo è estremamente fastidioso. Il supporto delle proprietà Unicode di Java è rigorosamente antemillennale , nel senso che non supporta alcuna proprietà Unicode emersa nell'ultimo decennio.

Non essere in grado di parlare correttamente degli spazi bianchi è super fastidioso. Considera la seguente tabella. Per ciascuno di questi punti di codice, esiste sia una colonna J-results per Java che una colonna P-results per Perl o qualsiasi altro motore regex basato su 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  -

Guarda quello?

Praticamente ognuno di quei risultati dello spazio bianco Java è ̲w̲r̲o̲n̲g̲ secondo Unicode. È davvero un grosso problema. Java è appena incasinato, dando risposte “sbagliate” secondo la pratica esistente e anche secondo Unicode. Inoltre Java non ti dà nemmeno accesso alle proprietà Unicode reali! In effetti, Java non supporta alcuna proprietà che corrisponde allo spazio bianco Unicode.


La soluzione a tutti questi problemi e altro ancora

Per affrontare questo e molti altri problemi correlati, ieri ho scritto una funzione Java per riscrivere una stringa di pattern che riscrive queste 14 escape di classe:

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

sostituendoli con elementi che funzionano effettivamente per abbinare Unicode in modo prevedibile e coerente. È solo un prototipo alfa da una singola sessione di hacking, ma è completamente funzionale.

Il racconto è che il mio codice riscrive quei 14 come segue:

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

Alcune cose da considerare ...

  • Che utilizza per la sua \Xdefinizione quanto Unicode ora si riferisce ad un gruppo legacy grafema , non un gruppo grafema estesa , in quanto quest'ultimo è alquanto più complicata. Perl stesso ora utilizza la versione più elaborata, ma la vecchia versione è ancora perfettamente realizzabile per le situazioni più comuni. EDIT: vedi addendum in fondo.

  • Cosa fare \ddipende dal tuo intento, ma l'impostazione predefinita è la definizione Uniode. Vedo persone che non sempre vogliono \p{Nd}, ma a volte o [0-9]o \pN.

  • Le due definizioni di confine \be \B, sono specificamente scritte per utilizzare la \wdefinizione.

  • Tale \wdefinizione è eccessivamente ampia, perché afferra le lettere con pergamena non solo quelle cerchiate. La Other_Alphabeticproprietà Unicode non è disponibile fino a JDK7, quindi è il massimo che puoi fare.


Esplorare i confini

I confini sono stati un problema sin da quando Larry Wall ha coniato per la prima volta la \be la \Bsintassi per parlarne per Perl 1.0 nel 1987. La chiave per capire come \bed \Bentrambi funzionano è dissipare due miti pervasivi su di loro:

  1. Sono sempre e solo alla ricerca di \wcaratteri di parole, mai di non parole.
  2. Non cercano specificamente il bordo della stringa.

Un \bconfine significa:

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

E quelli sono tutti definiti perfettamente come:

  • segue la parola è (?<=\w).
  • la parola precedente è (?=\w).
  • non segue la parola è (?<!\w).
  • non precede la parola è (?!\w).

Pertanto, poiché IF-THENè codificato come un and insieme ABin regex, an oris X|Y, e poiché andè maggiore in precedenza rispetto a or, è semplicemente AB|CD. Quindi ogni cosa \bche significhi un confine può essere tranquillamente sostituita con:

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

con il \wdefinito nel modo appropriato.

(Potresti pensare strano che i componenti Ae Csiano opposti. In un mondo perfetto, dovresti essere in grado di scriverlo AB|D, ma per un po 'stavo inseguendo contraddizioni di esclusione reciproca nelle proprietà Unicode - di cui penso di essermi preso cura , ma ho lasciato la doppia condizione al limite per ogni evenienza. Inoltre, ciò rende più estensibile se in seguito si ottengono idee extra.)

Per i \Bnon-limiti, la logica è:

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

Consentire la \Bsostituzione di tutte le istanze con:

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

Questo è davvero come \be \Bcomportarsi. Modelli equivalenti per loro sono

  • \busando il ((IF)THEN|ELSE)costrutto è(?(?<=\w)(?!\w)|(?=\w))
  • \Busando il ((IF)THEN|ELSE)costrutto è(?(?=\w)(?<=\w)|(?<!\w))

Ma le versioni con semplicemente AB|CDvanno bene, specialmente se mancano schemi condizionali nel tuo linguaggio regex - come Java. ☹

Ho già verificato il comportamento dei limiti utilizzando tutte e tre le definizioni equivalenti con una suite di test che controlla 110.385.408 corrispondenze per esecuzione e che ho eseguito su una dozzina di diverse configurazioni di dati in base a:

     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)

Tuttavia, le persone spesso vogliono un diverso tipo di confine. Vogliono qualcosa che sia consapevole degli spazi bianchi e del bordo della stringa:

  • bordo sinistro come(?:(?<=^)|(?<=\s))
  • bordo destro come(?=$|\s)

Riparazione di Java con Java

Il codice che ho postato nell'altra mia risposta offre questo e molte altre comodità. Ciò include le definizioni di parole, trattini, trattini e apostrofi in linguaggio naturale, oltre a un po 'di più.

Consente inoltre di specificare caratteri Unicode in punti di codice logici, non in surrogati UTF-16 idioti. È difficile sopravvalutare quanto sia importante! E questo è solo per l'espansione della stringa.

Per la sostituzione della classe di caratteri regex che fa sì che la classe nei regex Java funzioni finalmente su Unicode e funzioni correttamente, prendi l'intera fonte da qui . Puoi farcela a tuo piacimento, ovviamente. Se lo risolvi, mi piacerebbe saperlo, ma non è necessario. È piuttosto corto. Il coraggio della funzione di riscrittura regex principale è semplice:

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;

Comunque, quel codice è solo una versione alfa, roba che ho hackerato durante il fine settimana. Non rimarrà così.

Per la beta intendo:

  • piegare insieme la duplicazione del codice

  • fornisce un'interfaccia più chiara per quanto riguarda gli escape di stringa senza escape rispetto agli escape di regex in aumento

  • fornire una certa flessibilità \dnell'espansione, e forse il\b

  • fornire metodi di convenienza che gestiscono il voltaggio e la chiamata a Pattern.compile o String.matches o quant'altro per te

Per la versione di produzione, dovrebbe avere javadoc e una suite di test JUnit. Potrei includere il mio gigatester, ma non è scritto come test JUnit.


appendice

Ho buone notizie e cattive notizie.

La buona notizia è che ora ho un'approssimazione molto vicina a un cluster grapheme esteso da utilizzare per un miglioramento \X.

La cattiva notizia ☺ è che quel modello è:

(?:(?:\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:.))

che in Java dovresti scrivere come:

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


10
Questo è fantastico Molte grazie.
Tim Pietzcker,

9
Cristo, questa è una risposta illuminista. Solo non capisco il riferimento a Jon Skeet. Cosa ha a che fare con questo?
BalusC

12
@BalusC: è un riferimento a Jon in precedenza che mi ha detto di lasciarmi rispondere. Ma per favore, non rilasciare t@tchrist. Potrebbe andare nella mia testa. :)
dal

3
Hai pensato di aggiungere questo a OpenJDK?
Martijn Verburg,

2
@Martijn: no, no; Non sapevo che fosse "aperto". :) Ma ho pensato di rilasciarlo in un senso più formale; altri nel mio dipartimento desiderano vederlo fatto (con una specie di licenza open source, probabilmente BSD o ASL). Sono probabilmente andando a cambiare l'API da quello che è in questo prototipo alfa, ripulire il codice, ecc ma aiuta noi tremendamente, e noi capire che sarà aiutare gli altri, anche. Vorrei davvero che Sun facesse qualcosa per la loro biblioteca, ma Oracle non ispira fiducia.
tchrist,

15

È davvero un peccato che \wnon funzioni. La soluzione proposta \p{Alpha}non funziona neanche per me.

Sembra che [\p{L}]catturi tutte le lettere Unicode. Quindi \wdovrebbe essere l' equivalente Unicode di [\p{L}\p{Digit}_].


Ma \wcorrisponde anche a cifre e altro. Penso che solo per le lettere \p{L}funzionerebbe.
Tim Pietzcker,

Hai ragione. \p{L}è abbastanza. Inoltre ho pensato che solo le lettere fossero il problema. [\p{L}\p{Digit}_]dovrebbe catturare tutti i caratteri alfanumerici incluso il carattere di sottolineatura.
musiKk,

@MusicKk: vedi la mia risposta per una soluzione completa che ti permetta di scrivere i tuoi schemi normalmente, ma poi passali attraverso una funzione che corregge le lacune lacune di Java in modo che funzioni correttamente su Unicode.
tchrist,

No, \wè definito da Unicode come molto più ampio del solo \pLe delle cifre ASCII, di tutte le cose sciocche. Devi scrivere [\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]se vuoi un Unicode-compatibile \wper Java - o puoi semplicemente usare la mia unicode_charclassfunzione da qui . Scusa!
tchrist,

1
@ Tim, sì, per le lettere \pLfunziona (non è necessario abbracciare oggetti di scena di una lettera). Tuttavia, raramente lo desideri, perché devi stare piuttosto attento che la tua corrispondenza non ottenga risposte diverse solo perché i tuoi dati sono in Unicode Normalization Form D (aka NFD, che significa decomposizione canonica ) rispetto a essere in NFC (NFD seguito da canonico composizione ). Un esempio è che il punto di codice U + E9 ( "é") è \pLin forma NFC, ma la sua forma NFD diventa U + 65.301, quindi corrisponde \pL\pM. Puoi in qualche modo aggirare questo problema con \X:, (?:(?=\pL)\X)ma avrai bisogno della mia versione per Java. :(
tchrist,

7

In Java \we \dnon sono compatibili con Unicode; corrispondono solo ai caratteri ASCII [A-Za-z0-9_]e [0-9]. Lo stesso vale per gli \p{Alpha}amici (le "classi di caratteri" POSIX su cui si basano dovrebbero essere sensibili alle impostazioni locali, ma in Java hanno sempre e solo abbinato caratteri ASCII). Se si desidera abbinare "caratteri di parole" Unicode, è necessario precisarlo, ad esempio [\pL\p{Mn}\p{Nd}\p{Pc}]per lettere, modificatori senza spaziatura (accenti), cifre decimali e punteggiatura di collegamento.

Tuttavia, Java \b è un esperto di Unicode; utilizza Character.isLetterOrDigit(ch)e verifica anche le lettere accentate, ma l'unico carattere di "punteggiatura di connessione" che riconosce è il carattere di sottolineatura. EDIT: quando provo il tuo codice di esempio, stampa ""e élève"come dovrebbe ( vederlo su ideone.com ).


Mi dispiace, Alan, ma non puoi davvero dire che Java \bsia un esperto di Unicode. Fa tonnellate e tonnellate di errori. "\u2163=", "\u24e7="e "\u0301="tutti non riescono ad abbinare il modello "\\b="in Java, ma dovrebbero - come perl -le 'print /\b=/ || 0 for "\x{2163}=", "\x{24e7}=", "\x{301}="'rivela. Tuttavia, se (e solo se) si scambia la mia versione di un confine di parola anziché il nativo \bin Java, anche quelli funzionano tutti in Java.
tchrist,

@tchrist: non stavo commentando \bla correttezza, ma solo sottolineando che opera su caratteri Unicode (come implementato in Java), non solo su ASCII \we amici. Tuttavia, funziona correttamente rispetto a \u0301quando quel personaggio è associato a un personaggio base, come in e\u0301=. E non sono convinto che Java sia sbagliato in questo caso. Come può un segno di combinazione essere considerato un carattere di parola se non fa parte di un grappolo con una lettera?
Alan Moore,

3
@Alan, questo è qualcosa che è stato chiarito quando Unicode ha chiarito i cluster di grapheme discutendo i cluster di grapheme estesi vs legacy. La vecchia definizione di un cluster grapheme, in cui \Xsta per un non-mark seguito da un numero qualsiasi di mark, è problematica, perché dovresti essere in grado di descrivere tutti i file come corrispondenti /^(\X*\R)*\R?$/, ma non puoi se hai un \pMall'inizio il file, o anche di una riga. Quindi hanno deciso di abbinare sempre almeno un personaggio. Lo ha sempre fatto, ma ora fa funzionare il modello sopra. [... continua ...]
tchrist il

2
@Alan, fa più male che bene che il nativo di Java \bsia parzialmente compatibile con Unicode. Prendi in considerazione la corrispondenza della stringa "élève"con il motivo \b(\w+)\b. Vedi il problema?
tchrist,

1
@tchrist: Sì, senza i confini della parola, \w+trova due corrispondenze: le ve, che è abbastanza male. Ma con i confini delle parole non trova nulla, perché \briconosce ée ècome caratteri di parole. Come minimo, \be \wdovrebbe essere d'accordo su cosa sia una parola personaggio e cosa no.
Alan Moore,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.