IT story

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

hot-time 2020. 4. 14. 19:15
반응형

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 라이브러리를 추천 할 수 있습니까?

Java lib 또는 앱을 사용하여 CSV를 XML 파일로 변환 하시겠습니까?


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

물론이 스 니펫에서 추악함에 불편 함을 느끼면 스위치를 else-if로 변경할 수 있습니다. 그런 다음 구분 기호를 사용한 스위치 후 끊김이 없습니다. 스레드 안전성과 관련이없는 속도를 높이기 위해 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;
}

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


lookahead와 다른 미친 정규식을 사용하지 말고 따옴표를 먼저 빼십시오. 즉, 모든 견적 그룹화에 대해 해당 그룹화를 __IDENTIFIER_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((\"(.*?)\"|[^,])*),?");

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


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

boolean foundQuote = false;

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

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

else 

{
  string[] split = currentString.split(',');  
}

참고 : https://stackoverflow.com/questions/1757065/java-splitting-a-comma-separated-string-but-ignoring-commas-in-quotes

반응형