Java: séparer une chaîne de caractères séparée par des virgules mais ignorer les virgules dans les guillemets

j'ai une chaîne vaguement comme celle-ci:

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 regexp échoue; je suppose que je peux scanner manuellement et entrer dans un mode différent quand je vois un devis, mais il serait bien d'utiliser des bibliothèques préexistantes. ( edit : je suppose que je voulais dire les bibliothèques qui font déjà partie du JDK ou qui font déjà partie d'une bibliothèque couramment utilisée comme Apache Commun.)

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 simple contenue dans un fichier avec une structure globale plus grande

212
demandé sur Jason S 2009-11-18 19:04:58

9 réponses

, Essayez:

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: divisé sur la virgule Seulement si cette virgule A zéro, ou un nombre pair de citations 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.

EDIT

comme mentionné par @MikeFHay dans les commentaires:

je préfère utiliser Guava's Splitter , car il a saner par défaut (voir la discussion ci-dessus sur les allumettes vides étant paré par String#split() , donc j'ai fait:

Splitter.on(Pattern.compile(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"))
386
répondu Bart Kiers 2016-11-26 05:24:32

bien que j'aime les expressions régulières en général, pour ce type de tokenization dépendante de l'état je crois qu'un simple parser (qui dans ce cas est beaucoup plus simple que ce mot pourrait le rendre sonore) 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 manipulation de l'index de départ, pas de dernier caractère cas spécial) en remplaçant vos guillemets entre guillemets par quelque chose d'autre, puis en fractionnant les guillemets:

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(","));
40
répondu Fabian Steeg 2010-01-22 21:35:59

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

  1. l'Analyse est beaucoup plus rapide que le fractionnement avec la regex 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 trouve pas la chaîne vide après la dernière virgule. Ce n'était pas dans l'original question cependant, c'était mon exigence.

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 passer à else-ifs dans cet extrait si vous vous sentez mal à l'aise avec sa laideur. Noter l'absence de pause après l'interrupteur avec le séparateur. StringBuilder a été choisi à la place de StringBuffer par conception pour augmenter la vitesse, où la sécurité de fil est sans importance.

5
répondu Marcin Kosinski 2014-06-06 09:08:30

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

2
répondu Matthew Sowders 2009-11-18 16:14:33

vous êtes dans cette zone de limite ennuyeuse où regexps ne fera presque pas (comme L'a souligné Bart, échapper aux citations rendrait la vie difficile) , et pourtant un analyseur complet semble être exagéré.

si vous êtes susceptible d'avoir besoin d'une plus grande complexité à tout moment bientôt, je vais chercher une bibliothèque parser. Par exemple celui-ci

2
répondu djna 2009-11-18 16:15:31

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 (ce qui fonctionne pour mon application, je n'ai pas besoin de me soucier des citations échappées, car la substance dans les citations est limitée à 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 manipulation échappé à des citations à la recherche pour les barres obliques inverses.)

2
répondu Jason S 2009-11-18 16:47:49

plutôt que d'utiliser lookahead et d'autres farfelus regex, il suffit de sortir les guillemets en premier. C'est-à-dire, pour chaque groupement de citations, remplacer ce groupement par __IDENTIFIER_1 ou un autre indicateur,et faire correspondre ce groupement à une carte de chaîne, chaîne.

après avoir divisé en virgule, remplacez tous les identificateurs mappés par les valeurs originales de la chaîne de caractères.

0
répondu Stefan Kendall 2009-11-18 16:13:31

je ferais quelque chose comme ça:

boolean foundQuote = false;

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

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

else 

{
  string[] split = currentString.split(',');  
}
-1
répondu Woot4Moo 2011-11-29 20:23:37