синтаксический анализ строки, содержащей массив

#c# #string #list

#c# #строка #Список

Вопрос:

Я хотел бы преобразовать строку, содержащую рекурсивный массив строк, в массив с глубиной один.

Пример:

 StringToArray("[a, b, [c, [d, e]], f, [g, h], i]") == ["a", "b", "[c, [d, e]]", "f", "[g, h]", "i"]
  

Кажется довольно простым. Но я пришел из функционального фона и не очень хорошо знаком со стандартными библиотеками .NET Framework, поэтому каждый раз (я начинал с нуля примерно 3 раза) В итоге я получаю просто уродливый код. Моя последняя реализация находится здесь . Как вы видите, это чертовски уродливо.

Итак, каков способ C # сделать это?

Комментарии:

1. 1 для сложной задачи. Тем не менее, я думаю, что это обычно для codereview: codereview.stackexchange.com/faq#questions .

Ответ №1:

У @ojlovecd есть хороший ответ, использующий регулярные выражения.
Однако его ответ слишком сложен, поэтому вот мой аналогичный, более простой ответ.

 public string[] StringToArray(string input) {
    var pattern = new Regex(@"
        [
            (?:
            s*
                (?<results>(?:
                (?(open)  [^[]]   |  [^[],]   )
                |(?<open>[)
                |(?<-open>])
                ) )
                (?(open)(?!))
            ,?
            )*
        ]
    ", RegexOptions.IgnorePatternWhitespace);

    // Find the first match:
    var result = pattern.Match(input);
    if (result.Success) {
        // Extract the captured values:
        var captures = result.Groups["results"].Captures.Cast<Capture>().Select(c => c.Value).ToArray();
        return captures;
    }
    // Not a match
    return null;
}
  

Используя этот код, вы увидите, что StringToArray("[a, b, [c, [d, e]], f, [g, h], i]") будет возвращен следующий массив: ["a", "b", "[c, [d, e]]", "f", "[g, h]", "i"] .

Для получения дополнительной информации о сбалансированных группах, которые я использовал для сопоставления сбалансированных фигурных скобок, ознакомьтесь с документацией Microsoft.

Обновление:
согласно комментариям, если вы хотите также сбалансировать кавычки, вот возможная модификация. (Обратите внимание, что в C # " экранируется как "" ) Я также добавил описания шаблона, чтобы помочь прояснить его:

     var pattern = new Regex(@"
        [
            (?:
            s*
                (?<results>(?:              # Capture everything into 'results'
                    (?(open)                # If 'open' Then
                        [^[]]             #   Capture everything but brackets
                        |                   # Else (not open):
                        (?:                 #   Capture either:
                            [^[],'""]     #       Unimportant characters
                            |               #   Or
                            ['""][^'""]*?['""] #    Anything between quotes
                        )  
                    )                       # End If
                    |(?<open>[)            # Open bracket
                    |(?<-open>])           # Close bracket
                ) )
                (?(open)(?!))               # Fail while there's an unbalanced 'open'
            ,?
            )*
        ]
    ", RegexOptions.IgnorePatternWhitespace);
  

Комментарии:

1. Это фантастическое решение. 🙂

2. Спасибо, надеюсь, я не украл ваш гром 🙂

3. Конечно, нет. Есть только обсуждение и улучшение. 🙂

4. Ваше решение прекрасно. Мне наконец удалось найти время и изучить его, и это действительно фантастика. Только одна проблема: теперь я бы хотел, чтобы строки также сохранялись как атомарные объекты, даже если они содержат списки (так: «[a, «b, [c, d] «, e]» => [«a», «b, [c, d]»,»e»]), но кавычки не различают открывающую и закрывающую кавычки, поэтому балансирующие группы не работают. У вас есть элегантное решение и для этого? 🙂

5. Сбалансированные кавычки — способ бросить гаечный ключ в движок! Недавно я сделал это на JavaScript , но в этом решении в качестве механизма синтаксического анализа использовалось регулярное выражение, так что это не было чисто регулярным выражением. Возможно, было бы интересно взглянуть.

Ответ №2:

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

 static string[] StringToArray(string str)
{
    Regex reg = new Regex(@"^[(.*)]$");
    Match match = reg.Match(str);
    if (!match.Success)
        return null;
    str = match.Groups[1].Value;
    List<string> list = new List<string>();
    reg = new Regex(@"[[^[]]*(((?'Open'[)[^[]]*) ((?'-Open'])[^[]]*) )*(?(Open)(?!))]");
    Dictionary<string, string> dic = new Dictionary<string, string>();
    int index = 0;
    str = reg.Replace(str, m =>
    {
        string temp = "ojlovecd"   (index  ).ToString();
        dic.Add(temp, m.Value);
        return temp;
    });
    string[] result = str.Split(',');
    for (int i = 0; i < result.Length; i  )
    {
        string s = result[i].Trim();
        if (dic.ContainsKey(s))
            result[i] = dic[s].Trim();
        else
            result[i] = s;
    }
    return resu<
}
  

Комментарии:

1. Я также думаю, что регулярное выражение — это способ, но это не сработает, потому что вам нужно захватить «сбалансированные» фигурные скобки.

2. @ScottRippey Привет, Скотт, я изменил свой код, попробуйте, пожалуйста.

3. Это выглядит хорошо. Требуется некоторая очистка, но я предполагаю, что это работает 🙂 Для тех, кто еще интересуется этими «балансирующими группами», особенно для сбалансированного сопоставления фигурных скобок, вам следует ознакомиться с документацией Microsoft по «Балансирующим определениям групп».

Ответ №3:

Честно говоря, я бы просто написал этот метод в сборке F #, поскольку это, вероятно, намного проще. Если вы посмотрите на JavaScriptSerializer реализацию на C # (с помощью декомпилятора, такого как dotPeek или reflector), вы увидите, насколько запутан код синтаксического анализа массива для аналогичного массива в JSON. Конечно, это должно обрабатывать гораздо более разнообразный массив токенов, но вы поняли идею.

Вот их DeserializeList реализация, более уродливая, чем обычно, поскольку ее декомпилированная версия dotPeek, а не оригинал, но вы поняли идею. Будет выполнен DeserializeInternal возврат к дочернему списку.

 private IList DeserializeList(int depth)
{
  IList list = (IList) new ArrayList();
  char? nullable1 = this._s.MoveNext();
  if (((int) nullable1.GetValueOrDefault() != 91 ? 1 : (!nullable1.HasValue ? 1 : 0)) != 0)
    throw new ArgumentException(this._s.GetDebugString(AtlasWeb.JSON_InvalidArrayStart));
  bool flag = false;
  char? nextNonEmptyChar;
  char? nullable2;
  do
  {
    char? nullable3 = nextNonEmptyChar = this._s.GetNextNonEmptyChar();
    if ((nullable3.HasValue ? new int?((int) nullable3.GetValueOrDefault()) : new int?()).HasValue)
    {
      char? nullable4 = nextNonEmptyChar;
      if (((int) nullable4.GetValueOrDefault() != 93 ? 1 : (!nullable4.HasValue ? 1 : 0)) != 0)
      {
        this._s.MovePrev();
        object obj = this.DeserializeInternal(depth);
        list.Add(obj);
        flag = false;
        nextNonEmptyChar = this._s.GetNextNonEmptyChar();
        char? nullable5 = nextNonEmptyChar;
        if (((int) nullable5.GetValueOrDefault() != 93 ? 0 : (nullable5.HasValue ? 1 : 0)) == 0)
        {
          flag = true;
          nullable2 = nextNonEmptyChar;
        }
        else
          goto label_8;
      }
      else
        goto label_8;
    }
    else
      goto label_8;
  }
  while (((int) nullable2.GetValueOrDefault() != 44 ? 1 : (!nullable2.HasValue ? 1 : 0)) == 0);
  throw new ArgumentException(this._s.GetDebugString(AtlasWeb.JSON_InvalidArrayExpectComma));
 label_8:
  if (flag)
    throw new ArgumentException(this._s.GetDebugString(AtlasWeb.JSON_InvalidArrayExtraComma));
  char? nullable6 = nextNonEmptyChar;
  if (((int) nullable6.GetValueOrDefault() != 93 ? 1 : (!nullable6.HasValue ? 1 : 0)) != 0)
    throw new ArgumentException(this._s.GetDebugString(AtlasWeb.JSON_InvalidArrayEnd));
  else
    return list;
}
  

Рекурсивный синтаксический анализ просто не управляется так хорошо в C #, как в F #.

Ответ №4:

Не существует реального «стандартного» способа сделать это. Обратите внимание, что реализация может стать довольно запутанной, если вы хотите рассмотреть все возможности. Я бы порекомендовал что-то рекурсивное, например:

     private static IEnumerable<object> StringToArray2(string input)
    {
        var characters = input.GetEnumerator();
        return InternalStringToArray2(characters);
    }

    private static IEnumerable<object> InternalStringToArray2(IEnumerator<char> characters)
    {
        StringBuilder valueBuilder = new StringBuilder();

        while (characters.MoveNext())
        {
            char current = characters.Current;

            switch (current)
            {
                case '[':
                    yield return InternalStringToArray2(characters);
                    break;
                case ']':
                    yield return valueBuilder.ToString();
                    valueBuilder.Clear();
                    yield break;
                case ',':
                    yield return valueBuilder.ToString();
                    valueBuilder.Clear();
                    break;
                default:
                    valueBuilder.Append(current);
                    break;
            }
  

Хотя вы не ограничены рекурсивностью и всегда можете вернуться к одному методу, например

     private static IEnumerable<object> StringToArray1(string input)
    {
        Stack<List<object>> levelEntries = new Stack<List<object>>();
        List<object> current = null;
        StringBuilder currentLineBuilder = new StringBuilder();

        foreach (char nextChar in input)
        {
            switch (nextChar)
            {
                case '[':
                    levelEntries.Push(current);
                    current = new List<object>();
                    break;
                case ']':
                    current.Add(currentLineBuilder.ToString());
                    currentLineBuilder.Clear();
                    var last = current;
                    if (levelEntries.Peek() != null)
                    {
                        current = levelEntries.Pop();
                        current.Add(last);
                    }
                    break;
                case ',':
                    current.Add(currentLineBuilder.ToString());
                    currentLineBuilder.Clear();
                    break;
                default:
                    currentLineBuilder.Append(nextChar);
                    break;
            }
        }

        return current;
    }
  

Что бы ни пахло хорошо для вас

Ответ №5:

 using System;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.VisualBasic.FileIO; //Microsoft.VisualBasic.dll
using System.IO;

public class Sample {
    static void Main(){
        string data = "[a, b, [c, [d, e]], f, [g, h], i]";
        string[] fields = StringToArray(data);
        //check print
        foreach(var item in fields){
            Console.WriteLine(""{0}"",item);
        }
    }
    static string[] StringToArray(string data){
        string[] fields = null;
        Regex innerPat = new Regex(@"[s*(. )s*]");
        string innerStr = innerPat.Matches(data)[0].Groups[1].Value;
        StringBuilder wk = new StringBuilder();
        var balance = 0;
        for(var i = 0;i<innerStr.Length;  i){
            char ch = innerStr[i];
            switch(ch){
            case '[':
                if(balance == 0){
                    wk.Append('"');
                }
                wk.Append(ch);
                  balance;
                continue;
            case ']':
                wk.Append(ch);
                --balance;
                if(balance == 0){
                    wk.Append('"');
                }
                continue;
            default:
                wk.Append(ch);
                break;
            }
        }
        var reader = new StringReader(wk.ToString());
        using(var csvReader = new TextFieldParser(reader)){
            csvReader.SetDelimiters(new string[] {","});
            csvReader.HasFieldsEnclosedInQuotes = true;
            fields = csvReader.ReadFields();
        }
        return fields;
    }
}