Java: virgülle ayrılmış bir dize bölme ancak tırnak işaretleri arasında virgül yoksayma


249

Belli bir dize var böyle:

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

virgülle ayırmak istiyorum - ama tırnak işaretleri içinde virgül yoksaymalıyım. Bunu nasıl yapabilirim? Normal ifade yaklaşımı başarısız gibi görünüyor; Bir teklif gördüğümde manuel olarak tarayabilir ve farklı bir moda girebilirim, ancak önceden var olan kütüphaneleri kullanmak güzel olurdu. ( edit : Sanırım zaten JDK'nın veya zaten Apache Commons gibi yaygın olarak kullanılan kütüphanelerin bir parçası olan kütüphaneler demek istedim.)

yukarıdaki dize:

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

not: bu bir CSV dosyası DEĞİLDİR, daha büyük bir genel yapıya sahip bir dosyada bulunan tek bir dize

Yanıtlar:


435

Deneyin:

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

Çıktı:

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

Başka bir deyişle: virgül üzerinde yalnızca virgülün sıfır veya önünde çift tırnak varsa bölün .

Veya gözler için biraz dostça:

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

ve ilk örnekle aynı şeyi üretir.

DÜZENLE

Yorumlarda @MikeFHay tarafından belirtildiği gibi:

Ben saner varsayılan olarak Guava'nın Splitter kullanmayı tercih (boş maçlar tarafından kesilmiş hakkında tartışmaya bakın String#split(), bu yüzden yaptım:

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

RFC 4180'e göre: Bölüm 2.6: "Satır sonu (CRLF), çift tırnak ve virgül içeren alanlar çift tırnak içine alınmalıdır." Sec 2.7: "Alanları çift tırnak içine almak için kullanılırsa, bir alanın içinde görünen bir çift tırnak bir başka çift tırnak ile önüne geçilmeli" Yani String line = "equals: =,\"quote: \"\"\",\"comma: ,\"", tek yapmanız gereken yabancı çift tırnak karakter.
Paul Hanbury

@Bart: Demek istediğim, gömülü alıntılarla bile çözümünüzün hala çalışıyor olması
Paul Hanbury

6
@Alex, evet, virgül edilir eşleşti, ama boş maç sonucu değildir. Ekle -1bölüşüm yöntemi param için: line.split(regex, -1). Bakınız: docs.oracle.com/javase/6/docs/api/java/lang/…
Bart Kiers

2
Harika çalışıyor! Bu saner varsayılanlar (String # split tarafından kesilmiş boş maçlar hakkında yukarıdaki tartışmaya bakın), bu yüzden yaptım Guava'nın Splitter kullanmayı tercih ederim Splitter.on(Pattern.compile(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)")).
MikeFHay

2
UYARI!!!! Bu normal ifade yavaş !!! O (N ^ 2) davranışı vardır, çünkü her virgüldeki gözetleme dizenin sonuna kadar uzanır. Bu normal ifadeyi kullanmak büyük Spark işlerinde 4x yavaşlamaya neden oldu (örneğin 45 dakika -> 3 saat). Daha hızlı alternatif, findAllIn("(?s)(?:\".*?\"|[^\",]*)*")boş olmayan her alanı takip eden ilk (her zaman boş) alanı atlamak için bir işlem sonrası adımla kombinasyon gibi bir şeydir .
Urban Vagabond

46

Genel olarak düzenli ifadeleri sevmeme rağmen, bu tür devlete bağlı tokenizasyon için basit bir ayrıştırıcının (bu durumda bu kelimenin seslendirebileceğinden çok daha basit) muhtemelen özellikle daha sürdürülebilirlik açısından daha temiz bir çözüm olduğuna inanıyorum. , Örneğin:

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

Virgüllerin tırnak içinde korunmasını önemsemiyorsanız, virgüllerinizi tırnak içinde başka bir şeyle değiştirip virgüllere bölerek bu yaklaşımı basitleştirebilirsiniz (başlangıç ​​indeksinin işlenmesi yok, son karakter özel durumu yok):

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(","));

Tırnaklar, dize ayrıştırıldıktan sonra ayrıştırılmış belirteçlerden kaldırılmalıdır.
Sudhir N

Google ile bulundu, güzel algoritma kardeşim, basit ve kolay adapte, katılıyorum. durumsal çözümleyici ayrıştırıcı aracılığıyla yapılmalıdır, normal ifade karmaşadır.
Rudolf Schmidt

2
Virgül son karakter ise, son öğenin Dize değerinde olacağını unutmayın.
Gabriel Gates

21

3
OP'nin bir CSV dosyasını ayrıştırdığını tanıyan iyi bir çağrı. Harici bir kütüphane bu görev için son derece uygundur.
Stefan Kendall

1
Ancak dize bir CSV dizesidir; doğrudan bu dizede CSV api kullanabilmeniz gerekir.
Michael Brewer-Davis

evet, ama bu görev yeterince basit ve daha büyük bir uygulamanın çok daha küçük bir kısmı, başka bir harici kütüphaneye çekmeyi istemiyorum.
Jason S

7
illa ki ... becerilerim genellikle yeterli, ama honlanmaktan faydalanıyorlar.
Jason S

9

Bart'dan normal bir cevap önermem, bu özel durumda çözümleme çözümünü daha iyi buluyorum (Fabian'ın önerdiği gibi). Ben regex çözüm denedim ve kendi ayrıştırma uygulaması buldum:

  1. Ayrıştırma, backreferences ile regex ile bölünmekten çok daha hızlıdır - kısa dizeler için ~ 20 kat, uzun dizeler için ~ 40 kat daha hızlı.
  2. Normal virgül, son virgülden sonra boş dize bulamıyor. Bu orijinal bir soru değildi, benim gereksinimimdi.

