处理转义字符的正则表达式,例如字符串文字。

9

我希望能够匹配带转义引号的字符串字面量。 例如,我想搜索"this is a 'test with escaped\' values' ok"并正确识别反斜杠作为转义字符。我尝试了以下解决方案:

import re
regexc = re.compile(r"\'(.*?)(?<!\\)\'")
match = regexc.search(r""" Example: 'Foo \' Bar'  End. """)
print match.groups() 
# I want ("Foo \' Bar") to be printed above

看完这个问题后,发现有一个简单的问题,就是所使用的转义字符“\”本身无法被转义。我不知道该怎么做。我想要像下面这样的解决方案,但是负回顾断言需要固定长度:
# ...
re.compile(r"\'(.*?)(?<!\\(\\\\)*)\'")
# ...

有没有正则表达式专家能够解决这个问题?谢谢。
6个回答

16

re_single_quote = r"'[^'\\]*(?:\\.[^'\\]*)*'"

首先注意,MizardX的答案是100%准确的。我想补充一些关于效率的额外建议。其次,我想指出这个问题早就被解决和优化了-参见:精通正则表达式(第3版),(它详细介绍了这个特定问题-强烈推荐)。

首先让我们看一下子表达式,以匹配可能包含转义单引号的单引号字符串。如果你要允许转义单引号,最好也允许转义转义(这就是Douglas Leeder的答案所做的)。但只要你在做它,它同样容易允许转义任何其他字符。有了这些要求。MizardX是唯一一个得到正确表达式的人。这里有两种格式,长格式和短格式(我已经获得了这个自由,用大量的描述性注释写成了VERBOSE模式-对于非平凡的正则表达式,你应该总是这样做):

# MizardX's correct regex to match single quoted string:
re_sq_short = r"'((?:\\.|[^\\'])*)'"
re_sq_long = r"""
    '           # Literal opening quote
    (           # Capture group $1: Contents.
      (?:       # Group for contents alternatives
        \\.     # Either escaped anything
      | [^\\']  # or one non-quote, non-escape.
      )*        # Zero or more contents alternatives.
    )           # End $1: Contents.
    '
    """

这个代码可以正确匹配以下所有字符串测试用例:

text01 = r"out1 'escaped-escape:        \\ ' out2"
test02 = r"out1 'escaped-quote:         \' ' out2"
test03 = r"out1 'escaped-anything:      \X ' out2"
test04 = r"out1 'two escaped escapes: \\\\ ' out2"
test05 = r"out1 'escaped-quote at end:   \'' out2"
test06 = r"out1 'escaped-escape at end:  \\' out2"

好的,现在让我们开始改进这个内容。首先,备选项的顺序很重要,应该始终将最可能的备选项放在第一位。在这种情况下,非转义字符比转义字符更有可能出现,因此反转顺序将稍微提高正则表达式的效率,如下所示:

# Better regex to match single quoted string:
re_sq_short = r"'((?:[^\\']|\\.)*)'"
re_sq_long = r"""
    '           # Literal opening quote
    (           # $1: Contents.
      (?:       # Group for contents alternatives
        [^\\']  # Either a non-quote, non-escape,
      | \\.     # or an escaped anything.
      )*        # Zero or more contents alternatives.
    )           # End $1: Contents.
    '
    """

"Unrolling-the-Loop":

这样已经好了一些,但是可以通过应用Jeffrey Friedl的"unrolling-the-loop"效率技术(来自MRE3)进一步改进(显著)。上面的正则表达式不是最优的,因为它必须费力地将星号量词应用于两个替代项的非捕获组,每个替代项一次只消耗一个或两个字符。这种交替可以完全消除,方法是认识到类似的模式一遍又一遍地重复出现,并且可以构造等效的表达式来完成相同的事情而不需要交替。以下是一个优化后的表达式,用于匹配单引号字符串并将其内容捕获到组$1中:

# Better regex to match single quoted string:
re_sq_short = r"'([^'\\]*(?:\\.[^'\\]*)*)'"
re_sq_long = r"""
    '            # Literal opening quote
    (            # $1: Contents.
      [^'\\]*    # {normal*} Zero or more non-', non-escapes.
      (?:        # Group for {(special normal*)*} construct.
        \\.      # {special} Escaped anything.
        [^'\\]*  # More {normal*}.
      )*         # Finish up {(special normal*)*} construct.
    )            # End $1: Contents.
    '
    """

