web-dev-qa-db-fra.com

Comment diviser une chaîne séparée par des virgules tout en ignorant les virgules échappées?

J'ai besoin d'écrire une version étendue de la fonction StringUtils.commaDelimitedListToStringArray qui obtient un paramètre supplémentaire: le caractère d'échappement.

appelant ainsi mon:

commaDelimitedListToStringArray("test,test\\,test\\,test,test", "\\")

devrait retourner:

["test", "test,test,test", "test"]



Ma tentative actuelle consiste à utiliser String.split () pour diviser la chaîne à l'aide d'expressions régulières:

String[] array = str.split("[^\\\\],");

Mais le tableau retourné est:

["tes", "test\,test\,tes", "test"]

Des idées?

26
arturh

L'expression régulière

[^\\],

signifie "faire correspondre un caractère qui n'est pas une barre oblique inverse suivie d'une virgule" - c'est pourquoi des modèles tels que t, correspondent, car t est un caractère qui n'est pas une barre oblique inverse.

Je pense que vous devez utiliser une sorte de lookbehind négatif , pour capturer un , qui n'est pas précédé d'un \ sans capturer le caractère précédent, quelque chose comme

(?<!\\),

(BTW, notez que je n'ai pas délibérément échappé doublement aux barres obliques inverses pour le rendre plus lisible)

32
matt b

Essayer:

String array[] = str.split("(?<!\\\\),");

Fondamentalement, cela signifie fractionner sur une virgule, sauf lorsque cette virgule est précédée de deux barres obliques inverses. C'est ce qu'on appelle un lookbehind négatif assertion de largeur nulle .

30
cletus

Pour référence future, voici la méthode complète avec laquelle je me suis retrouvé:

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;
}
6
arturh

Comme Matt B l'a dit, [^\\], interprétera le caractère précédant la virgule comme faisant partie du délimiteur.

"test\\\\\\,test\\\\,test\\,test,test"
  -(split)->
["test\\\\\\,test\\\\,test\\,tes" , "test"]

Comme l'a dit drvdijk, (?<!\\), interprètera mal les barres obliques inversées.

"test\\\\\\,test\\\\,test\\,test,test"
  -(split)->
["test\\\\\\,test\\\\,test\\,test" , "test"]
  -(unescape commas)->
["test\\\\,test\\,test,test" , "test"]

Je m'attendrais à pouvoir également échapper aux barres obliques inverses ...

"test\\\\\\,test\\\\,test\\,test,test"
  -(split)->
["test\\\\\\,test\\\\" , "test\\,test" , "test"]
  -(unescape commas and backslashes)->
["test\\,test\\" , "test,test" , "test"]

drvdijk a suggéré (?<=(?<!\\\\)(\\\\\\\\){0,100}), qui fonctionne bien pour les listes avec des éléments se terminant par jusqu'à 100 barres obliques inverses. C'est assez loin ... mais pourquoi une limite? Existe-t-il un moyen plus efficace (ne regarde pas derrière gourmand)? Qu'en est-il des chaînes invalides?

J'ai cherché pendant un certain temps une solution générique, puis j'ai écrit la chose moi-même ... L'idée est de diviser en suivant un modèle qui correspond aux éléments de la liste (au lieu de faire correspondre le délimiteur).

Ma réponse ne prend pas le caractère d'échappement comme paramètre.

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;
}

Description du motif (sans échappement):

(?<=(^|,)) forward est le début d'une chaîne ou un ,

([^\\,]|\\,|\\\\)* l'élément composé de \,, \\ ou des caractères qui ne sont ni \ ni ,

(?=(,|$)) derrière est la fin d'une chaîne ou un ,

Le modèle peut être simplifié.

Même avec les 3 analyses (matches + find + replaceAll), cette méthode semble plus rapide que celle suggérée par drvdijk. Il peut toujours être optimisé en écrivant un analyseur spécifique.

Aussi, quel est le besoin d'avoir un personnage d'échappement si un seul personnage est spécial, il pourrait simplement être doublé ...

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;
}
2
boumbh