Çözümüm ve testim aşağıda.

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

Eğer çirkinliğinden rahatsız olursanız, elbette bu pasajdaki başkasını değiştirmekte özgürsünüz. Sonra ayırıcı ile geçişten sonra kırılma eksikliği not edin. İplik güvenliğinin önemsiz olduğu hızı artırmak için StringBuilder tasarım yerine StringBuffer olarak seçildi.


2
Ayrıştırma ve zaman ayrıştırma konusunda ilginç bir nokta. Ancak, # 2 ifadesi yanlış. -1Bart'ın cevabındaki bölme yöntemine bir eklerseniz , boş dizeleri (son virgülden sonra boş dizeler dahil) yakalarsınız:line.split(regex, -1)
Peter

+1, çünkü bir çözüm aradığım soruna daha iyi bir çözümdür: karmaşık bir HTTP POST gövde parametresi dizesini ayrıştırma
varontron

2

Şuna benzer bir görünüm deneyin (?!\"),(?!\"). Bu, ,çevrili olmayanlarla eşleşmelidir ".


Bunun "foo", bar, "baz" gibi bir liste için kırılacağından eminiz
Angelo Genovese

1
Sanırım demek istedin (?<!"),(?!"), ama yine de işe yaramayacak. Dize verildiğinde, one,two,"three,four"virgülle doğru şekilde eşleşir one,two, ancak virgülle eşleşir "three,four"ve biriyle eşleşemez two,"three.
Alan Moore

Benim için mükemmel bir şekilde çalışıyor, IMHO Bunun daha kısa ve daha kolay anlaşılabilir olması nedeniyle daha iyi bir cevap olduğunu düşünüyorum
Ordiel

2

Normal ifadelerin neredeyse yapmayacağı sinir bozucu sınır alanındasınız (Bart tarafından işaret edildiği gibi, tırnaklardan kaçmak hayatı zorlaştıracak) ve yine de tam bir ayrıştırıcı aşırıya kaçmış gibi görünüyor.

Yakın zamanda daha fazla karmaşıklığa ihtiyacınız varsa, ayrıştırıcı kitaplığı aramaya giderdim. Örneğin bu


2

Sabırsızdım ve cevapları beklememeyi seçtim ... referans için böyle bir şey yapmak o kadar zor görünmüyor (bu benim uygulama için çalışıyor, tırnak içinde şeyler gibi kaçan tırnaklar hakkında endişelenmenize gerek yok sınırlı sayıda formla sınırlıdır):

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

(okuyucu için egzersiz: ters eğik çizgi de arayarak kaçan tırnak işaretlerini işlemek için genişletin.)


1

En basit yaklaşım, sınırlayıcıları, yani virgülleri, gerçekte amaçlananla (dizgelerden alıntılanabilecek veriler) eşleştirmek için karmaşık bir ek mantıkla eşleştirmek değil, yalnızca yanlış sınırlayıcıları hariç tutmaktır, aksine amaçlanan verileri ilk etapta eşleştirmektir.

Desen iki alternatiften oluşur, tırnak içine alınmış bir dize ( "[^"]*"veya ".*?") veya bir sonraki virgül ( [^,]+) 'e kadar her şey . Boş hücreleri desteklemek için, alıntılanmamış öğenin boş olmasına izin vermeli ve varsa bir sonraki virgül tüketmeli ve \\Gbağlantıyı kullanmalıyız :

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

Model ayrıca, alıntılanan dizenin içeriği veya düz içeriği almak için iki yakalama grubu içerir.

Ardından, Java 9 ile bir dizi olarak

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

eski Java sürümleri ise

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

Öğeleri bir Listveya bir diziye eklemek okuyucuya bir tüketim olarak bırakılır.

Java 8 için, Java 9 çözümü gibi yapmak için bu cevabınresults() uygulanmasını kullanabilirsiniz .

Sorudaki gibi gömülü dizeleri olan karışık içerik için şunu kullanabilirsiniz:

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

Ama sonra, dizeler alıntılanmış formlarında tutulur.


0

İleri ve diğer çılgın normal ifadeleri kullanmak yerine, önce tırnak işaretlerini çekin. Diğer bir deyişle, her bir teklif grubu için, bu gruplamayı __IDENTIFIER_1veya başka bir göstergeyle değiştirin ve bu gruplamayı dize, dize eşlemesiyle eşleyin.

Virgül üzerine bölündükten sonra, eşlenen tüm tanımlayıcıları orijinal dize değerleriyle değiştirin.


ve çılgın regexS olmadan alıntı gruplamaları nasıl bulunur?
Kai Huppmann

Her karakter için, karakter alıntı ise, bir sonraki alıntıyı bulun ve gruplandırma ile değiştirin. Bir sonraki teklif yoksa, bitti.
Stefan Kendall

0

String.split () kullanarak tek satırdan ne haber?

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

-1

Böyle bir şey yapardım:

boolean foundQuote = false;

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

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

else 

{
  string[] split = currentString.split(',');  
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.