web-dev-qa-db-fra.com

Java: fractionner une chaîne séparée par des virgules mais ignorer les virgules entre guillemets

J'ai une corde vaguement comme ça:

foo,bar,c;qual="baz,blurb",d;junk="quux,syzygy"

que je veux diviser par des virgules - mais je dois ignorer les virgules entre guillemets. Comment puis-je faire ceci? On dirait qu'une approche d'expression rationnelle échoue; Je suppose que je peux scanner manuellement et entrer dans un mode différent quand je vois une citation, mais il serait bien d’utiliser des bibliothèques préexistantes. ( edit : Je suppose que je voulais dire des bibliothèques qui font déjà partie du JDK ou des bibliothèques couramment utilisées comme Apache Commons.)

la chaîne ci-dessus doit être divisée en:

foo
bar
c;qual="baz,blurb"
d;junk="quux,syzygy"

note: ceci n'est PAS un fichier CSV, c'est une chaîne unique contenue dans un fichier avec une structure globale plus grande

230
Jason S

Essayer:

public class Main { 
    public static void main(String[] args) {
        String line = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\"";
        String[] tokens = line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1);
        for(String t : tokens) {
            System.out.println("> "+t);
        }
    }
}

Sortie:

> foo
> bar
> c;qual="baz,blurb"
> d;junk="quux,syzygy"

En d'autres termes: ne se sépare sur la virgule que si cette virgule a zéro, ou un nombre pair de guillemets devant elle .

Ou, un peu plus convivial pour les yeux:

public class Main { 
    public static void main(String[] args) {
        String line = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\"";

        String otherThanQuote = " [^\"] ";
        String quotedString = String.format(" \" %s* \" ", otherThanQuote);
        String regex = String.format("(?x) "+ // enable comments, ignore white spaces
                ",                         "+ // match a comma
                "(?=                       "+ // start positive look ahead
                "  (?:                     "+ //   start non-capturing group 1
                "    %s*                   "+ //     match 'otherThanQuote' zero or more times
                "    %s                    "+ //     match 'quotedString'
                "  )*                      "+ //   end group 1 and repeat it zero or more times
                "  %s*                     "+ //   match 'otherThanQuote'
                "  $                       "+ // match the end of the string
                ")                         ", // stop positive look ahead
                otherThanQuote, quotedString, otherThanQuote);

        String[] tokens = line.split(regex, -1);
        for(String t : tokens) {
            System.out.println("> "+t);
        }
    }
}

qui produit le même que le premier exemple.

MODIFIER

Comme mentionné par @MikeFHay dans les commentaires:

Je préfère utiliser Guava's Splitter , car il a des paramètres par défaut plus corrects (voir la discussion ci-dessus sur les correspondances vides coupées par String#split(), donc j'ai:

Splitter.on(Pattern.compile(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"))
408
Bart Kiers

Bien que j'aime les expressions régulières en général, pour ce type de tokenization dépendant de l'état, je crois qu'un simple analyseur syntaxique (qui dans ce cas est beaucoup plus simple que ce que Word pourrait donner l'air) est probablement une solution plus propre, en particulier en ce qui concerne la maintenabilité. , par exemple:

String input = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\"";
List<String> result = new ArrayList<String>();
int start = 0;
boolean inQuotes = false;
for (int current = 0; current < input.length(); current++) {
    if (input.charAt(current) == '\"') inQuotes = !inQuotes; // toggle state
    boolean atLastChar = (current == input.length() - 1);
    if(atLastChar) result.add(input.substring(start));
    else if (input.charAt(current) == ',' && !inQuotes) {
        result.add(input.substring(start, current));
        start = current + 1;
    }
}

Si vous ne vous souciez pas de conserver les virgules dans les guillemets, vous pouvez simplifier cette approche (pas de traitement de l'index de départ, pas de dernier caractère cas particulier) en remplaçant vos virgules entre guillemets par quelque chose d'autre, puis en les séparant. à des virgules:

String input = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\"";
StringBuilder builder = new StringBuilder(input);
boolean inQuotes = false;
for (int currentIndex = 0; currentIndex < builder.length(); currentIndex++) {
    char currentChar = builder.charAt(currentIndex);
    if (currentChar == '\"') inQuotes = !inQuotes; // toggle state
    if (currentChar == ',' && inQuotes) {
        builder.setCharAt(currentIndex, ';'); // or '♡', and replace later
    }
}
List<String> result = Arrays.asList(builder.toString().split(","));
43
Fabian Steeg
21
Jonathan Feinberg

Je ne conseillerais pas une réponse rationnelle de Bart, je trouve que la solution d'analyse syntaxique est meilleure dans ce cas particulier (comme l'a proposé Fabian). J'ai essayé la solution regex et sa propre implémentation d'analyse syntaxique, j'ai constaté que:

  1. L'analyse est beaucoup plus rapide que la scission avec une expression rationnelle avec des références arrières - environ 20 fois plus rapide pour les chaînes courtes, environ 40 fois plus rapide pour les longues chaînes.
  2. Regex ne parvient pas à trouver une chaîne vide après la dernière virgule. Ce n'était pas dans la question initiale cependant, c'était l'exigence mienne.

Ma solution et test ci-dessous.

String tested = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\",";
long start = System.nanoTime();
String[] tokens = tested.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)");
long timeWithSplitting = System.nanoTime() - start;