这个表达式会一次性地匹配掉所有非引号、非反斜杠字符(大多数字符串都是如此),从而极大地减少了正则表达式引擎的工作量。你可能会问,这种方法好在哪里?我用RegexBuddy测试了本问题中提到的每个正则表达式,并测量了它们在以下字符串上完成匹配所需的步骤数(所有解决方案均正确匹配): 'This is an example string which contains one \'internally quoted\' string.'
以下是上述测试字符串的基准结果:
r"""
AUTHOR            SINGLE-QUOTE REGEX   STEPS TO: MATCH  NON-MATCH
Evan Fosmark      '(.*?)(?<!\\)'                  374     376
Douglas Leeder    '(([^\\']|\\'|\\\\)*)'          154     444
cletus/PEZ        '((?:\\'|[^'])*)(?<!\\)'        223     527
MizardX           '((?:\\.|[^\\'])*)'             221     369
MizardX(improved) '((?:[^\\']|\\.)*)'             153     369
Jeffrey Friedl    '([^\\']*(?:\\.[^\\']*)*)'       13      19
"""

这些步骤是使用RegexBuddy调试器函数匹配测试字符串所需的步骤数。 "NON-MATCH"列是从测试字符串中删除结束引号时声明匹配失败所需的步骤数。正如您所看到的,对于匹配和非匹配情况,差异都很大。还要注意,这些效率改进仅适用于使用回溯的NFA引擎(即Perl、PHP、Java、Python、Javascript、.NET、Ruby和大多数其他引擎)。DFA引擎不会通过此技术获得任何性能提升(参见:Regular Expression Matching Can Be Simple And Fast)。
完整解决方案如下:
原始问题的目标(我的解释)是从较大的字符串中挑选出单引号子字符串(可能包含转义引号)。如果已知引号外的文本永远不会包含转义单引号,则上面的正则表达式将完成工作。但是,为了在充满转义引号、转义转义和转义任何其他内容的文本海洋中正确匹配单引号子字符串(这是我对作者的理解),需要从字符串开头进行解析。不,(这是我最初的想法),但实际上不需要-可以使用MizardX非常聪明的(?<!\\)(?:\\\\)*表达式来实现。以下是一些测试字符串,用于练习各种解决方案:
text01 = r"out1 'escaped-escape:        \\ ' out2"
test02 = r"out1 'escaped-quote:         \' ' out2"
test03 = r"out1 'escaped-anything:      \X ' out2"
test04 = r"out1 'two escaped escapes: \\\\ ' out2"
test05 = r"out1 'escaped-quote at end:   \'' out2"
test06 = r"out1 'escaped-escape at end:  \\' out2"
test07 = r"out1           'str1' out2 'str2' out2"
test08 = r"out1 \'        'str1' out2 'str2' out2"
test09 = r"out1 \\\'      'str1' out2 'str2' out2"
test10 = r"out1 \\        'str1' out2 'str2' out2"
test11 = r"out1 \\\\      'str1' out2 'str2' out2"
test12 = r"out1         \\'str1' out2 'str2' out2"
test13 = r"out1       \\\\'str1' out2 'str2' out2"
test14 = r"out1           'str1''str2''str3' out2"

给定这个测试数据,让我们看看各种解决方案的表现('p'表示通过,'XX'表示失败):

r"""
AUTHOR/REGEX     01  02  03  04  05  06  07  08  09  10  11  12  13  14
Douglas Leeder    p   p  XX   p   p   p   p   p   p   p   p  XX  XX  XX
  r"(?:^|[^\\])'(([^\\']|\\'|\\\\)*)'"
cletus/PEZ        p   p   p   p   p  XX   p   p   p   p   p  XX  XX  XX
  r"(?<!\\)'((?:\\'|[^'])*)(?<!\\)'"
MizardX           p   p   p   p   p   p   p   p   p   p   p   p   p   p
  r"(?<!\\)(?:\\\\)*'((?:\\.|[^\\'])*)'"
ridgerunner       p   p   p   p   p   p   p   p   p   p   p   p   p   p
  r"(?<!\\)(?:\\\\)*'([^'\\]*(?:\\.[^'\\]*)*)'"
"""

