Java: tách một chuỗi được phân tách bằng dấu phẩy nhưng bỏ qua dấu phẩy trong dấu ngoặc kép


249

Tôi có một chuỗi mơ hồ như thế này:

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

rằng tôi muốn phân chia bằng dấu phẩy - nhưng tôi cần bỏ qua dấu phẩy trong dấu ngoặc kép. Tôi có thể làm cái này như thế nào? Có vẻ như một cách tiếp cận regrec thất bại; Tôi cho rằng tôi có thể tự quét và nhập một chế độ khác khi tôi thấy một trích dẫn, nhưng sẽ rất tuyệt nếu sử dụng các thư viện có sẵn. ( chỉnh sửa : Tôi đoán ý tôi là các thư viện đã là một phần của JDK hoặc đã là một phần của các thư viện thường được sử dụng như Apache Commons.)

chuỗi trên nên chia thành:

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

lưu ý: đây KHÔNG phải là tệp CSV, nó là một chuỗi chứa trong tệp có cấu trúc tổng thể lớn hơn

Câu trả lời:


435

Thử:

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

Đầu ra:

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

Nói cách khác: chỉ phân tách trên dấu phẩy nếu dấu phẩy đó bằng 0 hoặc số lượng trích dẫn chẵn trước nó .

Hoặc, một chút thân thiện hơn cho đôi mắt:

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

mà tạo ra giống như ví dụ đầu tiên.

BIÊN TẬP

Như được đề cập bởi @MikeFHay trong các bình luận:

Tôi thích sử dụng Guava's Splitter , vì nó có các mặc định saner hơn (xem phần thảo luận ở trên về các trận đấu trống được cắt bớt String#split(), vì vậy tôi đã làm:

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

Theo RFC 4180: Sec 2.6: "Các trường chứa ngắt dòng (CRLF), dấu ngoặc kép và dấu phẩy phải được đặt trong dấu ngoặc kép." Phần 2.7: "Nếu trích dẫn kép được sử dụng để bao quanh các trường, thì một trích dẫn kép xuất hiện bên trong một trường phải được thoát bằng cách đặt trước nó bằng một trích dẫn kép khác" Vì vậy, nếu String line = "equals: =,\"quote: \"\"\",\"comma: ,\"", tất cả những gì bạn cần làm là loại bỏ trích dẫn kép không liên quan nhân vật.
Paul Hanbury

@Bart: quan điểm của tôi là giải pháp của bạn vẫn hoạt động, ngay cả với các trích dẫn được nhúng
Paul Hanbury

6
@Alex, yeah, dấu phẩy được khớp, nhưng kết quả trống không có kết quả. Thêm vào -1phương thức chia param : line.split(regex, -1). Xem: docs.oracle.com/javase/6/docs/api/java/lang/iêu
Bart

2
Hoạt động tuyệt vời! Tôi thích sử dụng Guava's Splitter, vì nó có các mặc định saner (xem phần thảo luận ở trên về các kết quả trống được cắt bởi String # split), vì vậy tôi đã làm như vậy Splitter.on(Pattern.compile(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)")).
MikeFHay

2
CẢNH BÁO!!!! Regrec này là chậm !!! Nó có hành vi O (N ^ 2) trong đó giao diện ở mỗi dấu phẩy trông từ đầu đến cuối chuỗi. Việc sử dụng biểu thức chính quy này đã gây ra sự chậm lại 4 lần trong các công việc Spark lớn (ví dụ 45 phút -> 3 giờ). Cách thay thế nhanh hơn là một cái gì đó giống như findAllIn("(?s)(?:\".*?\"|[^\",]*)*")kết hợp với bước hậu xử lý để bỏ qua trường đầu tiên (luôn trống) sau mỗi trường không trống.
Urban Vagabond

46

Mặc dù tôi rất thích các biểu thức thông thường nói chung, đối với loại mã thông báo phụ thuộc trạng thái này, tôi tin rằng một trình phân tích cú pháp đơn giản (trong trường hợp này đơn giản hơn nhiều so với từ đó có thể phát ra âm thanh) có lẽ là một giải pháp sạch hơn, đặc biệt liên quan đến khả năng duy trì , ví dụ:

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