start = System.nanoTime(); 
List<String> tokensList = new ArrayList<String>();
boolean inQuotes = false;
StringBuilder b = new StringBuilder();
for (char c : tested.toCharArray()) {
    switch (c) {
    case ',':
        if (inQuotes) {
            b.append(c);
        } else {
            tokensList.add(b.toString());
            b = new StringBuilder();
        }
        break;
    case '\"':
        inQuotes = !inQuotes;
    default:
        b.append(c);
    break;
    }
}
tokensList.add(b.toString());
long timeWithParsing = System.nanoTime() - start;

System.out.println(Arrays.toString(tokens));
System.out.println(tokensList.toString());
System.out.printf("Time with splitting:\t%10d\n",timeWithSplitting);
System.out.printf("Time with parsing:\t%10d\n",timeWithParsing);

Bien sûr, vous êtes libre de changer d’option dans cet extrait si vous ne vous sentez pas à l’aise avec sa laideur. Notez ensuite l'absence de pause après l'interrupteur avec séparateur. StringBuilder a été choisi à la place de StringBuffer par conception pour augmenter la vitesse, où la sécurité des threads n'est pas pertinente.

7
Marcin Kosinski

J'étais impatiente et j'ai choisi de ne pas attendre les réponses ... pour référence, il n'a pas l'air si difficile de faire quelque chose comme ça (ce qui fonctionne pour mon application, je n'ai pas besoin de m'inquiéter pour les citations échappées, car ce qui est entre guillemets est limité à quelques formes contraintes):

final static private Pattern splitSearchPattern = Pattern.compile("[\",]"); 
private List<String> splitByCommasNotInQuotes(String s) {
    if (s == null)
        return Collections.emptyList();

    List<String> list = new ArrayList<String>();
    Matcher m = splitSearchPattern.matcher(s);
    int pos = 0;
    boolean quoteMode = false;
    while (m.find())
    {
        String sep = m.group();
        if ("\"".equals(sep))
        {
            quoteMode = !quoteMode;
        }
        else if (!quoteMode && ",".equals(sep))
        {
            int toPos = m.start(); 
            list.add(s.substring(pos, toPos));
            pos = m.end();
        }
    }
    if (pos < s.length())
        list.add(s.substring(pos));
    return list;
}

(Exercice pour le lecteur: étendre la gestion des citations échappées en recherchant également les barres obliques inverses.)

2
Jason S

Essayez un lookaround comme (?!\"),(?!\"). Cela doit correspondre à , qui n'est pas entouré de ".

2
Matthew Sowders

Vous vous trouvez dans cette zone délicate où les expressions rationnelles ne fonctionneront presque plus (comme Bart l'a fait remarquer, échapper aux guillemets rendrait la vie difficile), et pourtant, un analyseur syntaxique complet semble excessif.

Si vous êtes susceptible d'avoir besoin d'une plus grande complexité dans un avenir rapproché, j'irais chercher une bibliothèque d'analyse. Par exemple celui-ci

2
djna

Plutôt que d'utiliser lookahead et d'autres expressions rationnelles loufoques, extrayez simplement les guillemets en premier. Autrement dit, pour chaque groupe de devis, remplacez ce groupe par __IDENTIFIER_1 ou un autre indicateur et mappez ce groupe sur un mappage de chaîne, chaîne.

Une fois que vous avez fractionné la virgule, remplacez tous les identificateurs mappés par les valeurs de chaîne d'origine.

0
Stefan Kendall