一个工作的测试脚本:
import re
data_list = [
    r"out1 'escaped-escape:        \\ ' out2",
    r"out1 'escaped-quote:         \' ' out2",
    r"out1 'escaped-anything:      \X ' out2",
    r"out1 'two escaped escapes: \\\\ ' out2",
    r"out1 'escaped-quote at end:   \'' out2",
    r"out1 'escaped-escape at end:  \\' out2",
    r"out1           'str1' out2 'str2' out2",
    r"out1 \'        'str1' out2 'str2' out2",
    r"out1 \\\'      'str1' out2 'str2' out2",
    r"out1 \\        'str1' out2 'str2' out2",
    r"out1 \\\\      'str1' out2 'str2' out2",
    r"out1         \\'str1' out2 'str2' out2",
    r"out1       \\\\'str1' out2 'str2' out2",
    r"out1           'str1''str2''str3' out2",
    ]

regex = re.compile(
    r"""(?<!\\)(?:\\\\)*'([^'\\]*(?:\\.[^'\\]*)*)'""",
    re.DOTALL)

data_cnt = 0
for data in data_list:
    data_cnt += 1
    print ("\nData string %d" % (data_cnt))
    m_cnt = 0
    for match in regex.finditer(data):
        m_cnt += 1
        if (match.group(1)):
            print("  quoted sub-string%3d = \"%s\"" %
                (m_cnt, match.group(1)))

哎呀!

p.s. 感谢 MizardX 提供非常酷的 (?<!\\)(?:\\\\)* 表达式。每天都能学到新东西!


+1. 我从来没有想过修改的代价会这么高。你测试过初始的look-behind速度吗? - Markus Jarderot
@MizardX:不,我没有测量任何速度,但如果您需要优化生产正则表达式,则基准测试非常重要。构建既准确又高效的正则表达式是MRE3的重点。它讨论了基准测试,并详细介绍了如何利用内部正则表达式引擎优化来真正打造一个伟大的正则表达式。这是我读过的最有用的书籍。 - ridgerunner
你能告诉我书中讨论这部分的章节吗? - Peter Schorn

5
我想这个会起作用:
import re
regexc = re.compile(r"(?:^|[^\\])'(([^\\']|\\'|\\\\)*)'")

def check(test, base, target):
    match = regexc.search(base)
    assert match is not None, test+": regex didn't match for "+base
    assert match.group(1) == target, test+": "+target+" not found in "+base
    print "test %s passed"%test

check("Empty","''","")
check("single escape1", r""" Example: 'Foo \' Bar'  End. """,r"Foo \' Bar")
check("single escape2", r"""'\''""",r"\'")
check("double escape",r""" Example2: 'Foo \\' End. """,r"Foo \\")
check("First quote escaped",r"not matched\''a'","a")
check("First quote escaped beginning",r"\''a'","a")

正则表达式r"(?:^|[^\\])'(([^\\']|\\'|\\\\)*)'"仅向前匹配我们想要的字符串内容:
  1. 不是反斜杠或引号的字符。
  2. 转义引号
  3. 转义反斜杠

编辑:

在前面添加额外的正则表达式以检查第一个引号是否被转义。


当遇到第一个转义引号(即 ')时,-1 不起作用。 - cletus
它只允许引号和反斜杠进行转义。 - Markus Jarderot
MixardX,这就是我一直在寻找的。而且这种模式似乎足够灵活,如果我决定添加更多可转义字符,也可以轻松实现。 - Evan Fosmark
我认为如果你开始逃避大量字符,那么是时候考虑一个合适的解析器了。我不想维护一个更复杂的正则表达式。 - Douglas Leeder

3
道格拉斯·利德的模式((?:^|[^\\])'(([^\\']|\\'|\\\\)*)')不能匹配"test 'test \x3F test' test""test \\'test' test"。(包含引号和反斜杠以外转义字符的字符串,以及以转义反斜杠开头的字符串)
克莱图斯的模式((?<!\\)'((?:\\'|[^'])*)(?<!\\)')不能匹配"test 'test\\' test"。(以转义反斜杠结尾的字符串)
我对单引号字符串的建议是:
(?<!\\)(?:\\\\)*'((?:\\.|[^\\'])*)'

对于单引号或双引号的字符串,您可以使用以下方法:

