正则表达式 - 转义转义字符

18

我的问题非常复杂,但可以归结为一个简单的例子。

我正在编写一个自定义查询语言,用户可以输入字符串,我解析成 LinQ 表达式。

我想要实现的是按 * 字符拆分字符串,除非它被正确转义。

Input         Output                          Query Description
"*\\*"    --> { "*", "\\", "*" }       -- contains a '\'
"*\\\**"  --> { "*", "\\\*", "*" }     -- contains '\*'
"*\**"    --> { "*", "\*", "*" }       -- contains '*' (works now)

我不介意Regex.Split返回空字符串,但最终结果却是这样的:

Regex.Split(@"*\\*", @"(?<!\\)(\*)")  --> {"", "*", "\\*"}

你可以看到,我已经尝试使用负回顾后发断言,在所有情况下都有效,除了这种情况。我还尝试过Regex.Escape,但没有成功。

显然,我的问题是我正在寻找\*,而\\*匹配它。但在这种情况下,\\是另一个转义序列。

任何解决方案不一定需要涉及正则表达式。


3
感谢您将问题的本质提炼为一个简明扼要的问题,给您点赞。 - MarioDS
在这个例子中,您可以使用类似于 (\*)(\\\\)(\*) 的模式匹配来得到三个匹配组。在您的实际情况下,这是否是使用拆分的可接受替代方法? - Chris
我已经更新了问题,并提供了另一个例子。 - Troels Larsen
如果你想通过(未转义的)*字符来分割字符串,难道你的第二个示例不应该是这样的吗? "*\\\**" --> { "*", "\\\*", "*" } - groverboy
@Jerry 问,你就会得到(有时候)。 - Cruncher
显示剩余4条评论
3个回答

8

我认为匹配比分离容易得多,尤其是您不会从初始字符串中删除任何内容。那么要匹配什么?除了未转义的 * 之外的所有内容。

如何做到这一点?使用以下正则表达式:

@"(?:[^*\\]+|\\.)+|\*"

(?:[^*\\]+|\\.)+ 匹配除了*或任何转义字符以外的所有内容。不需要任何环视。

\* 将匹配分隔符。

在代码中:

using System;
using System.Text.RegularExpressions;
using System.Linq;
public class Test
{
    public static void Main()
    {   
        string[] tests = new string[]{
            @"*\\*",
            @"*\\\**",
            @"*\**",
        };

        Regex re = new Regex(@"(?:[^*\\]+|\\.)+|\*");

        foreach (string s in tests) {
            var parts = re.Matches(s)
             .OfType<Match>()
             .Select(m => m.Value)
             .ToList();

            Console.WriteLine(string.Join(", ", parts.ToArray()));
        }
    }
}

输出:

*, \\, *
*, \\\*, *
*, \*, *

ideone demo


1
两个答案都可以,但是考虑到我离开后将维护这段代码的可怜家伙:较短的正则表达式胜出:D - Troels Larsen
不错的解决方案!@TroelsLarsen - 可以稍微缩短为 @"(?:[^*\\]|\\.)+|\*"。外层的+使内部的+变得多余。 - groverboy
有那么一点点的速度下降,但是可以接受。@groverboy - Jerry

4
我想到了这个正则表达式(?<=(?:^|[^\\])(?:\\\\)*)(\*)
解释:
你只需要在*之前列出可能出现的情况,包括:
  • 字符串开头 ^
  • 不是 \ - [^\\]
  • (不是 \ 或字符串开头)然后是偶数个 \ - (^|[^\\])(\\\\)*
测试代码和示例:
string[] tests = new string[]{
    @"*\\*",
    @"*\\\**",
    @"*\**",
    @"test\**test2",
};

Regex re = new Regex(@"(?<=(?:^|[^\\])(?:\\\\)*)(\*)");

foreach (string s in tests) {
    string[] m = re.Split( s );
    Console.WriteLine(String.Format("{0,-20} {1}", s, String.Join(", ",
       m.Where(x => !String.IsNullOrEmpty(x)))));
}

结果:

*\\*                 *, \\, *
*\\\**               *, \\\*, *
*\**                 *, \*, *
test\**test2         test\*, *, test2

现在在我的稍微复杂一些的环境中进行测试...通过了36/36个测试用例。非常感谢您!我相信我会发现更多异常,但这个答案已经帮助我自己解决它们了。 - Troels Larsen

1

我认为一个纯解析的、非正则表达式的解决方案会是对这个问题的很好的补充。

相比于理解任何一个正则表达式,我可以更快地阅读它。这也使得修复意外情况变得容易。逻辑直接呈现。

public static String[] splitOnDelimiterWithEscape(String toSplit, char delimiter, char escape) {
    List<String> strings = new ArrayList<>();

    char[] chars = toSplit.toCharArray();
    String sub = "";

    for(int i = 0 ; i < chars.length ; i++) {
        if(chars[i] == escape) {
            sub += (i+1 < chars.length) ? chars[++i] : ""; //assign whatever char is after the escape to the string. This essentially makes single escape character non-existent. It just forces the next character to be literal. If the escape is at end, then we just ignore it

            //this is the simplest implementation of the escape. If escaping certain characters should have
            //special behaviour it should be implemented here.

            //You could even pass a Map mapping escape characters, to literal characters to make this even 
            //more general.

        } else if(chars[i] == delimiter) {
            strings.add(sub); //Found delimiter. So we split.
            sub = "";
        } else {
            sub += chars[i]; //nothing special. Just append to current string.
        }
    }

    strings.add(sub); //end of string is a boundary. Must include.

    return strings.toArray(new String[strings.size()]);
}

更新:我现在对问题有点困惑了。就我一直以来所知道的分割而言,它并不包括分隔符(但看起来你的例子是包括的)。如果你希望分隔符存在于数组中,且占据自己的位置,则对此进行修改相当简单。(我将其留给读者作为代码可维护性的证明)


这也让我感到惊讶。Regex.Split可以,而String.Split不行。有点奇怪。在我的情况下,我不需要*。 - Troels Larsen
@TroelsLarsen,这个解决方案对你来说有意义吗? - Cruncher
@TroelsLarsen 转义序列的一般思想是,如果您想要实际转义字符,则需要对其进行转义。在这种情况下仍然有效。无论如何,这很容易编辑。 - Cruncher
是的,如果其他方法都失败了,它们可以重新引入。而且你无法知道我需要这个字符串做什么。谢谢你的建议! - Troels Larsen
1
@TroelsLarsen regex split的作用是因为你将*放在括号中间。否则你就得不到它。我以为你需要期望输出中的星号。如果你实际上不需要它,那么我的正则表达式甚至会更短! - Jerry
显示剩余2条评论

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