Nếu bạn không quan tâm đến việc giữ dấu phẩy bên trong dấu ngoặc kép, bạn có thể đơn giản hóa cách tiếp cận này (không xử lý chỉ mục bắt đầu, không có trường hợp đặc biệt ký tự cuối cùng ) bằng cách thay thế dấu phẩy của bạn trong dấu ngoặc kép bằng cách khác:

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

Báo giá nên được xóa khỏi mã thông báo được phân tích cú pháp, sau khi chuỗi được phân tích cú pháp.
Sudhir N

Tìm thấy qua google, bro thuật toán đẹp, đơn giản và dễ thích nghi, đồng ý. công cụ trạng thái nên được thực hiện thông qua trình phân tích cú pháp, regex là một mớ hỗn độn.
Rudolf Schmidt

2
Hãy nhớ rằng nếu dấu phẩy là ký tự cuối cùng thì nó sẽ nằm trong giá trị Chuỗi của mục cuối cùng.
Gabriel Gates

21

3
Cuộc gọi tốt nhận ra rằng OP đã phân tích tệp CSV. Một thư viện bên ngoài là cực kỳ thích hợp cho nhiệm vụ này.
Stefan Kendall

1
Nhưng chuỗi là một chuỗi CSV; bạn sẽ có thể sử dụng api CSV trên chuỗi đó trực tiếp.
Michael Brewer-Davis

đúng, nhưng nhiệm vụ này đủ đơn giản và một phần nhỏ hơn của một ứng dụng lớn hơn, mà tôi không cảm thấy muốn kéo vào một thư viện bên ngoài khác.
Jason S

7
không nhất thiết ... kỹ năng của tôi thường đầy đủ, nhưng chúng được hưởng lợi từ việc được mài giũa.
Jason S

9

Tôi sẽ không tư vấn một câu trả lời regex từ Bart, tôi thấy giải pháp phân tích cú pháp tốt hơn trong trường hợp cụ thể này (như Fabian đề xuất). Tôi đã thử giải pháp regex và triển khai phân tích cú pháp riêng Tôi thấy rằng:

  1. Phân tích cú pháp nhanh hơn nhiều so với phân tách bằng regex với phản hồi - nhanh hơn ~ 20 lần đối với chuỗi ngắn, nhanh hơn ~ 40 lần đối với chuỗi dài.
  2. Regex không tìm thấy chuỗi trống sau dấu phẩy cuối cùng. Đó không phải là trong câu hỏi ban đầu, đó là yêu cầu của tôi.

Giải pháp của tôi và thử nghiệm dưới đây.

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

Tất nhiên, bạn có thể tự do thay đổi chuyển sang các if-if khác trong đoạn trích này nếu bạn cảm thấy không thoải mái với sự xấu xí của nó. Lưu ý sau đó thiếu ngắt sau khi chuyển đổi với dải phân cách. Thay vào đó, StringBuilder được chọn để StringBuffer theo thiết kế để tăng tốc độ, trong đó an toàn của luồng là không liên quan.


2
Điểm thú vị liên quan đến việc chia thời gian và phân tích cú pháp. Tuy nhiên, tuyên bố # 2 là không chính xác. Nếu bạn thêm -1phương thức phân tách vào câu trả lời của Bart, bạn sẽ bắt được các chuỗi trống (bao gồm cả các chuỗi trống sau dấu phẩy cuối cùng):line.split(regex, -1)
Peter

+1 vì đó là một giải pháp tốt hơn cho vấn đề mà tôi đang tìm kiếm giải pháp: phân tích chuỗi tham số thân HTTP POST phức tạp
varontron

2

