270 votes

Java : diviser une chaîne de caractères séparée par des virgules tout en ignorant les virgules entre guillemets

J'ai une chaîne vaguement similaire à ceci :

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 cela ? Il semble qu'une approche avec les regex échoue ; je suppose que je peux analyser manuellement et passer dans un mode différent lorsque je vois des guillemets, mais ce serait bien d'utiliser des bibliothèques préexistantes. (éditer : je suppose que je voulais dire des bibliothèques qui font déjà partie du JDK ou de bibliothèques couramment utilisées comme Apache Commons.)

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

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

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

474voto

Bart Kiers Points 79069

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 : séparez la virgule uniquement 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) "+ // activer les commentaires, ignorer les espaces blancs
                ",                         "+ // correspondre à une virgule
                "(?=                       "+ // commencer une recherche positive en avant
                "  (?:                     "+ //   démarrer le groupe non-capturant 1
                "    %s*                   "+ //     correspondre à 'otherThanQuote' zéro ou plusieurs fois
                "    %s                    "+ //     correspondre à 'quotedString'
                "  )*                      "+ //   fin du groupe 1 et le répéter zéro ou plusieurs fois
                "  %s*                     "+ //   correspondre à 'otherThanQuote'
                "  $                       "+ // correspondre à la fin de la chaîne
                ")                         ", // arrêter la recherche positive en avant
                otherThanQuote, quotedString, otherThanQuote);

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

qui produit le même résultat que le premier exemple.

ÉDITER

Comme mentionné par @MikeFHay dans les commentaires :

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

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

1 votes

Selon la RFC 4180: Sec 2.6: "Les champs contenant des sauts de ligne (CRLF), des guillemets doubles et des virgules doivent être enclos dans des guillemets doubles." Sec 2.7: "Si des guillemets doubles sont utilisés pour encadrer les champs, alors un guillemet double apparaissant à l'intérieur d'un champ doit être échappé en le précédant d'un autre guillemet double" Donc, si String line = "égal: =, \"citation: \"\", \",\"", tout ce que vous avez à faire est de supprimer les caractères de guillemets doubles superflus.

0 votes

@Bart: mon point est que ta solution fonctionne toujours, même avec des citations intégrées

0 votes

@Bart Kiers: Il semble que cela échoue lorsque vous avez une virgule à l'intérieur de la valeur de la chaîne : par exemple "op","ID","script","Mike,s","Content-Length"

52voto

Fabian Steeg Points 24261

Alors que j'aime en général les expressions régulières, pour ce type de tokenisation dépendante de l'état, je crois qu'un simple analyseur (qui dans ce cas est beaucoup plus simple que ce que ce mot pourrait laisser penser) 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 result = new ArrayList();
int start = 0;
boolean inQuotes = false;
for (int current = 0; current < input.length(); current++) {
    if (input.charAt(current) == '\"') inQuotes = !inQuotes; // basculer l'état
    else if (input.charAt(current) == ',' && !inQuotes) {
        result.add(input.substring(start, current));
        start = current + 1;
    }
}
result.add(input.substring(start));

Si vous ne vous souciez pas de conserver les virgules à l'intérieur des guillemets, vous pourriez simplifier cette approche (pas de gestion de l'index de début, pas de cas spécial du dernier caractère) en remplaçant les virgules entre guillemets par autre chose puis en séparant au niveau 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; // basculer l'état
    if (currentChar == ',' && inQuotes) {
        builder.setCharAt(currentIndex, ';'); // ou '♡', et remplacer plus tard
    }
}
List result = Arrays.asList(builder.toString().split(","));

0 votes

Les guillemets doivent être retirés des jetons analysés, après l'analyse de la chaîne de caractères.

0 votes

Trouvé via google, bel algorithme mec, simple et facile à adapter, d'accord. Les choses étatiques devraient être faites via le parseur, les expressions régulières sont un gâchis.

2 votes

Gardez à l'esprit que si une virgule est le dernier caractère, elle sera dans la valeur de chaîne du dernier élément.

21voto

Jonathan Feinberg Points 24791

3 votes

Bonne décision de reconnaître que l'OP analysait un fichier CSV. Une bibliothèque externe est extrêmement appropriée pour cette tâche.

1 votes

Mais la chaîne est une chaîne CSV ; vous devriez être en mesure d'utiliser une API CSV sur cette chaîne directement.

0 votes

Oui, mais cette tâche est assez simple et constitue une partie beaucoup plus petite d'une application plus importante, donc je n'ai pas envie de faire appel à une autre bibliothèque externe.

13voto

Marcin Kosinski Points 26

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

  1. L'analyse est beaucoup plus rapide que le fractionnement avec regex avec des références arrière - environ 20 fois plus rapide pour les courtes chaînes, environ 40 fois plus rapide pour les longues chaînes.
  2. Regex échoue à trouver une chaîne vide après la dernière virgule. Ce n'était cependant pas dans la question initiale, c'était ma requête.

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 tokensList = new ArrayList();
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 le switch en else-ifs dans cet extrait si vous vous sentez mal à l'aise avec sa laideur. Notez alors l'absence de break après le switch avec le séparateur. StringBuilder a été choisi à la place de StringBuffer par conception pour augmenter la vitesse, où la sécurité des threads est sans importance.

2 votes

Point intéressant concernant la division du temps par rapport à l'analyse. Cependant, l'affirmation n°2 est inexacte. Si vous ajoutez un -1 à la méthode de division dans la réponse de Bart, vous attraperez les chaînes vides (y compris les chaînes vides après la dernière virgule) : line.split(regex, -1)

0 votes

+1 car c'est une meilleure solution au problème pour lequel je cherchais une solution : l'analyse d'une chaîne de paramètres de corps de message POST HTTP complexe

2voto

djna Points 34761

Vous êtes dans cette zone limite ennuyante où les expressions régulières ne fonctionneront presque pas (comme l'a souligné Bart, échapper aux guillemets rendrait la vie difficile), et pourtant un analyseur complet semble excessif.

Si vous êtes susceptible d'avoir besoin d'une plus grande complexité dans un avenir proche, je vous recommanderais de chercher une bibliothèque d'analyseurs. Par exemple celui-ci

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X