C#, регулярные выражения: как анализировать значения, разделенные запятыми, где некоторые значения могут быть цитируемыми строками, содержащими запятые

В C#, используя Regex class, как можно анализировать значения, разделенные запятыми, где некоторые значения могут быть цитируемыми строками, содержащими запятые?

using System ;
using System.Text.RegularExpressions ;

class  Example
    {
    public static void Main ( )
        {
        string  myString  =  "cat,dog,"0 = OFF, 1 = ON",lion,tiger,'R = red, G = green, B = blue',bear" ;
        Console.WriteLine ( "nmyString is ...nt" + myString + "n" ) ;
        Regex   regex  =  new Regex  (  "(?<=,("|')).*?(?=("|'),)|(^.*?(?=,))|((?<=,).*?(?=,))|((?<=,).*?$)"  )  ;
        Match   match  =  regex.Match ( myString ) ;
        int j = 0 ;
        while ( match.Success )
            {
            Console.WriteLine ( j++ + " t" + match ) ;
            match  =  match.NextMatch() ;
            }
        }
    }

выход (частично) выглядит следующим образом:

0       cat
1       dog
2       "0 = OFF
3        1 = ON"
4       lion
5       tiger
6       'R = red
7        G = green
8        B = blue'
9       bear
, нужные вывод:
0       cat
1       dog
2       0 = OFF, 1 = ON
3       lion
4       tiger
5       R = red, G = green, B = blue
6       bear

9 ответов


попробуйте с этим регулярным выражением:

"[^"\r\n]*"|'[^'\r\n]*'|[^,\r\n]*

    Regex regexObj = new Regex(@"""[^""\r\n]*""|'[^'\r\n]*'|[^,\r\n]*");
    Match matchResults = regexObj.Match(input);
    while (matchResults.Success) 
    {
        Console.WriteLine(matchResults.Value);
        matchResults = matchResults.NextMatch();
    }

Ouputs:

  • кошки
  • собака
  • "0 = ВЫКЛ, 1 = ВКЛ"
  • Лев
  • тигр
  • 'R = красный, G = зеленый, B = синий'
  • медведь

Примечание: это решение regex будет работать для вашего случая, однако я рекомендую вам использовать специализированную библиотеку, такую как FileHelpers.


Почему бы не прислушаться к советам экспертов и Не сворачивайте свой собственный CSV-парсер.

ваша первая мысль: "мне нужно обрабатывать запятые внутри кавычек."

ваша следующая мысль будет: "о, дерьмо, мне нужно обрабатывать цитаты внутри цитат. Сбежавшие цитаты. Двойные кавычки. Простые кавычки..."

Это дорога к безумию. Не пиши свои собственные. Найдите библиотеку с обширным охватом модульных тестов, которая поражает все жесткие части и прошла ад для тебя. Для .NET используйте бесплатный и открытый исходный код FileHelpers библиотека.


Это не регулярное выражение, но я использовал Microsoft.На языке VisualBasic.FileIO.TextFieldParser для выполнения этого для csv-файлов. да, может показаться немного странным добавление ссылки на Microsoft.VisualBasic в приложении c#, может быть, даже немного грязный, но эй, это работает.


просто добавляю решение, над которым я работал сегодня утром.

var regex = new Regex("(?<=^|,)(\"(?:[^\"]|\"\")*\"|[^,]*)");

foreach (Match m in regex.Matches("<-- input line -->"))
{
    var s = m.Value; 
}

Как вы можете видеть, Вам нужно вызвать regex.Matches ()в строке. Затем он вернет MatchCollection с тем же количеством элементов, что и столбцы. Свойство Value каждого соответствия, очевидно, является анализируемым значением.

Это все еще работа, но она с удовольствием анализирует строки CSV, такие как:

2,3.03,"Hello, my name is ""Joshua""",A,B,C,,,D

А, Регулярное Выражение. Теперь у вас две проблемы. ;)

Я бы использовал tokenizer / parser, так как это довольно просто и, что более важно, намного проще читать для последующего обслуживания.

это работает, например:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;

