Como dividir uma string, mas também manter os delimitadores?

Eu tenho uma seqüência de várias linhas que é delimitada por um conjunto de diferentes delimitadores:

(Text1)(DelimiterA)(Text2)(DelimiterC)(Text3)(DelimiterB)(Text4) 

Eu posso dividir essa string em suas partes, usando String.split , mas parece que não consigo obter a string real, que corresponde à regex do delimitador.

Em outras palavras, é isso que eu recebo:

  • Text1
  • Text2
  • Text3
  • Text4

É isso que eu quero

  • Text1
  • DelimiterA
  • Text2
  • DelimiterC
  • Text3
  • DelimiterB
  • Text4

Existe alguma maneira JDK para dividir a string usando um regex delimitador, mas também manter os delimitadores?

Você pode usar Lookahead e Lookbehind. Como isso:

 System.out.println(Arrays.toString("a;b;c;d".split("(?<=;)"))); System.out.println(Arrays.toString("a;b;c;d".split("(?=;)"))); System.out.println(Arrays.toString("a;b;c;d".split("((?<=;)|(?=;))"))); 

E você terá:

 [a;, b;, c;, d] [a, ;b, ;c, ;d] [a, ;, b, ;, c, ;, d] 

O último é o que você quer.

((?<=;)|(?=;)) é igual a selecionar um caractere vazio antes ; ou depois ; .

Espero que isto ajude.

EDITAR Comentários de Fabian Steeg sobre Legibilidade é válido. A legibilidade é sempre o problema do RegEx. Uma coisa, eu faço para ajudar a aliviar isso é criar uma variável cujo nome representa o que o regex faz e usar o formato Java String para ajudar nisso. Como isso:

 static public final String WITH_DELIMITER = "((?<=%1$s)|(?=%1$s))" ; ... public void someMethod() { ... final String[] aEach = "a;b;c;d". split(String.format(WITH_DELIMITER, ";")) ; ... } ... 

Isso ajuda um pouco. 😀

