Como dividir uma corda separada por vírgula enquanto ignora vírgulas escapadas?
Pergunta
Preciso escrever uma versão estendida da função StringUtils.CommadeLimitedListTToStringArray, que obtém um parâmetro adicional: o char Escape.
Então, chamando meu:
commaDelimitedListToStringArray("test,test\\,test\\,test,test", "\\")
Deve voltar:
["test", "test,test,test", "test"]
Minha tentativa atual é usar string.split () para dividir a string usando expressões regulares:
String[] array = str.split("[^\\\\],");
Mas a matriz devolvida é:
["tes", "test\,test\,tes", "test"]
Alguma ideia?
Solução
A expressão regular
[^\\],
significa "corresponder a um personagem que não é uma barra de barriga seguida por uma vírgula" - é por isso que padrões como t,
são correspondentes, porque t
é um personagem que não é uma barra de barriga.
Eu acho que você precisa usar algum tipo de LookBehind negativo, para capturar um ,
que não é precedido por um \
sem capturar o personagem anterior, algo como
(?<!\\),
(Btw, observe que eu propositalmente não escapou duplamente as barras de barriga para tornar isso mais legível)
Outras dicas
Tentar:
String array[] = str.split("(?<!\\\\),");
Basicamente, isso está dizendo dividido em uma vírgula, exceto onde essa vírgula é precedida por duas barras de barriga. Isso é chamado de Afirmação negativa da largura da largura.
Para referência futura, aqui está o método completo com o qual acabei com:
public static String[] commaDelimitedListToStringArray(String str, String escapeChar) {
// these characters need to be escaped in a regular expression
String regularExpressionSpecialChars = "/.*+?|()[]{}\\";
String escapedEscapeChar = escapeChar;
// if the escape char for our comma separated list needs to be escaped
// for the regular expression, escape it using the \ char
if(regularExpressionSpecialChars.indexOf(escapeChar) != -1)
escapedEscapeChar = "\\" + escapeChar;
// see http://stackoverflow.com/questions/820172/how-to-split-a-comma-separated-string-while-ignoring-escaped-commas
String[] temp = str.split("(?<!" + escapedEscapeChar + "),", -1);
// remove the escapeChar for the end result
String[] result = new String[temp.length];
for(int i=0; i<temp.length; i++) {
result[i] = temp[i].replaceAll(escapedEscapeChar + ",", ",");
}
return result;
}
Como Matt B disse, [^\\],
interpretará o caráter que precede a vírgula como parte do delimitador.
"test\\\\\\,test\\\\,test\\,test,test"
-(split)->
["test\\\\\\,test\\\\,test\\,tes" , "test"]
Como Drvdijk disse, (?<!\\),
irá interpretar mal as barras de barriga escapar.
"test\\\\\\,test\\\\,test\\,test,test"
-(split)->
["test\\\\\\,test\\\\,test\\,test" , "test"]
-(unescape commas)->
["test\\\\,test\\,test,test" , "test"]
Eu esperaria poder escapar de barras de barriga também ...
"test\\\\\\,test\\\\,test\\,test,test"
-(split)->
["test\\\\\\,test\\\\" , "test\\,test" , "test"]
-(unescape commas and backslashes)->
["test\\,test\\" , "test,test" , "test"]
drvdijk sugeriu (?<=(?<!\\\\)(\\\\\\\\){0,100}),
O que funciona bem para listas com elementos terminando com até 100 barragens. Isso é suficiente ... mas por que um limite? Existe uma maneira mais eficiente (não parece gananciosa)? E as cordas inválidas?
Eu procurei um tempo para uma solução genérica, então escrevi a coisa ... A idéia é dividir -se de seguir um padrão que corresponde aos elementos da lista (em vez de combinar o delimitador).
Minha resposta não toma o personagem de fuga como um parâmetro.
public static List<String> commaDelimitedListStringToStringList(String list) {
// Check the validity of the list
// ex: "te\\st" is not valid, backslash should be escaped
if (!list.matches("^(([^\\\\,]|\\\\,|\\\\\\\\)*(,|$))+")) {
// Could also raise an exception
return null;
}
// Matcher for the list elements
Matcher matcher = Pattern
.compile("(?<=(^|,))([^\\\\,]|\\\\,|\\\\\\\\)*(?=(,|$))")
.matcher(list);
ArrayList<String> result = new ArrayList<String>();
while (matcher.find()) {
// Unescape the list element
result.add(matcher.group().replaceAll("\\\\([\\\\,])", "$1"));
}
return result;
}
Descrição para o padrão (inquilibrado):
(?<=(^|,))
a frente é o início da string ou um ,
([^\\,]|\\,|\\\\)*
o elemento composto de \,
, \\
ou personagens que não são nenhum \
nem ,
(?=(,|$))
atrás está o final da corda ou um ,
O padrão pode ser simplificado.
Mesmo com as 3 analingas (matches
+ find
+ replaceAll
), esse método parece mais rápido que o sugerido por drvdijk. Ainda pode ser otimizado escrevendo um analisador específico.
Além disso, qual é a necessidade de ter um personagem de fuga se apenas um personagem for especial, pode simplesmente ser dobrado ...
public static List<String> commaDelimitedListStringToStringList2(String list) {
if (!list.matches("^(([^,]|,,)*(,|$))+")) {
return null;
}
Matcher matcher = Pattern.compile("(?<=(^|,))([^,]|,,)*(?=(,|$))")
.matcher(list);
ArrayList<String> result = new ArrayList<String>();
while (matcher.find()) {
result.add(matcher.group().replaceAll(",,", ","));
}
return result;
}