java شرح - جافا:تقسيم سلسلة مفصولة بفواصل مع تجاهل الفواصل بين علامتي اقتباس




regular expression (9)

لدي سلسلة غامضة مثل هذا:

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 ، إنه عبارة عن سلسلة واحدة مضمنة في ملف ذي بنية عامة أكبر


Answers

في حين أنني أحب تعبيرات عادية بشكل عام ، لهذا النوع من الرموز التي تعتمد على الحالة أعتقد أن محلل بسيط (وهو في هذه الحالة أبسط بكثير من تلك الكلمة قد يجعله يبدو) هو الحل الأنظف ، خاصة فيما يتعلق بصيانة على سبيل المثال:

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

محاولة:

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's Splitter ، نظرًا لأنه يتضمن افتراضيًا افتراضيًا (انظر المناقشة السابقة حول التطابقات الفارغة التي يتم اقتطاعها بواسطة String#split() ، لذلك فعلت:

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

كنت غير صبور واخترت عدم الانتظار للحصول على إجابات ... للإشارة إلى أنه لا يبدو من الصعب القيام بشيء من هذا القبيل (الذي يعمل في طلبي ، لا داعي للقلق بشأن اقتباسات هرب ، كما هو الحال في الاقتباسات يقتصر على عدد قليل من الأشكال المقيدة):

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

(التمرين للقارئ: يمتد إلى التعامل مع عروض الأسعار الفارغة من خلال البحث عن الخطوط المائلة العكسية أيضًا).


جرِّب lookaround مثل (?!\"),(?!\") . يجب أن يتطابق هذا , غير محاط بـ " .


لا أنصحك بإجابة منطقية من بارت ، أجد حل إعراب أفضل في هذه الحالة بالذات (كما اقترح فابيان). لقد جربت تطبيق 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);

بالطبع أنت حر في تغيير التبديل إلى if-ifs في هذا المقتطف إذا كنت تشعر بعدم الارتياح مع قبحه. لاحظ ثم عدم وجود كسر بعد التبديل مع فاصل. تم اختيار StringBuilder بدلاً من ذلك إلى StringBuffer حسب التصميم لزيادة السرعة ، حيث تكون سلامة الخيط غير ذات صلة.



بدلا من استخدام lookahead وغيرها من regex مجنون ، فقط سحب الاقتباسات الأولى. بمعنى ، لكل مجموعة اقتباس ، __IDENTIFIER_1 تلك المجموعة بـ __IDENTIFIER_1 أو أي مؤشر آخر ، ثم قم بتعيين ذلك التجميع إلى خريطة السلسلة أو السلسلة.

بعد تقسيم الفاصلة ، استبدل جميع المعرفات المعيّنة بقيم السلسلة الأصلية.


سأفعل شيئًا كهذا:

boolean foundQuote = false;

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

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

else 

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

List <String> list = ...
String[] array = new String[list.size()];
int i=0;
for(String s: list){
  array[i++] = s;
}






java regex string