Você deseja usar lookarounds e dividir em correspondências de largura zero. aqui estão alguns exemplos:

 public class SplitNDump { static void dump(String[] arr) { for (String s : arr) { System.out.format("[%s]", s); } System.out.println(); } public static void main(String[] args) { dump("1,234,567,890".split(",")); // "[1][234][567][890]" dump("1,234,567,890".split("(?=,)")); // "[1][,234][,567][,890]" dump("1,234,567,890".split("(?<=,)")); // "[1,][234,][567,][890]" dump("1,234,567,890".split("(?<=,)|(?=,)")); // "[1][,][234][,][567][,][890]" dump(":a:bb::c:".split("(?=:)|(?<=:)")); // "[][:][a][:][bb][:][:][c][:]" dump(":a:bb::c:".split("(?=(?!^):)|(?<=:)")); // "[:][a][:][bb][:][:][c][:]" dump(":::a::::bb::c:".split("(?=(?!^):)(? 

E sim, isso é uma afirmação triplamente aninhada lá no último padrão.

Perguntas relacionadas

  • A divisão Java está comendo meus personagens.
  • Você pode usar a regex de correspondência de largura zero na divisão de string?
  • Como faço para converter o CamelCase em nomes legíveis por humanos em Java?
  • Backreferences em lookbehind

Veja também

  • regular-expressions.info/Lookarounds

Uma solução muito ingênua, que não envolva regex, seria executar uma string no seu delimitador ao longo das linhas de (supondo vírgula para delimitador):

 string.replace(FullString, "," , "~,~") 

Onde você pode replace o tilda (~) por um delimitador exclusivo apropriado.

Então, se você fizer uma divisão no seu novo delimitador, então acredito que você obterá o resultado desejado.

 import java.util.regex.*; import java.util.LinkedList; public class Splitter { private static final Pattern DEFAULT_PATTERN = Pattern.compile("\\s+"); private Pattern pattern; private boolean keep_delimiters; public Splitter(Pattern pattern, boolean keep_delimiters) { this.pattern = pattern; this.keep_delimiters = keep_delimiters; } public Splitter(String pattern, boolean keep_delimiters) { this(Pattern.compile(pattern==null?"":pattern), keep_delimiters); } public Splitter(Pattern pattern) { this(pattern, true); } public Splitter(String pattern) { this(pattern, true); } public Splitter(boolean keep_delimiters) { this(DEFAULT_PATTERN, keep_delimiters); } public Splitter() { this(DEFAULT_PATTERN); } public String[] split(String text) { if (text == null) { text = ""; } int last_match = 0; LinkedList splitted = new LinkedList(); Matcher m = this.pattern.matcher(text); while (m.find()) { splitted.add(text.substring(last_match,m.start())); if (this.keep_delimiters) { splitted.add(m.group()); } last_match = m.end(); } splitted.add(text.substring(last_match)); return splitted.toArray(new String[splitted.size()]); } public static void main(String[] argv) { if (argv.length != 2) { System.err.println("Syntax: java Splitter  "); return; } Pattern pattern = null; try { pattern = Pattern.compile(argv[0]); } catch (PatternSyntaxException e) { System.err.println(e); return; } Splitter splitter = new Splitter(pattern); String text = argv[1]; int counter = 1; for (String part : splitter.split(text)) { System.out.printf("Part %d: \"%s\"\n", counter++, part); } } } /* Example: > java Splitter "\W+" "Hello World!" Part 1: "Hello" Part 2: " " Part 3: "World" Part 4: "!" Part 5: "" */ 

Eu realmente não gosto do outro lado, onde você ganha um elemento vazio na frente e atrás. Um delimitador geralmente não está no início ou no final da string, portanto, na maioria das vezes, você acaba desperdiçando dois bons slots de array.

Editar: casos de limite fixo. Fonte comentada com casos de teste pode ser encontrada aqui: http://snippets.dzone.com/posts/show/6453

Cheguei atrasado, mas voltando à pergunta original, por que não usar apenas lookarounds?

 Pattern p = Pattern.compile("(?<=\\w)(?=\\W)|(?<=\\W)(?=\\w)"); System.out.println(Arrays.toString(p.split("'ab','cd','eg'"))); System.out.println(Arrays.toString(p.split("boo:and:foo"))); 

saída:

 [', ab, ',', cd, ',', eg, '] [boo, :, and, :, foo] 

EDIT: o que você vê acima é o que aparece na linha de comando quando eu executar esse código, mas agora vejo que é um pouco confuso. É difícil controlar quais vírgulas fazem parte do resultado e quais foram adicionadas por Arrays.toString() . O destaque da syntax do SO também não ajuda. Na esperança de obter o realce para trabalhar comigo em vez de contra mim, eis como essas matrizes pareceriam que eu as estava declarando no código-fonte:

 { "'", "ab", "','", "cd", "','", "eg", "'" } { "boo", ":", "and", ":", "foo" } 

Espero que seja mais fácil de ler. Obrigado pelo heads-up, @finnw.

Eu sei que esta é uma pergunta muito antiga e a resposta também foi aceita. Mas ainda assim gostaria de enviar uma resposta muito simples à pergunta original. Considere este código:

 String str = "Hello-World:How\nAre You&doing"; inputs = str.split("(?!^)\\b"); for (int i=0; i 

SAÍDA:

 a[0] = "Hello" a[1] = "-" a[2] = "World" a[3] = ":" a[4] = "How" a[5] = " " a[6] = "Are" a[7] = " " a[8] = "You" a[9] = "&" a[10] = "doing" 

Estou usando apenas o limite de palavras \b para delimitar as palavras, exceto quando é o início do texto.

Eu dei uma olhada nas respostas acima e honestamente nenhuma delas eu acho satisfatória. O que você quer fazer é essencialmente imitar a funcionalidade de divisão Perl. Por que o Java não permite isso e ter um método join () em algum lugar está além de mim, mas eu discordo. Você nem precisa de uma aula para isso. É apenas uma function. Execute este programa de amostra:

Algumas das respostas anteriores têm verificação excessiva de nulos, que recentemente escrevi uma resposta a uma pergunta aqui:

https://stackoverflow.com/users/18393/cletus

De qualquer forma, o código:

 public class Split { public static List split(String s, String pattern) { assert s != null; assert pattern != null; return split(s, Pattern.compile(pattern)); } public static List split(String s, Pattern pattern) { assert s != null; assert pattern != null; Matcher m = pattern.matcher(s); List ret = new ArrayList(); int start = 0; while (m.find()) { ret.add(s.substring(start, m.start())); ret.add(m.group()); start = m.end(); } ret.add(start >= s.length() ? "" : s.substring(start)); return ret; } private static void testSplit(String s, String pattern) { System.out.printf("Splitting '%s' with pattern '%s'%n", s, pattern); List tokens = split(s, pattern); System.out.printf("Found %d matches%n", tokens.size()); int i = 0; for (String token : tokens) { System.out.printf(" %d/%d: '%s'%n", ++i, tokens.size(), token); } System.out.println(); } public static void main(String args[]) { testSplit("abcdefghij", "z"); // "abcdefghij" testSplit("abcdefghij", "f"); // "abcde", "f", "ghi" testSplit("abcdefghij", "j"); // "abcdefghi", "j", "" testSplit("abcdefghij", "a"); // "", "a", "bcdefghij" testSplit("abcdefghij", "[bdfh]"); // "a", "b", "c", "d", "e", "f", "g", "h", "ij" } } 

Eu gosto da idéia de StringTokenizer porque é Enumerable.
Mas também é obsoleto e substitui por String.split que retorna uma String chata [] (e não inclui os delimitadores).

Então eu implementei um StringTokenizerEx que é um Iterable, e que leva um verdadeiro regexp para dividir uma string.

Um verdadeiro regexp significa que não é uma “sequência de caracteres” repetida para formar o delimitador:
‘o’ corresponderá apenas a ‘o’ e dividirá ‘ooo’ em três delimitadores, com duas sequências vazias no interior:

 [o], '', [o], '', [o] 

Mas o regexp o + retornará o resultado esperado ao dividir “aooob”

 [], 'a', [ooo], 'b', [] 

Para usar este StringTokenizerEx:

 final StringTokenizerEx aStringTokenizerEx = new StringTokenizerEx("boo:and:foo", "o+"); final String firstDelimiter = aStringTokenizerEx.getDelimiter(); for(String aString: aStringTokenizerEx ) { // uses the split String detected and memorized in 'aString' final nextDelimiter = aStringTokenizerEx.getDelimiter(); } 

O código desta class está disponível no DZone Snippets .

Como de costume para uma resposta de desafio de código (uma class independente com casos de teste incluídos), copie e cole-a (em um diretório ‘src / test’) e execute-a . Seu método main () ilustra os diferentes usos.


Nota: (edição de final de 2009)

O artigo Final Thoughts: Java Puzzler: Splitting Hairs faz um bom trabalho explicando o comportamento bizarro em String.split() .
Josh Bloch até comentou em resposta a esse artigo:

Sim, isso é uma dor. FWIW, foi feito por uma boa razão: compatibilidade com o Perl.
O cara que fez isso é Mike “madbot” McCloskey, que agora trabalha conosco no Google. Mike certificou-se de que as expressões regulares do Java passassem virtualmente todos os testes de expressão regular de 30K Perl (e funcionassem mais rápido).

A biblioteca comum do Google , Guava, também contém um Divisor, que é:

  • mais simples de usar
  • mantido pelo Google (e não por você)

Por isso, vale a pena ser verificado. De sua documentação preliminar inicial (pdf) :

JDK tem isso:

 String[] pieces = "foo.bar".split("\\."); 

É bom usar isso se você quiser exatamente o que ele faz: – expressão regular – resultado como uma matriz – sua maneira de lidar com peças vazias

Mini-puzzler: “, a ,, b,”. Split (“,”) retorna …

 (a) "", "a", "", "b", "" (b) null, "a", null, "b", null (c) "a", null, "b" (d) "a", "b" (e) None of the above 

Resposta: (e) Nenhuma das opções acima.

 ",a,,b,".split(",") returns "", "a", "", "b" 

Apenas vazios à direita são ignorados! (Quem sabe a solução para evitar o salto? É divertido …)

Em qualquer caso, nosso Divisor é simplesmente mais flexível: O comportamento padrão é simplista:

 Splitter.on(',').split(" foo, ,bar, quux,") --> [" foo", " ", "bar", " quux", ""] 

Se você quiser resources extras, peça para eles!

 Splitter.on(',') .trimResults() .omitEmptyStrings() .split(" foo, ,bar, quux,") --> ["foo", "bar", "quux"] 

Ordem de methods de configuração não importa – durante a divisão, o corte acontece antes de verificar se há vazios.

Aqui está uma implementação simples e limpa que é consistente com Pattern#split e trabalha com padrões de comprimento variável, que não podem suportar, e é mais fácil de usar. É semelhante à solução fornecida pelo @cletus.

 public static String[] split(CharSequence input, String pattern) { return split(input, Pattern.compile(pattern)); } public static String[] split(CharSequence input, Pattern pattern) { Matcher matcher = pattern.matcher(input); int start = 0; List result = new ArrayList<>(); while (matcher.find()) { result.add(input.subSequence(start, matcher.start()).toString()); result.add(matcher.group()); start = matcher.end(); } if (start != input.length()) result.add(input.subSequence(start, input.length()).toString()); return result.toArray(new String[0]); } 

Eu não faço verificações de nulos aqui, Pattern#split não, por que deveria I. Eu não gosto do if no final, mas é necessário para a consistência com o Pattern#split . Caso contrário, eu seria anexado incondicionalmente, resultando em uma cadeia vazia como o último elemento do resultado, se a cadeia de input terminar com o padrão.

Eu converto para String [] para consistência com Pattern#split , eu uso new String[0] ao invés de new String[result.size()] , veja aqui o porquê.

Aqui estão meus testes:

 @Test public void splitsVariableLengthPattern() { String[] result = Split.split("/foo/$bar/bas", "\\$\\w+"); Assert.assertArrayEquals(new String[] { "/foo/", "$bar", "/bas" }, result); } @Test public void splitsEndingWithPattern() { String[] result = Split.split("/foo/$bar", "\\$\\w+"); Assert.assertArrayEquals(new String[] { "/foo/", "$bar" }, result); } @Test public void splitsStartingWithPattern() { String[] result = Split.split("$foo/bar", "\\$\\w+"); Assert.assertArrayEquals(new String[] { "", "$foo", "/bar" }, result); } @Test public void splitsNoMatchesPattern() { String[] result = Split.split("/foo/bar", "\\$\\w+"); Assert.assertArrayEquals(new String[] { "/foo/bar" }, result); } 

Passe o terceiro aurgument como “true”. Ele também retornará os delimitadores.

 StringTokenizer(String str, String delimiters, true); 

Vou postar minhas versões de trabalho também (primeiro é muito parecido com o Markus).

 public static String[] splitIncludeDelimeter(String regex, String text){ List list = new LinkedList<>(); Matcher matcher = Pattern.compile(regex).matcher(text); int now, old = 0; while(matcher.find()){ now = matcher.end(); list.add(text.substring(old, now)); old = now; } if(list.size() == 0) return new String[]{text}; //adding rest of a text as last element String finalElement = text.substring(old); list.add(finalElement); return list.toArray(new String[list.size()]); } 

E aqui está a segunda solução e sua rodada 50% mais rápida que a primeira:

 public static String[] splitIncludeDelimeter2(String regex, String text){ List list = new LinkedList<>(); Matcher matcher = Pattern.compile(regex).matcher(text); StringBuffer stringBuffer = new StringBuffer(); while(matcher.find()){ matcher.appendReplacement(stringBuffer, matcher.group()); list.add(stringBuffer.toString()); stringBuffer.setLength(0); //clear buffer } matcher.appendTail(stringBuffer); ///dodajemy reszte ciagu list.add(stringBuffer.toString()); return list.toArray(new String[list.size()]); } 

Eu não sei de uma function existente na API Java que faz isso (o que não quer dizer que não existe), mas aqui está minha própria implementação (um ou mais delimitadores serão retornados como um único token; se você quiser cada delimitador a ser retornado como um token separado, precisará de um pouco de adaptação):

 static String[] splitWithDelimiters(String s) { if (s == null || s.length() == 0) { return new String[0]; } LinkedList result = new LinkedList(); StringBuilder sb = null; boolean wasLetterOrDigit = !Character.isLetterOrDigit(s.charAt(0)); for (char c : s.toCharArray()) { if (Character.isLetterOrDigit(c) ^ wasLetterOrDigit) { if (sb != null) { result.add(sb.toString()); } sb = new StringBuilder(); wasLetterOrDigit = !wasLetterOrDigit; } sb.append(c); } result.add(sb.toString()); return result.toArray(new String[0]); } 

Eu sugiro usar Pattern and Matcher, que quase certamente vai conseguir o que você quer. Sua expressão regular precisará ser um pouco mais complicada do que a que você está usando em String.split.

Eu não acho que é possível com String#split , mas você pode usar um StringTokenizer , embora isso não permita que você defina seu delimitador como um regex, mas apenas como uma class de caracteres de um dígito:

 new StringTokenizer("Hello, world. Hi!", ",.!", true); // true for returnDelims 

Se você puder pagar, use o método replace (CharSequence target, CharSequence replacement) do Java e preencha outro delimitador para dividir com. Exemplo: eu quero dividir a string “boo: e: foo” e manter ‘:’ no seu lado direito String.

 String str = "boo:and:foo"; str = str.replace(":","newdelimiter:"); String[] tokens = str.split("newdelimiter"); 

Nota importante: Isso só funciona se você não tiver mais “newdelimiter” na sua String! Assim, não é uma solução geral. Mas se você conhece um CharSequence do qual você pode ter certeza de que ele nunca aparecerá na String, esta é uma solução muito simples.

Outra solução candidata usando um regex. Mantém a ordem dos tokens, corresponde corretamente vários tokens do mesmo tipo em uma linha. A desvantagem é que o regex é meio desagradável.

 package javaapplication2; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class JavaApplication2 { /** * @param args the command line arguments */ public static void main(String[] args) { String num = "58.5+variable-+98*78/96+a/78.7-3443*12-3"; // Terrifying regex: // (a)|(b)|(c) match a or b or c // where // (a) is one or more digits optionally followed by a decimal point // followed by one or more digits: (\d+(\.\d+)?) // (b) is one of the set + * / - occurring once: ([+*/-]) // (c) is a sequence of one or more lowercase latin letter: ([az]+) Pattern tokenPattern = Pattern.compile("(\\d+(\\.\\d+)?)|([+*/-])|([az]+)"); Matcher tokenMatcher = tokenPattern.matcher(num); List tokens = new ArrayList<>(); while (!tokenMatcher.hitEnd()) { if (tokenMatcher.find()) { tokens.add(tokenMatcher.group()); } else { // report error break; } } System.out.println(tokens); } } 

Exemplo de saída:

 [58.5, +, variable, -, +, 98, *, 78, /, 96, +, a, /, 78.7, -, 3443, *, 12, -, 3] 

Resposta rápida: use limites não físicos como \ b para dividir. Vou tentar experimentar para ver se funciona (usei isso em PHP e JS).

É possível, e tipo de trabalho, mas pode dividir muito. Na verdade, depende da string que você deseja dividir e do resultado que você precisa. Dê mais detalhes, vamos ajudá-lo melhor.

Outra maneira é fazer sua própria divisão, capturando o delimitador (supondo que seja variável) e adicionando-o posteriormente ao resultado.

Meu teste rápido:

 String str = "'ab','cd','eg'"; String[] stra = str.split("\\b"); for (String s : stra) System.out.print(s + "|"); System.out.println(); 

Resultado:

 '|ab|','|cd|','|eg|'| 

Um pouco demais … 🙂

Tweaked Pattern.split () para include o padrão correspondente na lista

Adicionado

 // add match to the list matchList.add(input.subSequence(start, end).toString()); 

Fonte completa

 public static String[] inclusiveSplit(String input, String re, int limit) { int index = 0; boolean matchLimited = limit > 0; ArrayList matchList = new ArrayList(); Pattern pattern = Pattern.compile(re); Matcher m = pattern.matcher(input); // Add segments before each match found while (m.find()) { int end = m.end(); if (!matchLimited || matchList.size() < limit - 1) { int start = m.start(); String match = input.subSequence(index, start).toString(); matchList.add(match); // add match to the list matchList.add(input.subSequence(start, end).toString()); index = end; } else if (matchList.size() == limit - 1) { // last one String match = input.subSequence(index, input.length()) .toString(); matchList.add(match); index = end; } } // If no match was found, return this if (index == 0) return new String[] { input.toString() }; // Add remaining segment if (!matchLimited || matchList.size() < limit) matchList.add(input.subSequence(index, input.length()).toString()); // Construct result int resultSize = matchList.size(); if (limit == 0) while (resultSize > 0 && matchList.get(resultSize - 1).equals("")) resultSize--; String[] result = new String[resultSize]; return matchList.subList(0, resultSize).toArray(result); } 

Aqui está uma versão groovy baseada em alguns dos códigos acima, caso isso ajude. É curto, de qualquer maneira. Condicionalmente inclui a cabeça e cauda (se eles não estão vazios). A última parte é um caso de demonstração / teste.

 List splitWithTokens(str, pat) { def tokens=[] def lastMatch=0 def m = str=~pat while (m.find()) { if (m.start() > 0) tokens << str[lastMatch..this is the title',/<[^>]+>/], ['beforethis is the titleafter',/<[^>]+>/] ].each { println splitWithTokens(*it) } 

Uma solução extremamente ingênua e ineficiente que funciona mesmo assim. Use a divisão duas vezes na string e concatene as duas matrizes

 String temp[]=str.split("\\W"); String temp2[]=str.split("\\w||\\s"); int i=0; for(String string:temp) System.out.println(string); String temp3[]=new String[temp.length-1]; for(String string:temp2) { System.out.println(string); if((string.equals("")!=true)&&(string.equals("\\s")!=true)) { temp3[i]=string; i++; } // System.out.println(temp.length); // System.out.println(temp2.length); } System.out.println(temp3.length); String[] temp4=new String[temp.length+temp3.length]; int j=0; for(i=0;i 
  String expression = "((A+B)*CD)*E"; expression = expression.replaceAll("\\+", "~+~"); expression = expression.replaceAll("\\*", "~*~"); expression = expression.replaceAll("-", "~-~"); expression = expression.replaceAll("/+", "~/~"); expression = expression.replaceAll("\\(", "~(~"); //also you can use [(] instead of \\( expression = expression.replaceAll("\\)", "~)~"); //also you can use [)] instead of \\) expression = expression.replaceAll("~~", "~"); if(expression.startsWith("~")) { expression = expression.substring(1); } String[] expressionArray = expression.split("~"); System.out.println(Arrays.toString(expressionArray)); 

Se você está preocupado com as complicações que o look-ahead / look-behind pode introduzir, e apenas quer um método utilitário sólido que possa lidar com qualquer padrão de token e quaisquer separadores que você jogue nele. (Qual é provavelmente o caso!)

NB surpreso ao descobrir que as pessoas Apache Commons não parecem ter fornecido isso, por exemplo, em StringUtils .

Também sugiro que isso seja um sinalizador em Pattern : i..e INCLUDE_SEPARATORS .

Mas isso é bem simples se você usar as classs Pattern e Matcher corretamente:

  // NB could be a different spec for identifying tokens, of course! Pattern sepAndTokenPattern = Pattern.compile("(.*?)(\\w+)"); Matcher matcher = sepAndTokenPattern.matcher( stringForTokenising ); List tokenAndSeparatorList = new ArrayList(); // for most processing purposes you are going to want to know whether your // combined list of tokens and separators begins with a token or separator boolean startsWithToken = true; int matchEnd = -1; while (matcher.find()) { String preSep = matcher.group(1); if (!preSep.isEmpty()) { if( tokenAndSeparatorList.isEmpty() ){ startsWithToken = false; } // in implementation you wouldn't want these | characters, of course tokenAndSeparatorList.add("|" + preSep + "|"); // add sep } tokenAndSeparatorList.add("|" + matcher.group(2) + "|"); // add token matchEnd = matcher.end(); } // get trailing separator, if there is one: if( matchEnd != -1 ){ String trailingSep = stringForTokenising.substring( matchEnd ); if( ! trailingSep.isEmpty() ){ tokenAndSeparatorList.add( "|" + trailingSep + "|" ); } } System.out.println(String.format("# starts with token? %b - matchList %s", startsWithToken, tokenAndSeparatorList)); 

I don’t know Java too well, but if you can’t find a Split method that does that, I suggest you just make your own.

 string[] mySplit(string s,string delimiter) { string[] result = s.Split(delimiter); for(int i=0;i 

Its not too elegant, but it'll do.