2011-11-14 14 views
5

Quiero dividir una cadena en una lista o matriz.Cómo dividir cadenas utilizando expresiones regulares

Entrada: green,"yellow,green",white,orange,"blue,black"

El carácter de división es la coma (,), pero debe ignorar comas entre comillas.

La salida debe ser:

  • verde
  • amarillo, verde
  • blanco
  • naranja
  • azul, negro

Gracias.

+0

¿Tiene que usar expresiones regulares? – rownage

+0

hui, Siéntase libre de a) comentar, b) votar yc) elegir la respuesta que más le ayuda. :) –

Respuesta

11

En realidad esto es bastante fácil de usar solo partido:

 string subjectString = @"green,""yellow,green"",white,orange,""blue,black"""; 
     try 
     { 
      Regex regexObj = new Regex(@"(?<="")\b[a-z,]+\b(?="")|[a-z]+", RegexOptions.IgnoreCase); 
      Match matchResults = regexObj.Match(subjectString); 
      while (matchResults.Success) 
      { 
       Console.WriteLine("{0}", matchResults.Value); 
       // matched text: matchResults.Value 
       // match start: matchResults.Index 
       // match length: matchResults.Length 
       matchResults = matchResults.NextMatch(); 
      } 

Salida:

green 
yellow,green 
white 
orange 
blue,black 

Explicación:

@" 
      # Match either the regular expression below (attempting the next alternative only if this one fails) 
    (?<=   # Assert that the regex below can be matched, with the match ending at this position (positive lookbehind) 
     ""   # Match the character “""” literally 
    ) 
    \b   # Assert position at a word boundary 
    [a-z,]  # Match a single character present in the list below 
        # A character in the range between “a” and “z” 
        # The character “,” 
     +   # Between one and unlimited times, as many times as possible, giving back as needed (greedy) 
    \b   # Assert position at a word boundary 
    (?=   # Assert that the regex below can be matched, starting at this position (positive lookahead) 
     ""   # Match the character “""” literally 
    ) 
|   # Or match regular expression number 2 below (the entire match attempt fails if this one fails to match) 
    [a-z]  # Match a single character in the range between “a” and “z” 
     +   # Between one and unlimited times, as many times as possible, giving back as needed (greedy) 
" 
+0

@downvoter ¿Le importaría explicar el razonamiento detrás de su downvoting? ¿O continuarás escondiéndote detrás de tu anonimato? – FailedDev

+1

LOL primera respuesta aceptada que tiene voto a favor. ¿Hay una insignia para esto? : D – FailedDev

+0

No creo que las expresiones regulares sean el camino a seguir con este problema, ya que todo ese seguimiento hacia adelante y hacia atrás es muy lento. Pero te daré una votación positiva para resolver este simple ejemplo. –

2

Alguien pronto obtendrá una respuesta que hace esto con una sola expresión regular. No soy tan listo, pero solo por el bien del equilibrio, aquí hay una sugerencia que no usa una expresión completa. Basado en el viejo dicho de que cuando intentas resolver un problema con una expresión regular, entonces tienes dos problemas. :)

Personalmente dada mi falta de expresiones regulares-fu, lo haría uno de los siguientes:

  • uso de un simple Replace escapar ninguna coma dentro cotizaciones con algo más basado en expresiones regulares (es decir, "&comma;"). Luego puede hacer un simple string.Split() en el resultado y deshacer cada elemento en la matriz resultante antes de usarlo. Esto es asqueroso En parte porque se trata de un doble manejo de todo, y en parte porque también usa expresiones regulares. Boooo!
  • Parse it by hand, char by char. Convierta la cadena en una matriz de caracteres, luego repítala a través de ella, teniendo en cuenta si está "entre comillas" o no, y construya la matriz resultante de a a a la vez.
  • Igual que la sugerencia anterior, pero usando un csv-analizador de alguien en Internet. El ejemplo que creo a continuación no pasa exactamente todas las pruebas de la especificación csv, por lo que solo es una guía para ilustrar mi punto.

Hay una buena posibilidad de que las opciones no regexes tengan un mejor rendimiento si están bien escritas, porque las expresiones regulares pueden ser un poco caras ya que escanean cadenas internamente buscando patrones.

Realmente, solo quería señalar que no tiene que usar una expresión regular. :)

Aquí hay una implementación bastante ingenua de mi segunda sugerencia. En mi PC, me alegra analizar un millón de cadenas de 15 columnas en poco más de 4,5 segundos.

public class ManualParser : IParser 
{ 
    public IEnumerable<string> Parse(string line) 
    { 
     if (string.IsNullOrWhiteSpace(line)) return new List<string>(); 

     line = line.Trim(); 

     if (line.Contains(",") == false) return new[] { line.Trim('"') }; 

     if (line.Contains("\"") == false) return line.Split(',').Select(c => c.Trim()); 

     bool withinQuotes = false; 
     var builder = new List<string>(); 
     var trimChars = new[] { ' ', '"' }; 

     int left = 0; 
     int right = 0; 

     for (right = 0; right < line.Length; right++) 
     { 
      char c = line[right]; 

      if (c == '"') 
      { 
       withinQuotes = !withinQuotes; 
       continue; 
      } 

      if (c == ',' && !withinQuotes) 
      { 
       builder.Add(line.Substring(left, right - left).Trim(trimChars)); 
       right++; // Jump the comma 
       left = right; 
      } 
     } 

     builder.Add(line.Substring(left, right - left).Trim(trimChars)); 

     return builder; 
    } 
} 

Aquí hay algunas pruebas unitarias para ello:

[TestFixture] 
public class ManualParserTests 
{ 
    [Test] 
    public void Parse_GivenStringWithNoQuotesAndNoCommas_ShouldReturnThatString() 
    { 
     // Arrange 
     var parser = new ManualParser(); 

     // Act 
     string[] result = parser.Parse("This is my data").ToArray(); 

     // Assert 
     Assert.AreEqual(1, result.Length, "Should only be one column returned"); 
     Assert.AreEqual("This is my data", result[0], "Incorrect value is returned"); 
    } 

    [Test] 
    public void Parse_GivenStringWithNoQuotesAndOneComma_ShouldReturnTwoColumns() 
    { 
     // Arrange 
     var parser = new ManualParser(); 

     // Act 
     string[] result = parser.Parse("This is, my data").ToArray(); 

     // Assert 
     Assert.AreEqual(2, result.Length, "Should be 2 columns returned"); 
     Assert.AreEqual("This is", result[0], "First value is incorrect"); 
     Assert.AreEqual("my data", result[1], "Second value is incorrect"); 
    } 

    [Test] 
    public void Parse_GivenStringWithQuotesAndNoCommas_ShouldReturnColumnWithoutQuotes() 
    { 
     // Arrange 
     var parser = new ManualParser(); 

     // Act 
     string[] result = parser.Parse("\"This is my data\"").ToArray(); 

     // Assert 
     Assert.AreEqual(1, result.Length, "Should be 1 column returned"); 
     Assert.AreEqual("This is my data", result[0], "Value is incorrect"); 
    } 

    [Test] 
    public void Parse_GivenStringWithQuotesAndCommas_ShouldReturnColumnsWithoutQuotes() 
    { 
     // Arrange 
     var parser = new ManualParser(); 

     // Act 
     string[] result = parser.Parse("\"This is\", my data").ToArray(); 

     // Assert 
     Assert.AreEqual(2, result.Length, "Should be 2 columns returned"); 
     Assert.AreEqual("This is", result[0], "First value is incorrect"); 
     Assert.AreEqual("my data", result[1], "Second value is incorrect"); 
    } 

    [Test] 
    public void Parse_GivenStringWithQuotesContainingCommasAndCommas_ShouldReturnColumnsWithoutQuotes() 
    { 
     // Arrange 
     var parser = new ManualParser(); 

     // Act 
     string[] result = parser.Parse("\"This, is\", my data").ToArray(); 

     // Assert 
     Assert.AreEqual(2, result.Length, "Should be 2 columns returned"); 
     Assert.AreEqual("This, is", result[0], "First value is incorrect"); 
     Assert.AreEqual("my data", result[1], "Second value is incorrect"); 
    } 
} 

Y aquí es una aplicación de ejemplo que he probado el rendimiento con:

class Program 
{ 
    static void Main(string[] args) 
    { 
     RunTest(); 
    } 

    private static void RunTest() 
    { 
     var parser = new ManualParser(); 
     string csv = Properties.Resources.Csv; 
     var result = new StringBuilder(); 
     var s = new Stopwatch(); 

     for (int test = 0; test < 3; test++) 
     { 
      int lineCount = 0; 

      s.Start(); 
      for (int i = 0; i < 1000000/50; i++) 
      { 
       foreach (var line in csv.Split(new[] { Environment.NewLine }, StringSplitOptions.None)) 
       { 
        string cur = line + s.ElapsedTicks.ToString(); 
        result.AppendLine(parser.Parse(cur).ToString()); 
        lineCount++; 
       } 
      } 
      s.Stop(); 
      Console.WriteLine("Completed {0} lines in {1}ms", lineCount, s.ElapsedMilliseconds); 
      s.Reset(); 
      result = new StringBuilder(); 
     } 
    } 
} 
2

El formato de la cadena que está tratando de dividir parece ser CSV estándar. Usar un analizador CSV probablemente sería más fácil/más rápido.

5

lo que tienes ahí es un lenguaje irregular. En otras palabras, el significado de un personaje depende de la secuencia de caracteres antes o después de él. Como su nombre lo indica, las expresiones regulares son para analizar los idiomas regulares.

Lo que necesita aquí es un Tokenizer y Parser, un buen motor de búsqueda en Internet debería guiarlo por ejemplos. De hecho, como los tokens son solo personajes, probablemente ni siquiera necesites el Tokenizer.

Si bien puede hacer este caso simple con una expresión regular, es probable que sea muy lento. También podría causar problemas si alguna vez las comillas no están equilibradas, ya que una expresión regular no detectaría este error, como lo haría un analizador.

Si está importando un archivo CSV, le recomendamos echar un vistazo a la clase Microsoft.VisualBasic.FileIO.TextFieldParser (simplemente agregue una referencia a Microsoft.VisualBasic.dll en un proyecto C#) que analiza archivos CSV.

Otra manera de hacer esto es escribir su propio state machine (ejemplo a continuación), aunque esto aún no resuelve el problema de una cita en el medio de un valor:

using System; 
using System.Text; 

namespace Example 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      string subjectString = @"green,""yellow,green"",white,orange,""blue,black"""; 

      bool inQuote = false; 
      StringBuilder currentResult = new StringBuilder(); 
      foreach (char c in subjectString) 
      { 
       switch (c) 
       { 
        case '\"': 
         inQuote = !inQuote; 
         break; 

        case ',': 
         if (inQuote) 
         { 
          currentResult.Append(c); 
         } 
         else 
         { 
          Console.WriteLine(currentResult); 
          currentResult.Clear(); 
         } 
         break; 

        default: 
         currentResult.Append(c); 
         break; 
       } 
      } 
      if (inQuote) 
      { 
       throw new FormatException("Input string does not have balanced Quote Characters"); 
      } 
      Console.WriteLine(currentResult); 
     } 
    } 
} 
Cuestiones relacionadas