Hãy thử một lookaround như (?!\"),(?!\"). Điều này sẽ phù hợp ,mà không được bao quanh bởi ".


Khá chắc chắn rằng sẽ phá vỡ một danh sách như: "foo", bar, "baz"
Angelo Genovese

1
Tôi nghĩ bạn có ý đó (?<!"),(?!"), nhưng nó vẫn không hoạt động. Đưa ra chuỗi one,two,"three,four", nó khớp chính xác dấu phẩy one,two, nhưng nó cũng khớp với dấu phẩy "three,four"và không khớp với dấu phẩy two,"three.
Alan Moore

Nó phù hợp để làm việc hoàn hảo với tôi, IMHO Tôi nghĩ rằng đây là một câu trả lời tốt hơn vì nó ngắn hơn và dễ hiểu hơn
Ordiel

2

Bạn đang ở trong khu vực ranh giới phiền phức mà regexps gần như sẽ không làm (như đã được Bart chỉ ra, thoát khỏi các trích dẫn sẽ khiến cuộc sống trở nên khó khăn), và một trình phân tích cú pháp đầy đủ có vẻ như quá mức cần thiết.

Nếu bạn có thể cần sự phức tạp lớn hơn bất cứ lúc nào tôi sẽ sớm tìm kiếm một thư viện phân tích cú pháp. Ví dụ cái này


2

Tôi đã thiếu kiên nhẫn và chọn cách không chờ đợi câu trả lời ... để tham khảo, thật khó để làm điều gì đó như thế này (hoạt động cho ứng dụng của tôi, tôi không cần phải lo lắng về các trích dẫn đã thoát, như các công cụ trong ngoặc kép được giới hạn trong một vài hình thức ràng buộc):

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

(bài tập cho người đọc: mở rộng để xử lý các trích dẫn đã thoát bằng cách tìm kiếm dấu gạch chéo ngược.)


1

Cách tiếp cận đơn giản nhất là không khớp các dấu phân cách, tức là dấu phẩy, với logic bổ sung phức tạp để khớp với những gì được dự định thực sự (dữ liệu có thể được trích dẫn chuỗi), chỉ để loại trừ các dấu phân cách sai, nhưng phù hợp với dữ liệu dự định ở vị trí đầu tiên.

Mẫu bao gồm hai lựa chọn thay thế, một chuỗi được trích dẫn ( "[^"]*"hoặc ".*?") hoặc mọi thứ cho đến dấu phẩy tiếp theo ( [^,]+). Để hỗ trợ các ô trống, chúng tôi phải cho phép mục không được trích dẫn để trống và sử dụng dấu phẩy tiếp theo, nếu có và sử dụng \\Gneo:

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

Mẫu cũng chứa hai nhóm chụp để lấy một trong hai nội dung của chuỗi được trích dẫn hoặc nội dung đơn giản.

Sau đó, với Java 9, chúng ta có thể nhận được một mảng như

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

trong khi các phiên bản Java cũ hơn cần một vòng lặp như

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

Thêm các mục vào một Listhoặc một mảng là một đặc điểm cho người đọc.

Đối với Java 8, bạn có thể sử dụng việc results()thực hiện câu trả lời này , để thực hiện nó giống như giải pháp Java 9.

Đối với nội dung hỗn hợp với các chuỗi nhúng, như trong câu hỏi, bạn chỉ cần sử dụng

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

Nhưng sau đó, các chuỗi được giữ ở dạng trích dẫn của họ.


0

Thay vì sử dụng lookahead và regex điên rồ khác, chỉ cần rút ra các trích dẫn đầu tiên. Đó là, đối với mỗi nhóm trích dẫn, thay thế nhóm đó bằng __IDENTIFIER_1hoặc một số chỉ báo khác và ánh xạ nhóm thành bản đồ chuỗi, chuỗi.

Sau khi bạn phân tách bằng dấu phẩy, thay thế tất cả các định danh được ánh xạ bằng các giá trị chuỗi gốc.


và làm thế nào để tìm nhóm trích dẫn mà không có regexS điên?
Kai Huppmann

Đối với mỗi ký tự, nếu ký tự được trích dẫn, hãy tìm trích dẫn tiếp theo và thay thế bằng nhóm. Nếu không có trích dẫn tiếp theo, thực hiện.
Stefan Kendall

0

Điều gì về một lớp lót bằng String.split ()?

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

-1

Tôi sẽ làm một cái gì đó như thế này:

boolean foundQuote = false;

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

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

else 

{
  string[] split = currentString.split(',');  
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.