解析包含数组的字符串

9

我希望能将包含递归字符串数组的字符串转换为深度为1的数组。

示例:

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.stackexchange.com/faq#questions。 - Gert Arnold
5个回答

6
< p >@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);

这是一个非常棒的解决方案。 :) - ojlovecd
谢谢,希望我没有抢了你的风头 :) - Scott Rippey
当然不是。只有讨论和改进。 :) - ojlovecd
你的解决方案很棒。我终于找到时间来学习它,真的非常好。只有一个问题:现在我希望字符串即使包含列表也能作为原子实体保留(例如:"[a, "b, [c, d]", e]" => ["a", "b, [c, d]", "e"]),但是引号没有区分开口引号和闭合引号,所以平衡组无法正常工作。你是否有一个优雅的解决方案呢?:-) - dijxtra
平衡引号——让引擎出现问题的好方法!最近我用JavaScript做了这个(https://gist.github.com/1349099),但是那个解决方案使用了正则表达式作为解析引擎,所以它不是一个纯正则表达式解决方案。看一下可能会很有趣。 - Scott Rippey
我在我的回答中加入了尝试平衡引号的内容。 - Scott Rippey

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

我也认为正则表达式是解决这个问题的方法,但这并不可行,因为你需要捕获“平衡”的括号。 - Scott Rippey
@ScottRippey 你好,Scott,我修改了我的代码,请试一下。 - ojlovecd
看起来不错。需要一些清理,但我会假设它可以工作 :) 对于其他对这些“平衡组”感兴趣的人,特别是对于平衡括号匹配,您应该查看Microsoft关于“平衡组定义”的文档。 - Scott Rippey

0
说实话,我会把这个方法写成 F# 程序集,因为这样可能更容易。如果你查看 C# 中的 JavaScriptSerializer 实现(使用像 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#好。


0

没有真正的“标准”方法来做这件事。请注意,如果您想考虑所有可能性,实现可能会变得非常混乱。我建议使用递归方式,例如:

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

无论什么对你来说都很香


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接