(?<!\\)(?:\\\\)*("|')((?:\\.|(?!\1)[^\\])*)\1

使用Python进行测试运行:

Doublas Leeder´s test cases:
"''" matched successfully: ""
" Example: 'Foo \' Bar'  End. " matched successfully: "Foo \' Bar"
"'\''" matched successfully: "\'"
" Example2: 'Foo \\' End. " matched successfully: "Foo \\"
"not matched\''a'" matched successfully: "a"
"\''a'" matched successfully: "a"

cletus´ test cases:
"'testing 123'" matched successfully: "testing 123"
"'testing 123\\'" matched successfully: "testing 123\\"
"'testing 123" didn´t match, as exected.
"blah 'testing 123" didn´t match, as exected.
"blah 'testing 123'" matched successfully: "testing 123"
"blah 'testing 123' foo" matched successfully: "testing 123"
"this 'is a \' test'" matched successfully: "is a \' test"
"another \' test 'testing \' 123' \' blah" matched successfully: "testing \' 123"

MizardX´s test cases:
"test 'test \x3F test' test" matched successfully: "test \x3F test"
"test \\'test' test" matched successfully: "test"
"test 'test\\' test" matched successfully: "test\\"

当我有多个转义字符时,它是否仍然有效?例如,“示例'foo \\' bar'”,其中应该使用两个转义字符获取foo。 - Evan Fosmark
是的,它可以使用多个转义字符,在初始引号和结束引号之前。 - Markus Jarderot
+1 - 非常好的回答。但请注意,这可以加快速度。有关详细信息,请参见我的答案。并且使用(?<!\\)(?:\\\\)*表达式做得很好! - ridgerunner

1

如果我理解你的意思(我不确定),你想在字符串中查找引用的字符串,忽略转义的引号。是这样吗?如果是的话,请尝试以下方法:

/(?<!\\)'((?:\\'|[^'])*)(?<!\\)'/

基本上:

  • 以单引号开头,不得以反斜杠为前缀;
  • 匹配零个或多个出现次数:反斜杠,然后是引号或任何不是引号的字符;
  • 最后必须以引号结束;
  • 不要将中间的括号分组(使用 ?: 运算符);
  • 关闭引号前不能有反斜杠。

好的,我已在Java中进行了测试(抱歉这是我的强项而非Python,但原理相同):

private final static String TESTS[] = {
        "'testing 123'",
        "'testing 123\\'",
        "'testing 123",
        "blah 'testing 123",
        "blah 'testing 123'",
        "blah 'testing 123' foo",
        "this 'is a \\' test'",
        "another \\' test 'testing \\' 123' \\' blah"
};

public static void main(String args[]) {
    Pattern p = Pattern.compile("(?<!\\\\)'((?:\\\\'|[^'])*)(?<!\\\\)'");
    for (String test : TESTS) {
        Matcher m = p.matcher(test);
        if (m.find()) {
            System.out.printf("%s => %s%n", test, m.group(1));
        } else {
            System.out.printf("%s doesn't match%n", test);
        }
    }
}

结果:

'testing 123' => testing 123
'testing 123\' doesn't match
'testing 123 doesn't match
blah 'testing 123 doesn't match
blah 'testing 123' => testing 123
blah 'testing 123' foo => testing 123
this 'is a \' test' => is a \' test
another \' test 'testing \' 123' \' blah => testing \' 123

看起来是正确的。


我找到了一些相似的东西,只是我忘记检查转义的初始引号... 我不知道 (?>! 虽然。你是指 (?<! 还是其他我不知道的结构? - PhiLho
还不错,但在第一个引号之前有转义反斜杠的情况下会失败。 - Evan Fosmark
最后一个测试用例有一个初始转义引号。你能给我一个例子吗? - cletus

0

使用 Python 的 re.findall() 函数和 cletus 的表达式:

re.findall(r"(?<!\\)'((?:\\'|[^'])*)(?<!\\)'", s)

在字符串中查找多个匹配项的测试:

>>> re.findall(r"(?<!\\)'((?:\\'|[^'])*)(?<!\\)'",
 r"\''foo bar gazonk' foo 'bar' gazonk 'foo \'bar\' gazonk' 'gazonk bar foo\'")
['foo bar gazonk', 'bar', "foo \\'bar\\' gazonk"]
>>>

使用Cletus的TESTS字符串数组:

["%s => %s" % (s, re.findall(r"(?<!\\)'((?:\\'|[^'])*)(?<!\\)'", s)) for s in TESTS]

非常好用。(你可以自己试试,也可以相信我的话。)


实际上不是这样的。正如Evan所指出的那样,在第一个引号之前有转义的反斜杠时,它会失败。 - ridgerunner

0
>>> print re.findall(r"('([^'\\]|\\'|\\\\)*')",r""" Example: 'Foo \' Bar'  End. """)[0][0]

'Foo \' Bar'


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