Tokenizzare una stringa ma ignorando i delimitatori tra virgolette

Vorrei avere la seguente stringa

!cmd 45 90 "An argument" Another AndAnother "Another one in quotes" 

diventare una matrice di quanto segue

 { "!cmd", "45", "90", "An argument", "Another", "AndAnother", "Another one in quotes" } 

Provai

 new StringTokenizer(cmd, "\"") 

ma questo restituirebbe “Un altro” e “Eun altro come” Un altro AndAnother “che non è l’effetto desiderato.

Grazie.

EDIT: Ho cambiato ancora l’esempio, questa volta credo che spieghi meglio la situazione, anche se non è diverso dal secondo esempio.

È molto più semplice usare java.util.regex.Matcher e fare un find() piuttosto che qualsiasi tipo di split in questo tipo di scenario.

Cioè, invece di definire il modello per il delimitatore tra i token, si definisce il modello per i token stessi .

Ecco un esempio:

  String text = "1 2 \"333 4\" 55 6 \"77\" 8 999"; // 1 2 "333 4" 55 6 "77" 8 999 String regex = "\"([^\"]*)\"|(\\S+)"; Matcher m = Pattern.compile(regex).matcher(text); while (m.find()) { if (m.group(1) != null) { System.out.println("Quoted [" + m.group(1) + "]"); } else { System.out.println("Plain [" + m.group(2) + "]"); } } 

Le stampe di cui sopra ( come visto su ideone.com ):

 Plain [1] Plain [2] Quoted [333 4] Plain [55] Plain [6] Quoted [77] Plain [8] Plain [999] 

Il modello è essenzialmente:

 "([^"]*)"|(\S+) \_____/ \___/ 1 2 

Ci sono 2 alternative:

  • La prima alternativa corrisponde alla virgoletta doppia di apertura, una sequenza di tutto tranne la doppia citazione (catturata nel gruppo 1), quindi la doppia virgoletta di chiusura
  • La seconda alternativa corrisponde a qualsiasi sequenza di caratteri non bianchi, acquisita nel gruppo 2
  • L’ordine dei supplenti è importante in questo schema

Si noti che questo non gestisce le virgolette doppie con escape all’interno dei segmenti quotati. Se è necessario eseguire questa operazione, il motivo diventa più complicato, ma la soluzione Matcher funziona ancora.

Riferimenti

  • regular-expressions.info/Brackets per Raggruppamento e Cattura , Alternanza con Barre Verticali , Classe Carattere , Ripetizione con Stella e Plus

Guarda anche

  • regular-expressions.info/Examples – Programmer – Stringhe – per pattern con citazioni con escape

Appendice

Nota che StringTokenizer è una class legacy . Si consiglia di utilizzare java.util.Scanner o String.split o, naturalmente, java.util.regex.Matcher per la massima flessibilità.

Domande correlate

  • Differenza tra un’API deprecata e legacy?
  • Scanner vs. StringTokenizer vs. String.Split
  • Convalida dell’input usando java.util.Scanner – ha molti esempi

Fallo alla vecchia maniera. Crea una funzione che guardi ciascun carattere in un ciclo for. Se il carattere è uno spazio, prendi tutto fino a quello (escluso lo spazio) e aggiungilo come una voce all’array. Annota la posizione e fai lo stesso, aggiungendo quella successiva all’array dopo uno spazio. Quando si incontra una virgoletta doppia, contrassegna un booleano denominato ‘inQuote’ come vero, e ignora gli spazi quando inQuote è true. Quando premi le virgolette quando inQuote è vero, contrassegna come falso e torna a rompere le cose quando si incontra uno spazio. È quindi ansible estenderlo come necessario per supportare caratteri di escape, ecc.

Questo potrebbe essere fatto con una regex? Non lo so, immagino. Ma l’intera funzione richiederebbe meno per scrivere rispetto a questa risposta.