class Program
{
    static void Main(string[] args)
    {
        string myString = "cat,dog,\"0 = OFF, 1 = ON\",lion,tiger,'R = red, G = green,     B = blue',bear"; 
        Console.WriteLine("\nmyString is ...\n\t" + myString + "\n");
        CsvParser parser = new CsvParser(myString);

        Int32 lineNumber = 0;
        foreach (string s in parser)
        {
            Console.WriteLine(lineNumber + ": " + s);
        }

        Console.ReadKey();
    }
}

internal enum TokenType
{
    Comma,
    Quote,
    Value
}

internal class Token
{
    public Token(TokenType type, string value)
    {
        Value = value;
        Type = type;
    }

    public String Value { get; private set; }
    public TokenType Type { get; private set; }
}

internal class StreamTokenizer : IEnumerable<Token>
{
    private TextReader _reader;

    public StreamTokenizer(TextReader reader)
    {
        _reader = reader;    
    }

    public IEnumerator<Token> GetEnumerator()
    {
        String line;
        StringBuilder value = new StringBuilder();

        while ((line = _reader.ReadLine()) != null)
        {
            foreach (Char c in line)
            {
                switch (c)
                {
                    case '\'':
                    case '"':
                        if (value.Length > 0)
                        {
                            yield return new Token(TokenType.Value, value.ToString());
                            value.Length = 0;
                        }
                        yield return new Token(TokenType.Quote, c.ToString());
                        break;
                    case ',':
                       if (value.Length > 0)
                        {
                            yield return new Token(TokenType.Value, value.ToString());
                            value.Length = 0;
                        }
                        yield return new Token(TokenType.Comma, c.ToString());
                        break;
                    default:
                        value.Append(c);
                        break;
                }
            }

            // Thanks, dpan
            if (value.Length > 0) 
            {
                yield return new Token(TokenType.Value, value.ToString()); 
            }
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

internal class CsvParser : IEnumerable<String>
{
    private StreamTokenizer _tokenizer;

    public CsvParser(Stream data)
    {
        _tokenizer = new StreamTokenizer(new StreamReader(data));
    }

    public CsvParser(String data)
    {
        _tokenizer = new StreamTokenizer(new StringReader(data));
    }

    public IEnumerator<string> GetEnumerator()
    {
        Boolean inQuote = false;
        StringBuilder result = new StringBuilder();

        foreach (Token token in _tokenizer)
        {
            switch (token.Type)
            {
                case TokenType.Comma:
                    if (inQuote)
                    {
                        result.Append(token.Value);
                    }
                    else
                    {
                        yield return result.ToString();
                        result.Length = 0;
                    }
                    break;
                case TokenType.Quote:
                    // Toggle quote state
                    inQuote = !inQuote;
                    break;
                case TokenType.Value:
                    result.Append(token.Value);
                    break;
                default:
                    throw new InvalidOperationException("Unknown token type: " +    token.Type);
            }
        }

        if (result.Length > 0)
        {
            yield return result.ToString();
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

CSV не обычный. Если ваш язык регулярных выражений не имеет достаточной мощности для обработки статического характера синтаксического анализа csv (маловероятно, MS-нет), то любое чистое решение регулярных выражений-это список ошибок, ожидающих появления, Когда вы нажмете новый источник ввода, который не совсем обрабатывается последним регулярным выражением.

чтение CSV не так сложно писать как государственную машину, так как грамматика проста, но даже так вы должны учитывать: кавычки, запятые внутри кавычки, новые строки в кавычках, пустые поля.

таким образом, вы, вероятно, должны просто использовать чужой CSV-парсер. Я рекомендую CSVReader для .Net


:

    private List<string> ParseDelimitedString (string arguments, char delim = ',')
    {
        bool inQuotes = false;
        bool inNonQuotes = false; //used to trim leading WhiteSpace

        List<string> strings = new List<string>();

        StringBuilder sb = new StringBuilder();
        foreach (char c in arguments)
        {
            if (c == '\'' || c == '"')
            {
                if (!inQuotes)
                    inQuotes = true;
                else
                    inQuotes = false;
            }else if (c == delim)
            {
                if (!inQuotes)
                {
                    strings.Add(sb.Replace("'", string.Empty).Replace("\"", string.Empty).ToString());
                    sb.Remove(0, sb.Length);
                    inNonQuotes = false;
                }
                else
                {
                    sb.Append(c);
                }
            }
            else if ( !char.IsWhiteSpace(c) && !inQuotes && !inNonQuotes)  
            {
                if (!inNonQuotes) inNonQuotes = true;
                sb.Append(c);
            }
        }
        strings.Add(sb.Replace("'", string.Empty).Replace("\"", string.Empty).ToString());


        return strings;
    }

использование

    string myString = "cat,dog,\"0 = OFF, 1 = ON\",lion,tiger,'R = red, G = green, B = blue',bear,         text";
    List<string> strings = ParseDelimitedString(myString);

    foreach( string s in strings )
            Console.WriteLine( s );

выход:

cat
dog
0 = OFF, 1 = ON
lion
tiger
R = red, G = green, B = blue
bear
text

Я нашел несколько ошибок в этой версии, например, строку без кавычек, которая имеет одну кавычку в значении.

и я согласен использовать библиотеку FileHelper, когда вы можете, однако эта библиотека требует, чтобы вы знали, как будут выглядеть ваши данные... Мне нужен общий парсер.

поэтому я обновил код до следующего и подумал, что поделюсь...

    static public List<string> ParseDelimitedString(string value, char delimiter)
    {
        bool inQuotes = false;
        bool inNonQuotes = false;
        bool secondQuote = false;
        char curQuote = '';

        List<string> results = new List<string>();

        StringBuilder sb = new StringBuilder();
        foreach (char c in value)
        {
            if (inNonQuotes)
            {
                // then quotes are just characters
                if (c == delimiter)
                {
                    results.Add(sb.ToString());
                    sb.Remove(0, sb.Length);
                    inNonQuotes = false;
                }
                else
                {
                    sb.Append(c);
                }
            }
            else if (inQuotes)
            {
                // then quotes need to be double escaped
                if ((c == '\'' && c == curQuote) || (c == '"' && c == curQuote))
                {
                    if (secondQuote)
                    {
                        secondQuote = false;
                        sb.Append(c);
                    }
                    else
                        secondQuote = true;
                }
                else if (secondQuote && c == delimiter)
                {
                    results.Add(sb.ToString());
                    sb.Remove(0, sb.Length);
                    inQuotes = false;
                }
                else if (!secondQuote)
                {
                    sb.Append(c);
                }
                else
                {
                    // bad,as,"user entered something like"this,poorly escaped,value
                    // just ignore until second delimiter found
                }
            }
            else
            {
                // not yet parsing a field
                if (c == '\'' || c == '"')
                {
                    curQuote = c;
                    inQuotes = true;
                    inNonQuotes = false;
                    secondQuote = false;
                }
                else if (c == delimiter)
                {
                    // blank field
                    inQuotes = false;
                    inNonQuotes = false;
                    results.Add(string.Empty);
                }
                else
                {
                    inQuotes = false;
                    inNonQuotes = true;
                    sb.Append(c);
                }
            }
        }

        if (inQuotes || inNonQuotes)
            results.Add(sb.ToString());

        return results;
    }

так как этот вопрос:Regex для разбора csv с вложенными кавычками

отчеты здесь и гораздо более общие, и поскольку регулярное выражение на самом деле не является правильным способом решения этой проблемы (т. е. у меня было много проблем с катастрофическим возвратом (http://www.regular-expressions.info/catastrophic.html)

вот простая реализация парсера на Python, а также

def csv_to_array(string):
    stack = []
    match = []
    matches = []

    for c in string:
        # do we have a quote or double quote?
        if c == "\"":
            # is it a closing match?
            if len(stack) > 0 and stack[-1] == c:
                stack.pop()
            else:
                stack.append(c)
        elif (c == "," and len(stack) == 0) or (c == "\n"):
            matches.append("".join(match))
            match = []
        else:
            match.append(c)

    return matches