在不在 [ ] 内时,将字符串按 / 拆分

5
我正在尝试拆分表示XPath的字符串,例如:
string myPath = "/myns:Node1/myns:Node2[./myns:Node3=123456]/myns:Node4";

我需要在“/”上进行分割(结果不包括“/”,就像普通的字符串分割一样),除非“/”恰好在“[...]”内(在此情况下,“/”既不会被分割,也会包含在结果中)。
所以,使用正常的string[] result = myPath.Split("/".ToCharArray())得到的结果是:
result[0]: //Empty string, this is ok
result[1]: myns:Node1
result[2]: myns:Node2[.
result[3]: myns:Node3=123456]
result[4]: myns:Node4

`results[2]`和`result[3]`应该被合并,最终我应该得到:
result[0]: //Empty string, this is ok
result[1]: myns:Node1
result[2]: myns:Node2[./myns:Node3=123456]
result[3]: myns:Node4

由于我对正则表达式不是非常熟练,所以在分割后手动重新组合结果成为一个新数组,但让我担心的是,虽然在这个例子中让它工作很容易,但在我得到更复杂的xpath时,正则表达式似乎是更好的选择。

就记录而言,我看过以下问题:
Regex split string preserving quotes
C# Regex Split - commas outside quotes
Split a string that has white spaces, unless they are enclosed within "quotes"?

虽然它们应该足以帮助我解决问题,但我遇到了一些问题/令人困惑的方面,阻止它们帮助我。
在前两个链接中,作为一个正则表达式新手,我发现它们很难理解和学习。它们正在寻找引号,这些引号在左右成对的情况下看起来相同,因此将其转换为 [ 和 ] 对我来说很困惑,尝试和错误没有教会我任何东西,而是让我更加沮丧。我可以理解相当基本的正则表达式,但这些答案所做的事情比我目前理解的要多一些,即使在第一个链接中有解释。
在第三个链接中,我将无法使用LINQ,因为代码将在较旧版本的.NET中使用。


我同意链接问题中的正则表达式可能会使初学者感到不知所措...当需要时,我喜欢认为自己在正则表达式方面还算过得去,但我承认那些确实让我感到困惑... - Broots Waymb
4个回答

5
XPath是一种复杂的语言,在根级别上尝试在斜杠上拆分XPath表达式在许多情况下都会失败,例如:
/myns:Node1/myns:Node2[./myns:Node3=123456]/myns:Node4
string(/myns:Node1/myns:Node2)

