首先让我们看一下子表达式,以匹配可能包含转义单引号的单引号字符串。如果你要允许转义单引号,最好也允许转义转义(这就是Douglas Leeder的答案所做的)。但只要你在做它,它同样容易允许转义任何其他字符。有了这些要求。MizardX是唯一一个得到正确表达式的人。这里有两种格式,长格式和短格式(我已经获得了这个自由,用大量的描述性注释写成了VERBOSE
模式-对于非平凡的正则表达式,你应该总是这样做):
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"
好的,现在让我们开始改进这个内容。首先,备选项的顺序很重要,应该始终将最可能的备选项放在第一位。在这种情况下,非转义字符比转义字符更有可能出现,因此反转顺序将稍微提高正则表达式的效率,如下所示:
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
中:
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 提供非常酷的 (?<!\\)(?:\\\\)*
表达式。每天都能学到新东西!