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


249

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

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

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

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

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

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

답변:


435

시험:

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(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"))

RFC 4180에 따르면 : 2.6 절 : "줄 바꿈 (CRLF), 큰 따옴표 및 쉼표를 포함하는 필드는 큰 따옴표로 묶어야합니다." 초 2.7 : 경우에, 그래서 "큰 따옴표는 다음 필드 내부에 나타나는 큰 따옴표가 또 다른 큰 따옴표로를 붙여야해야 묶으 분야에 사용되는 경우" String line = "equals: =,\"quote: \"\"\",\"comma: ,\""당신이 할 필요는 외부 따옴표 오프 스트립입니다 문자.
Paul Hanbury

@Bart : 내 포인트 솔루션은 아직도 포함 따옴표로 작동되는
폴 버리

6
@Alex, 예, 쉼표 일치하지만 빈 일치는 결과에 없습니다. -1split 메소드 param :에 추가하십시오 line.split(regex, -1). 참조 : docs.oracle.com/javase/6/docs/api/java/lang/...
바트 Kiers

2
잘 작동합니다! 내가 한 그래서 말짱 기본값을 가지고로서 나는, 구아바의 분배기를 사용하여 선호, (빈 일치는 문자열 # 분할에 의해 손질 것에 대해 위의 설명을 참조) Splitter.on(Pattern.compile(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)")).
MikeFHay

2
경고!!!! 이 정규 표현식은 느립니다 !!! 각 쉼표의 lookahead가 문자열 끝까지 보이도록 O (N ^ 2) 동작이 있습니다. 이 정규 표현식을 사용하면 대규모 Spark 작업에서 4 배 속도가 느려졌습니다 (예 : 45 분-> 3 시간). 더 빠른 대안은 findAllIn("(?s)(?:\".*?\"|[^\",]*)*")비어 있지 않은 각 필드 다음에 오는 첫 번째 (항상 비어있는) 필드를 건너 뛰는 후 처리 단계와 결합 된 것과 같습니다 .
Urban Vagabond

46

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

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

문자열을 구문 분석 한 후 구문 분석 된 토큰에서 따옴표를 제거해야합니다.
Sudhir N

구글을 통해 발견, 좋은 알고리즘 형제, 간단하고 적응하기 쉬운 동의. 상태 저장은 파서를 통해 수행해야하며 정규 표현식은 엉망입니다.
루돌프 슈미트

2
쉼표가 마지막 문자이면 마지막 항목의 문자열 값에 있음을 명심하십시오.
가브리엘 게이츠

21

3
OP가 CSV 파일을 구문 분석하고 있음을 인식하는 것이 좋습니다. 이 작업에는 외부 라이브러리가 매우 적합합니다.
Stefan Kendall

1
그러나 문자열은 CSV 문자열입니다. 해당 문자열에서 직접 CSV API를 사용할 수 있어야합니다.
Michael Brewer-Davis

예, 그러나이 작업은 충분히 간단하고 더 큰 응용 프로그램의 훨씬 작은 부분이므로 다른 외부 라이브러리를 가져 오는 느낌이 들지 않습니다.
Jason S

7
필연적으로 ... 내 기술이 종종 적합하지만 연마를 통해 이익을 얻습니다.
Jason S

9

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 대신에 선택되었습니다.


2
시간 분할과 파싱에 관한 흥미로운 점. 그러나 명령문 # 2는 정확하지 않습니다. -1Bart의 답변에서 split 메소드에 a 를 추가하면 빈 문자열 (마지막 쉼표 뒤의 빈 문자열 포함)을 잡을 수 있습니다.line.split(regex, -1)
Peter

해결책을 찾고 있던 문제에 대한 더 나은 해결책이기 때문에 +1 : 복잡한 HTTP POST 본문 매개 변수 문자열 구문 분석
varontron

2

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


"foo", bar, "baz"
Angelo Genovese

1
나는 당신이 의미한다고 생각 (?<!"),(?!")하지만 여전히 작동하지 않습니다. 문자열이 주어지면 one,two,"three,four"쉼표와 정확하게 일치 one,two하지만 쉼표와 일치하고 일치 "three,four"하지 않습니다 two,"three.
Alan Moore

완벽하게 작동하는 이음새, IMHO 나는 이것이 더 짧고 이해하기 쉬우므로 더 나은 답변이라고 생각합니다.
Ordiel

2

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

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


2

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

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

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


1

가장 간단한 방법은 실제로 의도 한 것과 일치하는 복잡한 추가 논리 (예 : 문자열로 인용 될 수있는 데이터)와 구분 기호, 즉 쉼표를 일치시키는 것이 아니라 잘못된 구분 기호를 제외하는 것이 아니라 처음에 의도 된 데이터를 일치시키는 것입니다.

패턴은 따옴표 붙은 문자열 ( "[^"]*"또는 ".*?") 또는 다음 쉼표 ( [^,]+) 까지 의 두 가지 대안으로 구성됩니다 . 빈 셀을 지원하려면 인용되지 않은 항목을 비우고 다음 쉼표 (있는 경우)를 사용하고 \\G앵커를 사용해야합니다 .

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

이 패턴에는 인용 된 문자열의 내용이나 일반 내용 중 하나를 얻기 위해 두 개의 캡처 그룹이 포함되어 있습니다.

그런 다음 Java 9를 사용하면 다음과 같이 배열을 얻을 수 있습니다.

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

이전 Java 버전은 다음과 같은 루프가 필요합니다.

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

에 항목 추가 List 배열이나 배열에 것은 독자에게 소비로 남습니다.

Java 8 results()경우이 답변 의 구현을 사용할 수 있습니다 하여 Java 9 솔루션처럼 수행 할 수 있습니다.

질문과 같이 문자열이 포함 된 혼합 콘텐츠의 경우 간단히 사용할 수 있습니다

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

그러나 문자열은 인용 된 형태로 유지됩니다.


0

lookahead와 다른 미친 정규식을 사용하는 대신 따옴표를 먼저 빼십시오. 즉, 모든 견적 그룹화에 대해 해당 그룹화를__IDENTIFIER_1 다른 표시기로 를 문자열, 문자열의 맵에 맵핑하십시오.

쉼표로 분할 한 후 매핑 된 모든 식별자를 원래 문자열 값으로 바꾸십시오.


미친 정규 표현식없이 견적 그룹을 찾는 방법은 무엇입니까?
Kai Huppmann

각 문자에 대해 문자가 따옴표 인 경우 다음 따옴표를 찾아 그룹화로 바꾸십시오. 다음 견적이 없으면 완료하십시오.
Stefan Kendall

0

String.split ()을 사용하는 단일 라이너는 어떻습니까?

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

-1

나는 이런 식으로 할 것입니다 :

boolean foundQuote = false;

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

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

else 

{
  string[] split = currentString.split(',');  
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.