我建议采用另一种方法来覆盖更多情况。不要尝试拆分,而是使用Regex.Matches(String, String) 方法匹配每个斜杠之间的部分。这种方式的优点在于您可以自由地描述这些部分的外观。
string pattern = @"(?xs)
    [^][/()]+ # all that isn't a slash or a bracket
    (?: # predicates (eventually nested)
        \[ 
        (?: [^]['""] | (?<c>\[) | (?<-c>] )
          | "" (?> [^""\\]* (?: \\. [^""\\]* )* ) "" # quoted parts
          | '  (?> [^'\\]*  (?: \\. [^'\\]*  )* ) '
        )*?
        (?(c)(?!$)) # check if brackets are balanced
        ]
      |  # same thing for round brackets
        \(
        (?: [^()'""] | (?<d>\() | (?<-d>\) )
          | "" (?> [^""\\]* (?: \\. [^""\\]* )* ) ""
          | '  (?> [^'\\]*  (?: \\. [^'\\]*  )* ) '
        )*?
        (?(d)(?!$))
        \)
    )*
  |
    (?<![^/])(?![^/]) # empty string between slashes, at the start or end
";

注意:如果要确保字符串被完全解析,可以在模式的末尾添加类似于|\z(?<=(.))的内容。这样,您可以测试捕获组是否存在,以了解是否已到达字符串的末尾。(但您也可以使用匹配位置、长度和字符串长度。)

演示


那将是我们无法推特的正则表达式 :P - vks
1
@vks:我承认它有点长。 - Casimir et Hippolyte
用这么长的正则表达式,我宁愿手动解析字符串。 :/ - Abion47
@Abion47:请随意进行(使用相同或更好的功能)。 - Casimir et Hippolyte
1
@CasimiretHippolyte,我已经添加了手动解析选项作为答案。虽然我不是很熟悉XPath语法,但如果您能指出我可能忽略的错误情况,我将不胜感激。 - Abion47
@Abion47:这是一个不错的举措,如果执行得当,可能是性能方面最有效的方式(因为C#是编译的)。这是这类问题缺失的答案。我不确定我有没有想到XPath语法的所有细节,但是简要记录一下,我尝试在我的回答中处理以下情况:1)可能嵌套的谓词及其内部路径,2)可能带引号的谓词内部的部分(可能包含斜杠),3)像 string(...) 这样必须视为单个部分的函数。 - Casimir et Hippolyte

3
如果需要像Casimir et Hippolyte建议的那样复杂的Regex模式,则在此情况下,Regex可能不是最佳选择。为了添加一个非Regex解决方案,这是手动解析XPath字符串时过程可能看起来像:
public string[] Split(string input, char splitChar, char groupStart, char groupEnd)
{
    List<string> splits = new List<string>();

    int startIdx = 0;
    int groupNo = 0;

    for (int i = 0; i < input.Length; i++)
    {
        if (input[i] == splitChar && groupNo == 0)
        {
            splits.Add(input.Substring(startIdx, i - startIdx));
            startIdx = i + 1;
        }
        else if (input[i] == groupStart)
        {
            groupNo++;
        }
        else if (input[i] == groupEnd)
        {
            groupNo = Math.Max(groupNo - 1, 0);
        }
    }

    splits.Add(input.Substring(startIdx, input.Length - startIdx));

    return splits.Where(s => !string.IsNullOrEmpty(s)).ToArray();
}

个人认为这种方法更易于理解和实现。使用它,您可以执行以下操作:

var input = "/myns:Node1/myns:Node2[./myns:Node3=123456]/myns:Node4[text(‌​)='some[] brackets']";
var split = Split(input, '/', '[', ']');

这将输出以下内容:
split[0] = "myns:Node1"
split[1] = "myns:Node2[./myns:Node3=123456]"
split[2] = "myns:Node4[text(‌​)='some[] brackets']"

顺便提一下,一个模式长并不意味着它比一个短模式效率低。请记住,像 a(?!.*b) 这样的东西必须为每个成功匹配测试字符串直到结尾。在我的模式中,每个结果都只被一个失败位置(在每个斜杠处)与前一个分开。但我承认,可以使用捕获组来提取部分和 \G 锚点来确保匹配的连续性,从而使其更好地编写。 - Casimir et Hippolyte
1
@CasimiretHippolyte 我不是说Regex不好,因为一个较长的模式意味着它效率更低。我是说,Regex模式越长,调试、维护和理解就会变得更加麻烦。到了一定程度,你只是强迫工具运行而没有任何原因使用它。在这些情况下,自定义解析方法变得更加可取,因为它更简单易懂且易于维护,并且在执行上可能更有效率。(始终牢记KISS原则。) - Abion47
1
这是一个常见但幼稚的想法。实际上并不幼稚。我更愿意维护一个编译安全、简洁明了的方法,而不是一个冗长且可能出错的正则表达式模式。 - Abion47
1
关于KISS原则,我不是美国海军,很抱歉我无法理解。KISS原则代表“保持简单,愚蠢”。它是一种帮助防止陷入特定思维模式而导致过度复杂化的原则,强制使其工作的过程比必要的要复杂得多。 - Abion47
@CasimiretHippolyte 哎呀,刚意识到我忘记在我的回复中标记你了。 - Abion47
显示剩余4条评论

1
你发的第二个链接其实非常适合你的需求。只需要稍微调整一下就能检测括号而不是撇号:
\/(?=(?:[^[]*\[[^\]]*])*[^]]*$)

这个函数的基本作用是只包含那些前面有左方括号,后面有右方括号的斜杠,并在下一个斜杠之前使用它。您可以像这样使用它:

string[] matches = Regex.Split(myPath, "\\/(?=(?:[^[]*\\[[^\\]]*])*[^]]*$)")

太好了!谢谢!那个链接让人困惑的部分就是应该用哪个撇号代替哪个括号。但有了这个,我可以比较两者并从中学习! - Code Stranger
一旦某个字符串字面量中有括号,例如/myns:Node1/myns:Node2[./myns:Node3=123456]/myns:Node4[text()='some[] brackets'],这段代码就无法正常工作。 - Wiktor Stribiżew
@WiktorStribiżew 我不是XPath专家,但从未见过这样的写法,我的意思是在引号内使用 [] - vks
1
@vks:带括号的值在表单中经常出现,例如复选框:<input type="checkbox" name="myvar[]" />。为了使用XPath访问它们,您可以编写//input[@name="myvar[]"。另外,在XPath语言中,谓词可以嵌套://span[./ancestor::div[@class="myclass"]] - Casimir et Hippolyte
@CasimiretHippolyte 我知道这是可能的,但我在我所参与的任何项目中都没有见过这样奇怪的命名...第二种情况似乎更合理!!!!!!谢谢 - vks
@vks:复选框大多数情况下都是这样命名的。(目标是通过GET或POST在服务器端获取表单中所有复选框的数组) - Casimir et Hippolyte

1
\/(?![^\[]*\])

尝试一下。查看演示。

https://regex101.com/r/uLcWux/1

使用@\\/(?![^\\[]*\\])进行配合使用

附注:仅适用于简单的XPath,不包含引号内的嵌套括号[]


@CodeStranger,可以有嵌套的括号吗? - vks
@flakes 可以有嵌套的括号吗?问题中没有提到。 - vks
从我目前看到的xpath中,还没有出现过。我从未在xpath中使用过嵌套括号,所以我会说这种情况极其不可能发生,但也许不是完全不可能? - Code Stranger
@CodeStranger 如果不是这种情况,你可以使用这个简单的。 - vks
1
@flakes!!!! 没有见过这样的例子...谢谢...我会在答案中添加一条注释。 - vks

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