java - examples - regular expression شرح




جافا: تقسيم سلسلة مفصولة بفواصل مع تجاهل الفواصل بين علامتي اقتباس (6)

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

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


أنت في هذا المجال الحدودي المزعج حيث لا تفعل تقريبا regexps (كما أشار بارت ، فإن الهروب من الاقتباسات سيجعل الحياة صعبة) ، ومع ذلك يبدو المحلل الكامل مثل المبالغة.

إذا كنت ستحتاج إلى مزيد من التعقيد في أي وقت قريب ، فسوف أذهب للبحث عن مكتبة محلل. على سبيل المثال هذا واحد


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

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


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

boolean foundQuote = false;

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

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

else 

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

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

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

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


محاولة:

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




string