जावा: अल्पविराम से अलग स्ट्रिंग को विभाजित करना लेकिन उद्धरणों में अल्पविराम की अनदेखी करना


249

मैं इस तरह से एक स्ट्रिंग है:

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

कि मैं अल्पविराम से विभाजित करना चाहता हूं - लेकिन मुझे उद्धरणों में अल्पविराम को अनदेखा करना होगा। मैं यह कैसे कर सकता हूँ? एक regexp दृष्टिकोण की तरह लगता है विफल रहता है; मुझे लगता है कि मैं मैन्युअल रूप से स्कैन कर सकता हूं और एक अलग मोड में प्रवेश कर सकता हूं जब मैं एक उद्धरण देख सकता हूं, लेकिन preexisting पुस्तकालयों का उपयोग करना अच्छा होगा। ( संपादित करें : मुझे लगता है कि मेरा मतलब था कि पुस्तकालय जो पहले से ही जेडीके का हिस्सा हैं या पहले से ही अपाचे कॉमन्स जैसे आमतौर पर उपयोग किए जाने वाले पुस्तकालयों का हिस्सा हैं।)

उपरोक्त स्ट्रिंग में विभाजित होना चाहिए:

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"

दूसरे शब्दों में: अल्पविराम पर केवल तभी विभाजित करें जब उस अल्पविराम में शून्य हो, या उससे आगे उद्धरणों की संख्या भी हो

या, आंखों के लिए थोड़ा मित्रवत:

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 ने टिप्पणियों में बताया है:

मैं का उपयोग करना पसंद अमरूद के स्प्लिटर , के रूप में यह saner चूक है (खाली मैचों से छंटनी की जा रही के बारे में ऊपर चर्चा को देखें String#split(), तो मैं क्या किया:

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

RFC 4180: Sec 2.6 के अनुसार: "लाइन ब्रेक (CRLF), डबल कोट्स और कॉमा से युक्त फ़ील्ड्स को डबल-कोट्स में संलग्न किया जाना चाहिए।" सेक २.: "यदि डबल-कोट्स का उपयोग खेतों को घेरने के लिए किया जाता है, तो किसी फ़ील्ड के अंदर दिखाई देने वाले दोहरे-उद्धरण को दूसरे दोहरे उद्धरण से पहले ले जाने से बचना चाहिए", यदि String line = "equals: =,\"quote: \"\"\",\"comma: ,\"", आपको बस इतना करना है कि आप एक्सट्रूज़न के दोहरे उद्धरण को हटा दें। पात्र।
पॉल हनबरी

@ बर्ट: मेरा कहना है कि आपका समाधान अभी भी काम करता है, यहां तक ​​कि एम्बेडेड उद्धरणों के साथ भी
पॉल हनबरी

6
@ एलेक्स, हाँ, अल्पविराम का मिलान किया जाता है, लेकिन खाली मिलान परिणाम में नहीं है। -1विभाजन विधि में जोड़ें परम line.split(regex, -1):। देखें: docs.oracle.com/javase/6/docs/api/java/lang/...
बार्ट Kiers

2
बहुत अच्छा काम करता है! मैं अमरूद के स्प्लिटर का उपयोग करना पसंद करता हूं, क्योंकि इसमें सेर डिफॉल्ट है (ऊपर की चर्चा करें कि खाली मैचों को स्ट्रिंग # विभाजित करके ट्रिम किया जा रहा है), इसलिए मैंने किया Splitter.on(Pattern.compile(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)"))
माइकफा

2
चेतावनी !!!! यह regexp धीमा है !!! इसमें O (N ^ 2) व्यवहार है कि प्रत्येक अल्पविराम पर लुकहेड स्ट्रिंग के अंत में सभी तरह दिखता है। इस regexp के इस्तेमाल से स्पार्क की बड़ी नौकरियों (जैसे 45 मिनट -> 3 घंटे) में 4 गुना मंदी आ गई। तेज़ विकल्प कुछ ऐसा है जैसे findAllIn("(?s)(?:\".*?\"|[^\",]*)*")प्रत्येक गैर-रिक्त फ़ील्ड के बाद पहले (हमेशा-खाली) फ़ील्ड को छोड़ने के लिए पोस्टप्रोसेसिंग चरण के साथ संयोजन में।
अर्बन वागाबोंड

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

स्ट्रिंग के पार्स होने के बाद, उद्धरण पार्स किए गए टोकन से हटा दिए जाने चाहिए।
सुधीर एन

Google के माध्यम से मिला, अच्छा एल्गोरिथ्म भाई, सरल और अनुकूलित करना, सहमत होना। स्टेटफुल सामान को पार्सर के माध्यम से किया जाना चाहिए, रेगेक्स एक गड़बड़ है।
रुडोल्फ श्मिट

2
ध्यान रखें कि यदि एक अल्पविराम अंतिम वर्ण है तो वह अंतिम वस्तु के स्ट्रिंग मूल्य में होगा।
गेब्रियल गेट्स

21

3
ओपी एक सीएसवी फ़ाइल पार्स कर रहा था, यह पहचानते हुए अच्छी कॉल। इस कार्य के लिए एक बाहरी पुस्तकालय अत्यंत उपयुक्त है।
स्टीफन केंडल

1
लेकिन स्ट्रिंग एक सीएसवी स्ट्रिंग है; आपको उस स्ट्रिंग पर सीधे CSV एपीआई का उपयोग करने में सक्षम होना चाहिए।
माइकल ब्रेवर-डेविस

हां, लेकिन यह कार्य काफी सरल है, और एक बड़े अनुप्रयोग का एक छोटा हिस्सा है, कि मुझे किसी अन्य बाहरी पुस्तकालय में खींचने का मन नहीं है।
जेसन एस

7
जरूरी नहीं ... मेरे कौशल अक्सर पर्याप्त हैं, लेकिन वे सम्मानित होने से लाभ उठाते हैं।
जेसन एस

9

मैं बार्ट से रेगेक्स जवाब की सलाह नहीं दूंगा, मुझे इस विशेष मामले में बेहतर समाधान मिल रहा है (जैसा कि फेबियन प्रस्तावित)। मैंने रेगेक्स सॉल्यूशन और स्वयं पार्सिंग कार्यान्वयन की कोशिश की है जो मैंने पाया है कि:

  1. पार्सिंग बैकरेफेरेंस के साथ रेगेक्स के साथ विभाजित होने की तुलना में बहुत तेज है - छोटे तारों के लिए ~ 20 गुना तेज, लंबे तारों के लिए ~ 40 गुना तेज।
  2. Regex पिछले कॉमा के बाद खाली स्ट्रिंग खोजने में विफल रहता है। हालांकि यह मूल प्रश्न में नहीं था, यह मेरी आवश्यकता थी।

मेरा समाधान और परीक्षण नीचे।

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 गलत है। यदि आप -1बार्ट के उत्तर में विभाजित विधि में जोड़ते हैं , तो आप खाली तारों को पकड़ लेंगे (अंतिम अल्पविराम के बाद खाली तार सहित):line.split(regex, -1)
पीटर

+1 क्योंकि यह उस समस्या का एक बेहतर समाधान है जिसके लिए मैं एक समाधान खोज रहा था: एक जटिल HTTP POST बॉडी पैरामीटर स्ट्रिंग पार्सिंग
varontron

2

एक प्रयास करें lookaround की तरह (?!\"),(?!\")। यह मैच होना चाहिए ,जो कि घिरे नहीं हैं "


बहुत यकीन है कि इस तरह की एक सूची के लिए टूट जाएगा: "फू", बार, "बाज"
एंजेलो जेनोविस

1
मुझे लगता है कि आपका मतलब था (?<!"),(?!"), लेकिन यह अभी भी काम नहीं करेगा। स्ट्रिंग को देखते हुए one,two,"three,four", यह कॉमा को अंदर से सही ढंग से मेल खाता है one,two, लेकिन यह कॉमा से भी मेल खाता है "three,four", और एक में मैच करने में विफल रहता है two,"three
एलन मूर

यह मेरे लिए पूरी तरह से काम करता है, IMHO मुझे लगता है कि यह एक बेहतर जवाब है क्योंकि इसके छोटे और अधिक आसानी से समझ में आने के कारण
ऑर्डिएल

2

आप उस कष्टप्रद सीमा क्षेत्र में हैं जहाँ regexps लगभग नहीं चलेगा (जैसा कि बार्ट द्वारा इंगित किया गया है, उद्धरणों से बचकर जीवन कठिन हो जाएगा), और फिर भी एक पूर्ण विकसित पार्सर ओवरकिल जैसा लगता है।

यदि आपको किसी भी समय अधिक जटिलता की आवश्यकता है तो जल्द ही मैं एक पार्सर लाइब्रेरी की तलाश में जाऊंगा। उदाहरण के लिए यह एक


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\"(.*?)\",?|([^,]*),?");

पैटर्न में दो कैप्चरिंग समूह भी होते हैं, या तो उद्धृत स्ट्रिंग की सामग्री या सादे सामग्री।

फिर, जावा 9 के साथ, हम एक सरणी प्राप्त कर सकते हैं

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

जबकि पुराने जावा संस्करणों को लूप की तरह की आवश्यकता होती है

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

आइटम को एक Listसरणी में जोड़ने से रीडर के लिए एक एक्साइज के रूप में छोड़ दिया जाता है।

जावा 8 के लिए, आप इस उत्तर के results()कार्यान्वयन का उपयोग कर सकते हैं , इसे जावा 9 समाधान की तरह करें।

मिश्रित तारों के साथ मिश्रित सामग्री के लिए, जैसे प्रश्न में, आप बस उपयोग कर सकते हैं

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

लेकिन फिर, स्ट्रिंग्स को उनके उद्धृत रूप में रखा जाता है।


0

लुकहेड और अन्य पागल रेगेक्स का उपयोग करने के बजाय, पहले उद्धरणों को बाहर निकालें। यही है, हर उद्धरण समूहन के लिए, उस समूह को __IDENTIFIER_1किसी अन्य संकेतक के साथ बदलें , और उस समूह को स्ट्रिंग, स्ट्रिंग के मानचित्र पर मैप करें।

अल्पविराम पर विभाजित होने के बाद, सभी मैप किए गए पहचानकर्ताओं को मूल स्ट्रिंग मानों से बदलें।


और कैसे पागल regexS के बिना उद्धरण समूहों को खोजने के लिए?
काई हप्पमन

प्रत्येक चरित्र के लिए, यदि चरित्र उद्धरण है, तो अगला उद्धरण ढूंढें और समूहीकरण के साथ बदलें। अगर अगली बोली नहीं, किया।
स्टीफन केंडल

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(',');  
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.