Java: การแยกสตริงที่คั่นด้วยเครื่องหมายจุลภาค แต่ละเว้นเครื่องหมายจุลภาคในเครื่องหมายคำพูด


249

ฉันมีสตริงคลุมเครือเช่นนี้

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

ที่ฉันต้องการแยกด้วยเครื่องหมายจุลภาค - แต่ฉันต้องละเว้นเครื่องหมายจุลภาคในเครื่องหมายคำพูด ฉันจะทำสิ่งนี้ได้อย่างไร ดูเหมือนว่าวิธีการ regexp ล้มเหลว; ฉันคิดว่าฉันสามารถสแกนด้วยตนเองและเข้าสู่โหมดที่แตกต่างกันเมื่อฉันเห็นคำพูด แต่มันจะดีที่จะใช้ห้องสมุดมาก่อน ( แก้ไข : ฉันเดาว่าฉันหมายถึงไลบรารีที่เป็นส่วนหนึ่งของ 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"

กล่าวอีกนัยหนึ่ง: แยกเครื่องหมายจุลภาคเฉพาะถ้าเครื่องหมายจุลภาคนั้นมีศูนย์หรือจำนวนเครื่องหมายอัญประกาศหน้าคู่

หรือเป็นมิตรกับตามากขึ้น:

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เนื่องจากมีค่าเริ่มต้น saner (ดูการสนทนาข้างต้นเกี่ยวกับการจับคู่ที่ว่างเปล่าที่ถูกตัดแต่งโดยString#split()ดังนั้นฉันจึง:

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

ตาม RFC 4180: Sec 2.6: "ฟิลด์ที่มีตัวแบ่งบรรทัด (CRLF), เครื่องหมายคำพูดคู่และเครื่องหมายจุลภาคควรอยู่ในเครื่องหมายคำพูดคู่" ตอนที่ 2.7: "ถ้าใช้เครื่องหมายคำพูดคู่เพื่อใส่เขตข้อมูลเครื่องหมายอัญประกาศคู่ที่ปรากฏในเขตข้อมูลจะต้องถูกหลีกเลี่ยงโดยนำหน้าด้วยเครื่องหมายคำพูดคู่อื่น" ดังนั้นถ้าString line = "equals: =,\"quote: \"\"\",\"comma: ,\""คุณต้องทำคือถอดแถบคำพูดภายนอกที่ไม่เกี่ยวข้อง ตัวละคร
พอล Hanbury

@Bart: ประเด็นของฉันคือการที่โซลูชันของคุณยังใช้งานได้แม้จะมีเครื่องหมายคำพูดฝังอยู่ก็ตาม
Paul Hanbury

6
@ Alex ใช่จุลภาคจะถูกจับคู่ แต่การแข่งขันที่ว่างเปล่าไม่ได้อยู่ในผล เพิ่ม-1ไปยังวิธีการแยกพารามิเตอร์: line.split(regex, -1). โปรดดู: docs.oracle.com/javase/6/docs/api/java/lang/…
Bart Kiers

2
ใช้งานได้ดี! ผมชอบใช้ของฝรั่ง Splitter เป็นมันมีค่าเริ่มต้นของ saner (ดูการอภิปรายข้างต้นเกี่ยวกับการแข่งขันที่ว่างเปล่าถูกตัดแต่งด้วยสตริง # แยก) Splitter.on(Pattern.compile(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)"))ดังนั้นฉันได้
MikeFHay

2
คำเตือน!!!! regexp นี้ช้า !!! มันมีพฤติกรรม O (N ^ 2) โดยที่ lookahead ที่เครื่องหมายจุลภาคแต่ละตัวจะมองไปจนถึงจุดสิ้นสุดของสตริง การใช้ regexp นี้ส่งผลให้งาน 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

พบได้ผ่านทาง google, อัลกอริทึมที่ดี, ง่ายและง่ายต่อการปรับตัวเห็นด้วย สิ่ง stateful ควรทำผ่าน parser, regex เป็นระเบียบ
Rudolf Schmidt

2
โปรดทราบว่าหากเครื่องหมายจุลภาคเป็นอักขระตัวสุดท้ายจะอยู่ในค่าสตริงของรายการสุดท้าย
Gabriel Gates เมื่อ

21

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

https://github.com/pupi1985/JavaCSV-Reloaded (ทางแยกของไลบรารีก่อนหน้าซึ่งจะอนุญาตให้ผลลัพธ์ที่สร้างขึ้นมีตัวสิ้นสุดบรรทัด Windows \r\nเมื่อไม่ได้ใช้ Windows)

http://opencsv.sourceforge.net/

CSV API สำหรับ Java

คุณสามารถแนะนำห้องสมุด Java สำหรับการอ่าน (และอาจจะเขียน) ไฟล์ CSV หรือไม่

Java lib หรือแอปแปลง CSV เป็นไฟล์ XML หรือไม่


3
การโทรที่ดีรับรู้ว่า OP กำลังแยกวิเคราะห์ไฟล์ CSV ไลบรารีภายนอกเหมาะสมอย่างยิ่งสำหรับงานนี้
Stefan Kendall

1
แต่สตริงนั้นเป็นสตริง CSV คุณควรจะใช้ CSV API บนสตริงนั้นโดยตรง
Michael Brewer-Davis

ใช่ แต่งานนี้ง่ายพอและส่วนเล็ก ๆ ของแอพพลิเคชั่นที่ใหญ่กว่านั้นฉันไม่รู้สึกอยากดึงในไลบรารี่ภายนอกอื่น
46499 Jason S

7
ไม่จำเป็นต้อง ... ทักษะของฉันมักจะเพียงพอ แต่พวกเขาได้รับประโยชน์จากการถูกฝึกฝน
46499 Jason S

9

ฉันจะไม่แนะนำให้คำตอบ regex จาก Bart ฉันหาวิธีการแยกวิเคราะห์ที่ดีกว่าในกรณีนี้ (ตามที่เสนอ Fabian) ฉันได้ลองใช้โซลูชัน regex และใช้การแยกวิเคราะห์ตัวเองฉันพบว่า:

  1. การแยกวิเคราะห์เร็วกว่าการแยกด้วย regex กับ backreferences เร็วกว่า ~ ~ ~ ~ ~ ~ ~ ~ 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วิธีการแยกในคำตอบของ Bart คุณจะได้รับสตริงว่าง (รวมถึงสตริงว่างหลังจากเครื่องหมายจุลภาคสุดท้าย):line.split(regex, -1)
Peter Peter

+1 เพราะเป็นทางออกที่ดีกว่าสำหรับปัญหาที่ฉันค้นหาวิธีแก้ปัญหา: การแยกสตริงพารามิเตอร์ HTTP POST body ที่ซับซ้อน
varontron

2

ลองLookAround(?!\"),(?!\")เช่น นี้ควรตรงกับที่ไม่ได้ล้อมรอบด้วย,"


ค่อนข้างแน่ใจว่าจะทำลายรายการเช่น: "foo", bar, "baz"
Angelo Genovese

1
ฉันคิดว่าคุณหมายถึง(?<!"),(?!")แต่มันก็ยังไม่ทำงาน ได้รับสายone,two,"three,four"ก็ถูกต้องตรงกับเครื่องหมายจุลภาคในone,twoแต่ก็ยังตรงกับเครื่องหมายจุลภาคในและล้มเหลวเพื่อให้ตรงกับหนึ่งใน"three,four" two,"three
Alan Moore

มันทำงานได้อย่างสมบูรณ์แบบสำหรับฉัน IMHO ฉันคิดว่านี่เป็นคำตอบที่ดีกว่าเนื่องจากมันสั้นกว่าและเข้าใจง่ายกว่านี้
Ordiel

2

คุณอยู่ในเขตแดนที่น่ารำคาญที่ซึ่ง regexps แทบจะไม่ทำ (ตามที่ 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หรือตัวบ่งชี้อื่น ๆ และแผนที่ที่จัดกลุ่มไปยังแผนที่ของสตริงสตริง

หลังจากคุณแยกด้วยเครื่องหมายจุลภาคให้แทนที่ตัวระบุที่แมปทั้งหมดด้วยค่าสตริงเดิม


และวิธีการค้นหาการจัดกลุ่มคำพูดโดยไม่ต้อง regexS บ้า?
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.