In un modo antiquato:

 public static String[] split(String str) { str += " "; // To detect last token when not quoted... ArrayList strings = new ArrayList(); boolean inQuote = false; StringBuilder sb = new StringBuilder(); for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (c == '"' || c == ' ' && !inQuote) { if (c == '"') inQuote = !inQuote; if (!inQuote && sb.length() > 0) { strings.add(sb.toString()); sb.delete(0, sb.length()); } } else sb.append(c); } return strings.toArray(new String[strings.size()]); } 

Presumo che le virgolette nidificate siano illegali e che anche i token vuoti possano essere omessi.

Apache Commons in soccorso!

 import org.apache.commons.text.StringTokenizer import org.apache.commons.text.matcher.StringMatcher import org.apache.commons.text.matcher.StringMatcherFactory @Grab(group='org.apache.commons', module='commons-text', version='1.3') def str = /is this 'completely "impossible"' or """slightly"" impossible" to parse?/ StringTokenizer st = new StringTokenizer( str ) StringMatcher sm = StringMatcherFactory.INSTANCE.quoteMatcher() st.setQuoteMatcher( sm ) println st.tokenList 

Produzione:

[è, questo, completamente “imansible” o “leggermente” imansible da analizzare?]

Alcune note:

  1. questo è scritto in Groovy … è in effetti uno script Groovy. La linea @Grab fornisce un indizio sul tipo di linea di dipendenza di cui hai bisogno (ad esempio in build.gradle ) … o semplicemente include il .jar nel tuo classpath, ovviamente
  2. StringTokenizer qui NON è java.util.StringTokenizer … poiché la riga di import mostra che è org.apache.commons.text.StringTokenizer
  3. la def str = ... line è un modo per produrre una String in Groovy che contiene sia le virgolette singole che le virgolette doppie senza dover andare per fuggire
  4. StringMatcherFactory in apache commons-text 1.3 può essere trovato qui : come puoi vedere, l’ INSTANCE può fornirti un sacco di diversi StringMatcher . Si potrebbe persino eseguire il rollover: ma è necessario esaminare il codice sorgente StringMatcherFactory per vedere come è fatto.
  5. SÌ! Non puoi includere solo “altro tipo di citazione” ed è correttamente interpretato come non un confine di token … ma puoi anche sfuggire alla citazione effettiva che viene utilizzata per distriggersre la tokenizzazione , raddoppiando la citazione all’interno della tokenizzazione -protected bit of the String! Prova a implementarlo con poche righe di codice … o meglio non farlo!

PS perché è meglio usare Apache Commons di qualsiasi altra soluzione? A parte il fatto che non ha senso reinventare la ruota, posso pensare ad almeno due ragioni:

  1. Gli ingegneri di Apache possono contare su di aver anticipato tutti i trucchi e sviluppato un codice affidabile, ampiamente testato e affidabile
  2. Significa che non ingombrare il tuo bel codice con metodi di utilità sciatti – hai solo un bel pezzo di codice che fa esattamente quello che dice sulla latta, lasciandoti andare avanti con, ehm, cose interessanti .. .

PPS Niente ti obbliga a guardare il codice Apache come misteriose “scatole nere”. La fonte è aperta e scritta in Java, solitamente perfettamente accessibile. Di conseguenza, sei libero di esaminare come vengono fatte le cose a tuo piacimento. Spesso è abbastanza istruttivo farlo.

dopo

Interessato abbastanza dalla domanda di ArtB, ho dato un’occhiata alla fonte:

in StringMatcherFactory.java vediamo:

 private static final AbstractStringMatcher.CharSetMatcher QUOTE_MATCHER = new AbstractStringMatcher.CharSetMatcher( "'\"".toCharArray()); 

… abbastanza noioso …

in modo che induca uno a cercare StringTokenizer.java:

 public StringTokenizer setQuoteMatcher(final StringMatcher quote) { if (quote != null) { this.quoteMatcher = quote; } return this; } 

OK … e poi, nello stesso file java:

 private int readWithQuotes(final char[] srcChars ... 

che contiene il commento:

 // If we've found a quote character, see if it's followed by a second quote. If so, then we need to actually put the quote character into the token rather than end the token. 

… Non posso essere disturbato a seguire gli indizi ulteriormente. Avete una scelta: o la vostra soluzione “hackish”, in cui sistematicamente pre-processate le vostre stringhe prima di inviarle per la tokenizzazione, girando | \\\ “| s in | \” \ “| s … (cioè dove sostituite ogni | \ “ | with | ” “ |) …
Oppure … esamini org.apache.commons.text.StringTokenizer.java per capire come modificare il codice. È un file piccolo. Non penso che sarebbe così difficile. Quindi si compila, essenzialmente facendo un fork del codice Apache.

Non penso che possa essere configurato. Ma se hai trovato una soluzione di modifica del codice che avrebbe avuto senso potresti inviarla ad Apache e quindi potrebbe essere accettata per la successiva iterazione del codice, e il tuo nome figurerebbe almeno nella parte “richiesta di funzionalità” di Apache: questo potrebbe essere una forma di kleos attraverso la quale si raggiunge la programmazione dell’immortalità …

L’esempio che hai qui dovrebbe essere diviso dal carattere di doppia citazione.

Questa è una vecchia domanda, tuttavia questa era la mia soluzione come macchina a stati finiti.

Trucchi efficienti, prevedibili e senza fantasia.

Copertura del 100% sui test.

Trascina e rilascia nel tuo codice.

 /** * Splits a command on whitespaces. Preserves whitespace in quotes. Trims excess whitespace between chunks. Supports quote * escape within quotes. Failed escape will preserve escape char. * * @return List of split commands */ static List splitCommand(String inputString) { List matchList = new LinkedList<>(); LinkedList charList = inputString.chars() .mapToObj(i -> (char) i) .collect(Collectors.toCollection(LinkedList::new)); // Finite-State Automaton for parsing. CommandSplitterState state = CommandSplitterState.BeginningChunk; LinkedList chunkBuffer = new LinkedList<>(); for (Character currentChar : charList) { switch (state) { case BeginningChunk: switch (currentChar) { case '"': state = CommandSplitterState.ParsingQuote; break; case ' ': break; default: state = CommandSplitterState.ParsingWord; chunkBuffer.add(currentChar); } break; case ParsingWord: switch (currentChar) { case ' ': state = CommandSplitterState.BeginningChunk; String newWord = chunkBuffer.stream().map(Object::toString).collect(Collectors.joining()); matchList.add(newWord); chunkBuffer = new LinkedList<>(); break; default: chunkBuffer.add(currentChar); } break; case ParsingQuote: switch (currentChar) { case '"': state = CommandSplitterState.BeginningChunk; String newWord = chunkBuffer.stream().map(Object::toString).collect(Collectors.joining()); matchList.add(newWord); chunkBuffer = new LinkedList<>(); break; case '\\': state = CommandSplitterState.EscapeChar; break; default: chunkBuffer.add(currentChar); } break; case EscapeChar: switch (currentChar) { case '"': // Intentional fall through case '\\': state = CommandSplitterState.ParsingQuote; chunkBuffer.add(currentChar); break; default: state = CommandSplitterState.ParsingQuote; chunkBuffer.add('\\'); chunkBuffer.add(currentChar); } } } if (state != CommandSplitterState.BeginningChunk) { String newWord = chunkBuffer.stream().map(Object::toString).collect(Collectors.joining()); matchList.add(newWord); } return matchList; } private enum CommandSplitterState { BeginningChunk, ParsingWord, ParsingQuote, EscapeChar } 

Un altro modo in cui la vecchia scuola è:

 public static void main(String[] args) { String text = "One two \"three four\" five \"six seven eight\" nine \"ten\""; String[] splits = text.split(" "); List list = new ArrayList<>(); String token = null; for(String s : splits) { if(s.startsWith("\"") ) { token = "" + s; } else if (s.endsWith("\"")) { token = token + " "+ s; list.add(token); token = null; } else { if (token != null) { token = token + " " + s; } else { list.add(s); } } } System.out.println(list); } 

Risultato: – [Uno, due, “tre quattro”, cinque, “sei sette otto”, nove]

prova questo:

 String str = "One two \"three four\" five \"six seven eight\" nine \"ten\""; String[] strings = str.split("[ ]?\"[ ]?"); 

Non conosco il contesto di ciò che stai cercando di fare, ma sembra che tu stia cercando di analizzare gli argomenti della riga di comando. In generale, questo è abbastanza complicato con tutti i problemi che sfuggono; se questo è il tuo objective, guarderei personalmente qualcosa come JCommander.

Prova questo:

 String str = "One two \"three four\" five \"six seven eight\" nine \"ten\""; String strArr[] = str.split("\"|\s"); 

È un po ‘complicato perché devi sfuggire alle doppie virgolette. Questa espressione regolare dovrebbe tokenize la stringa utilizzando uno spazio bianco (\ s) o una doppia citazione.

Dovresti usare il metodo split di String perché accetta espressioni regolari, mentre l’argomento costruttore per delimitatore in StringTokenizer no. Alla fine di ciò che ho fornito sopra, puoi semplicemente aggiungere quanto segue:

 String s; for(String k : strArr) { s += k; } StringTokenizer strTok = new StringTokenizer(s);