Java: fractionnement d'une chaîne séparée par des virgules mais en ignorant les virgules entre guillemets


249

J'ai une chaîne 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? Semble comme une approche regexp échoue; Je suppose que je peux numériser manuellement et entrer dans un mode différent lorsque je vois un devis, mais ce serait bien d'utiliser des bibliothèques préexistantes. ( modifier : je suppose que je voulais dire des bibliothèques qui font déjà partie du JDK ou qui font déjà partie d'une bibliothèque couramment utilisée comme Apache Commons.)

la chaîne ci-dessus doit se diviser en:

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

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

Réponses:


435

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

Production:

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

En d'autres termes: divisez 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) "+ // 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.

ÉDITER

Comme mentionné par @MikeFHay dans les commentaires:

Je préfère utiliser le séparateur de Guava , car il a des valeurs par défaut plus saines (voir la discussion ci-dessus à propos des correspondances vides qui sont coupées par String#split(), donc je l'ai fait:

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

Selon RFC 4180: Sec 2.6: "Les champs contenant des sauts de ligne (CRLF), des guillemets doubles et des virgules doivent être placés entre guillemets doubles." Sec 2.7: "Si des guillemets doubles sont utilisés pour entourer des 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 = "equals: =,\"quote: \"\"\",\"comma: ,\"", tout ce que vous avez à faire est de supprimer le guillemet double superflu personnages.
Paul Hanbury

@Bart: je veux dire que votre solution fonctionne toujours, même avec des citations intégrées
Paul Hanbury

6
@ Alex, oui, la virgule est adapté, mais le match vide est pas dans le résultat. Ajouter -1à la méthode split param: line.split(regex, -1). Voir: docs.oracle.com/javase/6/docs/api/java/lang/…
Bart Kiers

2
Fonctionne très bien! Je préfère utiliser le séparateur de Guava, car il a des valeurs par défaut plus saines (voir la discussion ci-dessus sur les correspondances vides coupées par String # split), donc je l'ai fait Splitter.on(Pattern.compile(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)")).
MikeFHay

2
AVERTISSEMENT!!!! Cette expression régulière est lente !!! Il a un comportement O (N ^ 2) en ce que l'anticipation de chaque virgule regarde jusqu'à la fin de la chaîne. L'utilisation de cette expression régulière a provoqué un ralentissement de 4x dans les gros travaux Spark (par exemple 45 minutes -> 3 heures). L'alternative la plus rapide est quelque chose comme findAllIn("(?s)(?:\".*?\"|[^\",]*)*")en combinaison avec une étape de post-traitement pour ignorer le premier champ (toujours vide) après chaque champ non vide.
Urban Vagabond du

46

Bien que j'aime les expressions régulières en général, pour ce type de tokenisation dépendant de l'état, je crois qu'un simple analyseur (qui dans ce cas est beaucoup plus simple que ce mot pourrait le faire entendre) 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 à l'intérieur des guillemets, vous pouvez simplifier cette approche (pas de gestion de l'index de démarrage, pas de casse de dernier caractère ) en remplaçant vos virgules entre guillemets par autre chose, puis divisées en 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(","));

Les citations doivent être supprimées des jetons analysés, une fois la chaîne analysée.
Sudhir N

Trouvé via google, joli algorithme bro, simple et facile à adapter, d'accord. les choses avec état doivent être faites via l'analyseur, l'expression régulière est un gâchis.
Rudolf Schmidt

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

21

3
Bon appel reconnaissant que l'OP analysait un fichier CSV. Une bibliothèque externe est extrêmement appropriée pour cette tâche.
Stefan Kendall

1
Mais la chaîne est une chaîne CSV; vous devriez pouvoir utiliser une API CSV directement sur cette chaîne.
Michael Brewer-Davis,

oui, mais cette tâche est assez simple, et une partie beaucoup plus petite d'une plus grande application, que je n'ai pas envie de tirer dans une autre bibliothèque externe.
Jason S

7
pas nécessairement ... mes compétences sont souvent adéquates, mais elles ont tout intérêt à être perfectionnées.
Jason S

9

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é une solution regex et une implémentation d'analyse propre, j'ai constaté que:

  1. L'analyse est beaucoup plus rapide que le fractionnement avec l'expression régulière avec des références arrières - ~ 20 fois plus rapide pour les chaînes courtes, ~ 40 fois plus rapide pour les chaînes longues.
  2. Regex ne parvient pas à trouver une chaîne vide après la dernière virgule. Ce n'était pas dans la question d'origine cependant, c'était la 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 de commutateur pour les autres éléments de cet extrait si vous vous sentez mal à l'aise avec sa laideur. A noter alors absence de rupture après passage avec séparateur. StringBuilder a été choisi à la place de StringBuffer par conception pour augmenter la vitesse, là où la sécurité des threads n'est pas pertinente.


2
Point intéressant concernant le fractionnement du temps vs l'analyse. Cependant, l'énoncé # 2 est inexact. Si vous ajoutez un -1à la méthode de fractionnement dans la réponse de Bart, vous attraperez des chaînes vides (y compris des chaînes vides après la dernière virgule):line.split(regex, -1)
Peter

+1 parce que c'est une meilleure solution au problème pour lequel je cherchais une solution: analyser une chaîne de paramètres de corps HTTP POST complexe
varontron

2

Essayez un lookaround comme (?!\"),(?!\"). Cela devrait correspondre à ceux ,qui ne sont pas entourés ".


Je suis sûr que ça casserait pour une liste comme: "foo", bar, "baz"
Angelo Genovese

1
Je pense que tu voulais dire (?<!"),(?!"), mais ça ne marchera toujours pas. Étant donné la chaîne one,two,"three,four", elle correspond correctement à la virgule one,two, mais elle correspond également à la virgule "three,four"et ne parvient pas à en faire correspondre une two,"three.
Alan Moore

Cela semble fonctionner parfaitement pour moi, à mon humble avis, je pense que c'est une meilleure réponse car elle est plus courte et plus facilement compréhensible
Ordiel

2

Vous êtes dans cette zone de frontière ennuyeuse où les expressions rationnelles ne feront presque pas l'affaire (comme l'a souligné Bart, échapper aux citations rendrait la vie difficile), et pourtant un analyseur à part entière semble exagéré.

Si vous êtes susceptible d'avoir besoin d'une plus grande complexité de sitôt, j'irais à la recherche d'une bibliothèque d'analyseur. Par exemple celui-ci


2

J'étais impatient et j'ai choisi de ne pas attendre les réponses ... pour référence, il ne semble pas si difficile de faire quelque chose comme ça (qui fonctionne pour mon application, je n'ai pas besoin de m'inquiéter des citations échappées, comme les choses 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: étendez la gestion des citations échappées en recherchant également des barres obliques inverses.)


1

L'approche la plus simple n'est pas de faire correspondre les délimiteurs, c'est-à-dire les virgules, avec une logique supplémentaire complexe pour faire correspondre ce qui est réellement prévu (les données qui pourraient être des chaînes de caractères), juste pour exclure les faux délimiteurs, mais plutôt faire correspondre les données prévues en premier lieu.

Le modèle se compose de deux alternatives, une chaîne entre guillemets ( "[^"]*"ou ".*?") ou tout jusqu'à la prochaine virgule ( [^,]+). Pour prendre en charge les cellules vides, nous devons autoriser l'élément non cité à être vide et consommer la virgule suivante, le cas échéant, et utiliser l' \\Gancre:

Pattern p = Pattern.compile("\\G\"(.*?)\",?|([^,]*),?");

Le modèle contient également deux groupes de capture pour obtenir soit le contenu de la chaîne citée, soit le contenu brut.

Ensuite, avec Java 9, nous pouvons obtenir un tableau comme

String[] a = p.matcher(input).results()
    .map(m -> m.group(m.start(1)<0? 2: 1))
    .toArray(String[]::new);

alors que les anciennes versions de Java ont besoin d'une boucle comme

for(Matcher m = p.matcher(input); m.find(); ) {
    String token = m.group(m.start(1)<0? 2: 1);
    System.out.println("found: "+token);
}

L'ajout des éléments à un Listou à un tableau est laissé comme accise au lecteur.

Pour Java 8, vous pouvez utiliser l' results()implémentation de cette réponse , pour le faire comme la solution Java 9.

Pour un contenu mixte avec des chaînes intégrées, comme dans la question, vous pouvez simplement utiliser

Pattern p = Pattern.compile("\\G((\"(.*?)\"|[^,])*),?");

Mais ensuite, les chaînes sont conservées dans leur forme entre guillemets.


0

Plutôt que d'utiliser l'anticipation et d'autres expressions rationnelles folles, retirez d'abord les guillemets. Autrement dit, pour chaque groupe de devis, remplacez ce groupe par __IDENTIFIER_1ou un autre indicateur et mappez ce groupe à une carte de chaîne, chaîne.

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


et comment trouver des regroupements de devis sans regexS fou?
Kai Huppmann

Pour chaque caractère, si le caractère est une citation, recherchez la citation suivante et remplacez-la par un regroupement. Si pas de prochain devis, c'est fait.
Stefan Kendall

0

que diriez-vous d'un one-liner en utilisant String.split ()?

String s = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\"";
String[] split = s.split( "(?<!\".{0,255}[^\"]),|,(?![^\"].*\")" );

-1

Je ferais quelque chose comme ça:

boolean foundQuote = false;

if(charAtIndex(currentStringIndex) == '"')
{
   foundQuote = true;
}

if(foundQuote == true)
{
   //do nothing
}

else 

{
  string[] split = currentString.split(',');  
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.