[java] Java : 쉼표로 구분 된 문자열을 분할하지만 따옴표로 쉼표는 무시

다음과 같이 모호한 문자열이 있습니다.

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

쉼표로 나누고 싶지만 따옴표로 쉼표를 무시해야합니다. 어떻게해야합니까? 정규식 접근 방식이 실패한 것 같습니다. 따옴표를 볼 때 수동으로 스캔하고 다른 모드로 들어갈 수 있다고 가정하지만 기존 라이브러리를 사용하는 것이 좋습니다. ( 편집 : 이미 JDK의 일부이거나 Apache Commons와 같이 일반적으로 사용되는 라이브러리의 일부인 라이브러리를 의미한다고 생각합니다.)

위의 문자열은 다음과 같이 나뉩니다.

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

참고 : 이것은 CSV 파일이 아니며 전체 구조가 더 큰 파일에 포함 된 단일 문자열입니다



답변

시험:

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

산출:

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

, 쉼표에 0이 있거나 그 앞에 따옴표가 짝수 인 경우에만 쉼표로 분할하십시오 .

또는 눈에 조금 친숙합니다.

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

첫 번째 예제와 동일합니다.

편집하다

의견에서 @MikeFHay가 언급 한 바와 같이 :

나는 기본값이 더 이상 없기 때문에 Guava ‘s Splitter를 선호한다 String#split().

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

답변

나는 일반적인 정규 표현식을 좋아하지만, 이런 종류의 상태 의존적 토큰 화의 경우, 특히 유지 보수와 관련하여 간단한 파서 (이 경우 해당 단어가 소리를 낼 수있는 것보다 훨씬 간단합니다)가 더 깨끗한 솔루션이라고 생각합니다 예 :

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

따옴표 안에 쉼표를 유지하는 데 신경 쓰지 않는다면 따옴표 로 쉼표를 다른 것으로 바꾸고 쉼표로 나누면이 방법 (시작 색인 처리, 마지막 문자 특수 경우 제외)을 단순화 할 수 있습니다.

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


답변

http://sourceforge.net/projects/javacsv/

https://github.com/pupi1985/JavaCSV-Reloaded
(이전 라이브러리의 포크는 Windows를 \r\n실행하지 않을 때 생성 된 출력에 Windows 줄 종결자가 있을 수 있도록합니다 )

http://opencsv.sourceforge.net/

Java 용 CSV API

CSV 파일 읽기 및 쓰기에 Java 라이브러리를 추천 할 수 있습니까?

CSV를 XML 파일로 변환하는 Java 라이브러리 또는 앱?


답변

Bart의 정규식 답변을 조언하지 않을 것입니다.이 특별한 경우 (Fabian이 제안한 것처럼) 구문 분석 솔루션이 더 좋습니다. 정규식 솔루션과 자체 구문 분석 구현을 시도했지만 다음을 발견했습니다.

  1. 역 참조를 사용하는 정규 표현식으로 분할하는 것보다 구문 분석이 훨씬 빠릅니다. 짧은 문자열의 경우 ~ 20 배, 긴 문자열의 경우 ~ 40 배 더 빠릅니다.
  2. 정규식이 마지막 쉼표 뒤에 빈 문자열을 찾지 못했습니다. 그것은 원래의 질문에는 없었지만 그것은 나의 요구 사항이었습니다.

내 솔루션과 테스트는 다음과 같습니다.

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

물론이 스 니펫에서 추악함에 불편 함을 느낀다면 다른 스위치로 자유롭게 바꿀 수 있습니다. 그런 다음 구분 기호를 사용한 스위치 후 끊김이 없습니다. 스레드 안전성과 관련이없는 속도를 높이기 위해 StringBuilder가 StringBuffer 대신에 선택되었습니다.


답변

과 같은 둘러보기를 시도하십시오 (?!\"),(?!\"). 에 ,둘러싸이지 않은 일치해야합니다 ".


답변

정규 표현식이 거의 수행하지 않는 성가신 경계 영역에 있습니다 (Bart가 지적한 것처럼 따옴표를 탈출하면 삶이 어려워 질 것입니다). 그러나 완전한 파서는 과도하게 보입니다.

조만간 더 큰 복잡성이 필요할 경우 파서 라이브러리를 찾아 볼 것입니다. 예를 들어 이것


답변

나는 참을성이 없었고 답을 기다리지 않기로 결정했습니다 … 참조를 위해 이런 식으로하기가 어렵지 않습니다 (응용 프로그램에서 작동하므로 이스케이프 된 따옴표에 대해 걱정할 필요가 없습니다. 제한된 형식으로 제한됨)

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

(독자 운동 : 백 슬래시도 찾아 이스케이프 된 따옴표 처리까지 확장하십시오.)