有没有一个正则表达式可以检测一个有效的正则表达式?

1156

是否可能使用另一个正则表达式检测有效的正则表达式?如果可以,请在下面提供示例代码。


380
谁验证验证正则表达式? - bevacqua
17
@Nico社区。 - Janusz Lenar
80
您的问题是验证一个正则表达式,您选择使用正则表达式来解决它。我想知道正则表达式的问题编号增加属性是加法还是乘法。感觉像是4个问题而不是2个 :) - abesto
19
正则表达式有许多符号表示法,其中一些特性和它们的拼写方式大多数是相同的,而有些则不同或仅适用于某种符号表示法。大多数这些符号表示法并不是在正则语法意义上“规则”的——你需要一个上下文自由解析器来处理子表达式的无限嵌套——尽管许多现代“正则表达式”符号表示法都有超越原始形式定义的扩展,并且可能允许识别它们自己的符号表示法。总之,为什么不直接询问您的正则表达式库每个正则表达式是否有效呢? - user180247
8
使用你所用的编程语言中的异常处理机制,实际编译/运行正则表达式(模式)以进行检查。因此,编程语言的正则表达式引擎/编译器本身将对其进行检查。这假设基本语法正确,以便程序可以运行,但可以通过使用您的语言工具来将字符串作为(可能有语法错误的)代码进行评估,或者类似方法来包含在检查中。 - zdim
显示剩余3条评论
10个回答

1069
/
^                                             # start of string
(                                             # first group start
  (?:
    (?:[^?+*{}()[\]\\|]+                      # literals and ^, $
     | \\.                                    # escaped characters
     | \[ (?: \^?\\. | \^[^\\] | [^\\^] )     # character classes
          (?: [^\]\\]+ | \\. )* \]
     | \( (?:\?[:=!]|\?<[=!]|\?>)? (?1)?? \)  # parenthesis, with recursive content
     | \(\? (?:R|[+-]?\d+) \)                 # recursive matching
     )
    (?: (?:[?+*]|\{\d+(?:,\d*)?\}) [?+]? )?   # quantifiers
  | \|                                        # alternative
  )*                                          # repeat content
)                                             # end first group
$                                             # end of string
/

这是一个递归的正则表达式,在许多正则表达式引擎中不被支持。基于PCRE的引擎应该支持它。
不带空格和注释的表达式:
/^((?:(?:[^?+*{}()[\]\\|]+|\\.|\[(?:\^?\\.|\^[^\\]|[^\\^])(?:[^\]\\]+|\\.)*\]|\((?:\?[:=!]|\?<[=!]|\?>)?(?1)??\)|\(\?(?:R|[+-]?\d+)\))(?:(?:[?+*]|\{\d+(?:,\d*)?\})[?+]?)?|\|)*)$/

.NET不直接支持递归。(使用(?1)(?R)结构。) 递归必须转换为计算平衡组:

^                                         # start of string
(?:
  (?: [^?+*{}()[\]\\|]+                   # literals and ^, $
   | \\.                                  # escaped characters
   | \[ (?: \^?\\. | \^[^\\] | [^\\^] )   # character classes
        (?: [^\]\\]+ | \\. )* \]
   | \( (?:\?[:=!]
         | \?<[=!]
         | \?>
         | \?<[^\W\d]\w*>
         | \?'[^\W\d]\w*'
         )?                               # opening of group
     (?<N>)                               #   increment counter
   | \)                                   # closing of group
     (?<-N>)                              #   decrement counter
   )
  (?: (?:[?+*]|\{\d+(?:,\d*)?\}) [?+]? )? # quantifiers
| \|                                      # alternative
)*                                        # repeat content
$                                         # end of string
(?(N)(?!))                                # fail if counter is non-zero.

压缩:

^(?:(?:[^?+*{}()[\]\\|]+|\\.|\[(?:\^?\\.|\^[^\\]|[^\\^])(?:[^\]\\]+|\\.)*\]|\((?:\?[:=!]|\?<[=!]|\?>|\?<[^\W\d]\w*>|\?'[^\W\d]\w*')?(?<N>)|\)(?<-N>))(?:(?:[?+*]|\{\d+(?:,\d*)?\})[?+]?)?|\|)*$(?(N)(?!))

从评论中:

这个会验证替换和翻译吗?

它只会验证替换和翻译的正则表达式部分。 s/<this part>/.../

使用正则表达式匹配所有有效的正则语法在理论上是不可能的。

如果正则表达式引擎支持递归(例如PCRE),那么这是可能的,但这不能再称为正则表达式了。

事实上,“递归正则表达式”不是正则表达式。但是,这是常用的正则表达式引擎扩展...具有讽刺意味的是,这种扩展正则表达式无法匹配扩展正则表达式。

“在理论上,理论和实践是相同的。在实践中,它们并不相同。”几乎所有了解正则表达式的人都知道正则表达式不支持递归。但是,PCRE和大多数其他实现支持比基本正则表达式更多的功能。

在grep命令中使用此模式的shell脚本时,它显示一些错误.. grep:{}的内容无效。我正在编写一个脚本,可以对代码库进行grep,以查找包含正则表达式的所有文件

此模式利用了称为递归正则表达式的扩展。这不受POSIX正则表达式支持。您可以尝试使用-P开关启用PCRE正则表达式风格。

正则表达式本身“不是正则语言,因此无法通过正则表达式解析...”

这对于经典正则表达式来说是正确的。一些现代实现允许递归,这使其成为上下文自由语言,尽管对于这个任务而言有些冗长。

我看到你匹配了[]()/\等特殊正则表达式字符。你在哪里允许非特殊字符?它似乎会匹配^(?:[\.]+)$,但不会匹配^abcdefg$。那是一个有效的正则表达式。

[^?+*{}()[\]\\|]将匹配任何单个字符,不属于其他结构之一。这包括文字(a - z)和某些特殊字符(^$.)。


21
这个回答会让人们产生完全错误的方向。他们不应该使用正则表达式来定位正则表达式,因为这种方法在所有情况下都不能正确地工作。请查看我添加的答案。 - vitaly-t
3
.{,1} 无法匹配。请更改为 ^((?:(?:[^?+*{}()[\]\\|]+|\\.|\[(?:\^?\\.|\^[^\\]|[^\\^])(?:[^\]\\]+|\\.)*\]|\((?:\?[:=!]|\?<[=!]|\?>)?(?1)??\)|\(\?(?:R|[+-]?\d+)\))(?:(?:[?+*]|\{\d*(?:,\d*)?\})[?+]?)?|\|)*)$ 可以匹配。将 \d+ 更改为 \d* - yunzen
7
正则表达式本身不应该具有递归,至少在您的回答中应该这样说。您的正则表达式引擎可能“过于强大”,并不是真正的正则表达式引擎。 - Charlie Parker
2
@vitaly-t:你的答案在哪里? - Peter V. Mørch
3
@PeterV.Mørch 我删除了它,因为很奇怪地,它收到了很多踩。这里,我将我的回答复制到了Gist中 - vitaly-t
显示剩余3条评论

354

不太可能。

在你的语言中使用 try..catch 或其他提供的方法进行评估。


4
但是,如果从用户处接收到一个值,他会得到许多机会来利用正则表达式引擎中的某些漏洞。 - SerG
2
通过另一个正则表达式验证正则表达式本身,对于这种正则表达式注入攻击是没有影响的。 - jsbueno

245
不包括某些实际上是上下文无关语法的正则表达式实现,如果您严格来说是关于正则表达式而不是解释某些正则表达式实现,那么是的,有一个限制使得不可能编写一个可以匹配所有正则表达式的正则表达式。您不能匹配成对的大括号等实现。正则表达式使用许多这样的构造,让我们以[]为例。每当有一个[时,必须有一个匹配的],这对正则表达式"\[.*\]"来说非常简单。
正则表达式无法匹配嵌套的括号,这是使其不可能的原因。你如何编写一个可以匹配嵌套括号的正则表达式呢?答案是你不能,除非使用一个无限长的正则表达式。虽然可以通过蛮力方法匹配任意数量的嵌套圆括号,但永远无法匹配任意长度的嵌套括号集合。
这种能力通常被称为计数,因为你正在计算嵌套的深度。根据定义,正则表达式没有计数的能力。
我最终写了 "Regular Expression Limitations" 关于这个问题。

1
你是否曾经编写过递归正则表达式的代码片段,这个片段在你上面提到的文章中被引用了(“在不久的将来,我将探索.Net正则表达式语言的递归扩展。”)? - Vérace
1
正如@Markus Jarderot在他的答案中指出的那样,使用递归正则表达式。或者使用正则表达式注释括号的嵌套级别,然后使用一个正则表达式进行递归函数调用以匹配成对的括号,并解析/验证所涉及的正则表达式--请参见https://stackoverflow.com/a/66130030/7475450 - Peter Thoeny

62

好问题。

真正的正则表达式无法决定任意深度的嵌套括号。如果您的字母表包含 '('')',则目标是确定这些字符的字符串是否具有匹配的括号。由于这是正则表达式的必要要求,因此答案是否定的。

但是,如果您放宽要求并添加递归,则可能可以做到这一点。原因是递归可以充当堆栈,让您通过将其推入此堆栈来“计数”当前嵌套深度。

Russ Cox 写了《正则表达式匹配可以简单快速》,这是一篇关于正则表达式引擎实现的精彩论文。


没错。您可以使用正则表达式将括号/圆括号注释为嵌套级别,接着使用递归函数调用带有匹配对的正则表达式,并在 https://stackoverflow.com/a/66130030/7475450 中查看有关解决/验证所涉及的正则表达式的详细信息。 - Peter Thoeny

22
不可以,如果使用标准正则表达式的话。这是因为你无法满足正则语言的泵引理。泵引理指出,如果存在一个数字"N",使得将字符串分成三个子串xyz,其中|x|>=1 && |xy|<=N,那么该字符串属于语言"L"。你可以重复y任意次数,整个字符串仍然属于L。泵引理的一个结果是,你不能有形如a^Nb^Mc^N的正则字符串,即两个长度相同的子串由另一个字符串分隔开。无论你如何将这样的字符串分割成xyz,你都无法“泵动”y,而不改变"a"和"c"的数量,从而离开原来的语言。例如,在正则表达式中的括号就是这种情况。

8
那不是对 Pumping Lemma 很精确的描述。首先,它是针对整个语言是否正则,而不是单个字符串。第二,这只是判断正则性的必要条件,而非充分条件。最后,只有足够长的字符串可以被“泵动”。 - darij grinberg

16
尽管像MizardX发布的递归正则表达式是完全可行的,但对于这种情况来说,使用解析器更加有用。正则表达式最初是用于处理正则语言的,递归或具有平衡组只是一种修补程序。
定义有效正则表达式的语言实际上是一个上下文无关文法,并且您应该使用适当的解析器来处理它。这是一个用于解析简单正则表达式(不包含大多数结构)的大学项目示例。它使用JavaCC。是的,注释是用西班牙语编写的,但方法名称非常自说明。
SKIP :
{
    " "
|   "\r"
|   "\t"
|   "\n"
}
TOKEN : 
{
    < DIGITO: ["0" - "9"] >
|   < MAYUSCULA: ["A" - "Z"] >
|   < MINUSCULA: ["a" - "z"] >
|   < LAMBDA: "LAMBDA" >
|   < VACIO: "VACIO" >
}

IRegularExpression Expression() :
{
    IRegularExpression r; 
}
{
    r=Alternation() { return r; }
}

// Matchea disyunciones: ER | ER
IRegularExpression Alternation() :
{
    IRegularExpression r1 = null, r2 = null; 
}
{
    r1=Concatenation() ( "|" r2=Alternation() )?
    { 
        if (r2 == null) {
            return r1;
        } else {
            return createAlternation(r1,r2);
        } 
    }
}

// Matchea concatenaciones: ER.ER
IRegularExpression Concatenation() :
{
    IRegularExpression r1 = null, r2 = null; 
}
{
    r1=Repetition() ( "." r2=Repetition() { r1 = createConcatenation(r1,r2); } )*
    { return r1; }
}

// Matchea repeticiones: ER*
IRegularExpression Repetition() :
{
    IRegularExpression r; 
}
{
    r=Atom() ( "*" { r = createRepetition(r); } )*
    { return r; }
}

// Matchea regex atomicas: (ER), Terminal, Vacio, Lambda
IRegularExpression Atom() :
{
    String t;
    IRegularExpression r;
}
{
    ( "(" r=Expression() ")" {return r;}) 
    | t=Terminal() { return createTerminal(t); }
    | <LAMBDA> { return createLambda(); }
    | <VACIO> { return createEmpty(); }
}

// Matchea un terminal (digito o minuscula) y devuelve su valor
String Terminal() :
{
    Token t;
}
{
    ( t=<DIGITO> | t=<MINUSCULA> ) { return t.image; }
}

2
对于任何对此感兴趣的非西班牙人来说。Matchea意思是“匹配”,vacio意思是“空”,digito意思是“数字”,miniscula意思是“小写字母”。Matchea disyunciones = 匹配析取。Matchea concatenaciones = 匹配连接。Matchea repeticiones = 匹配重复。Matchea regex atomicas = 匹配原子正则表达式。Matchea一个终端(数字或小写字母)并返回其值。 - Heriberto Lugo
Matchear不是西班牙语,但我明白它的来源... "matches"在正确的西班牙语中应该是"coincide con"(匹配)。 - juandesant

14

您可以将正则表达式提交给 preg_match,如果正则表达式无效,则会返回 false。不要忘记使用@符号来禁止错误消息:

@preg_match($regexToTest, '');
  • 如果正则表达式是//,则返回1。
  • 如果正则表达式没问题,则返回0。
  • 否则返回false。

7
以下是Paul McGuire提供的示例,最初来自pyparsing wiki,但现在仅通过Wayback Machine 链接可用,它给出了一个语法来解析一些正则表达式,以返回匹配字符串的集合。因此,它会拒绝包含无限重复项(如“+”和“*”)的re。但这应该能够让您了解如何构建处理re的解析器的结构。
# 
# invRegex.py
#
# Copyright 2008, Paul McGuire
#
# pyparsing script to expand a regular expression into all possible matching strings
# Supports:
# - {n} and {m,n} repetition, but not unbounded + or * repetition
# - ? optional elements
# - [] character ranges
# - () grouping
# - | alternation
#
__all__ = ["count","invert"]

from pyparsing import (Literal, oneOf, printables, ParserElement, Combine, 
    SkipTo, operatorPrecedence, ParseFatalException, Word, nums, opAssoc,
    Suppress, ParseResults, srange)

class CharacterRangeEmitter(object):
    def __init__(self,chars):
        # remove duplicate chars in character range, but preserve original order
        seen = set()
        self.charset = "".join( seen.add(c) or c for c in chars if c not in seen )
    def __str__(self):
        return '['+self.charset+']'
    def __repr__(self):
        return '['+self.charset+']'
    def makeGenerator(self):
        def genChars():
            for s in self.charset:
                yield s
        return genChars

class OptionalEmitter(object):
    def __init__(self,expr):
        self.expr = expr
    def makeGenerator(self):
        def optionalGen():
            yield ""
            for s in self.expr.makeGenerator()():
                yield s
        return optionalGen

class DotEmitter(object):
    def makeGenerator(self):
        def dotGen():
            for c in printables:
                yield c
        return dotGen

class GroupEmitter(object):
    def __init__(self,exprs):
        self.exprs = ParseResults(exprs)
    def makeGenerator(self):
        def groupGen():
            def recurseList(elist):
                if len(elist)==1:
                    for s in elist[0].makeGenerator()():
                        yield s
                else:
                    for s in elist[0].makeGenerator()():
                        for s2 in recurseList(elist[1:]):
                            yield s + s2
            if self.exprs:
                for s in recurseList(self.exprs):
                    yield s
        return groupGen

class AlternativeEmitter(object):
    def __init__(self,exprs):
        self.exprs = exprs
    def makeGenerator(self):
        def altGen():
            for e in self.exprs:
                for s in e.makeGenerator()():
                    yield s
        return altGen

class LiteralEmitter(object):
    def __init__(self,lit):
        self.lit = lit
    def __str__(self):
        return "Lit:"+self.lit
    def __repr__(self):
        return "Lit:"+self.lit
    def makeGenerator(self):
        def litGen():
            yield self.lit
        return litGen

def handleRange(toks):
    return CharacterRangeEmitter(srange(toks[0]))

def handleRepetition(toks):
    toks=toks[0]
    if toks[1] in "*+":
        raise ParseFatalException("",0,"unbounded repetition operators not supported")
    if toks[1] == "?":
        return OptionalEmitter(toks[0])
    if "count" in toks:
        return GroupEmitter([toks[0]] * int(toks.count))
    if "minCount" in toks:
        mincount = int(toks.minCount)
        maxcount = int(toks.maxCount)
        optcount = maxcount - mincount
        if optcount:
            opt = OptionalEmitter(toks[0])
            for i in range(1,optcount):
                opt = OptionalEmitter(GroupEmitter([toks[0],opt]))
            return GroupEmitter([toks[0]] * mincount + [opt])
        else:
            return [toks[0]] * mincount

def handleLiteral(toks):
    lit = ""
    for t in toks:
        if t[0] == "\\":
            if t[1] == "t":
                lit += '\t'
            else:
                lit += t[1]
        else:
            lit += t
    return LiteralEmitter(lit)    

def handleMacro(toks):
    macroChar = toks[0][1]
    if macroChar == "d":
        return CharacterRangeEmitter("0123456789")
    elif macroChar == "w":
        return CharacterRangeEmitter(srange("[A-Za-z0-9_]"))
    elif macroChar == "s":
        return LiteralEmitter(" ")
    else:
        raise ParseFatalException("",0,"unsupported macro character (" + macroChar + ")")

def handleSequence(toks):
    return GroupEmitter(toks[0])

def handleDot():
    return CharacterRangeEmitter(printables)

def handleAlternative(toks):
    return AlternativeEmitter(toks[0])


_parser = None
def parser():
    global _parser
    if _parser is None:
        ParserElement.setDefaultWhitespaceChars("")
        lbrack,rbrack,lbrace,rbrace,lparen,rparen = map(Literal,"[]{}()")

        reMacro = Combine("\\" + oneOf(list("dws")))
        escapedChar = ~reMacro + Combine("\\" + oneOf(list(printables)))
        reLiteralChar = "".join(c for c in printables if c not in r"\[]{}().*?+|") + " \t"

        reRange = Combine(lbrack + SkipTo(rbrack,ignore=escapedChar) + rbrack)
        reLiteral = ( escapedChar | oneOf(list(reLiteralChar)) )
        reDot = Literal(".")
        repetition = (
            ( lbrace + Word(nums).setResultsName("count") + rbrace ) |
            ( lbrace + Word(nums).setResultsName("minCount")+","+ Word(nums).setResultsName("maxCount") + rbrace ) |
            oneOf(list("*+?")) 
            )

        reRange.setParseAction(handleRange)
        reLiteral.setParseAction(handleLiteral)
        reMacro.setParseAction(handleMacro)
        reDot.setParseAction(handleDot)

        reTerm = ( reLiteral | reRange | reMacro | reDot )
        reExpr = operatorPrecedence( reTerm,
            [
            (repetition, 1, opAssoc.LEFT, handleRepetition),
            (None, 2, opAssoc.LEFT, handleSequence),
            (Suppress('|'), 2, opAssoc.LEFT, handleAlternative),
            ]
            )
        _parser = reExpr

    return _parser

def count(gen):
    """Simple function to count the number of elements returned by a generator."""
    i = 0
    for s in gen:
        i += 1
    return i

def invert(regex):
    """Call this routine as a generator to return all the strings that
       match the input regular expression.
           for s in invert("[A-Z]{3}\d{3}"):
               print s
    """
    invReGenerator = GroupEmitter(parser().parseString(regex)).makeGenerator()
    return invReGenerator()

def main():
    tests = r"""
    [A-EA]
    [A-D]*
    [A-D]{3}
    X[A-C]{3}Y
    X[A-C]{3}\(
    X\d
    foobar\d\d
    foobar{2}
    foobar{2,9}
    fooba[rz]{2}
    (foobar){2}
    ([01]\d)|(2[0-5])
    ([01]\d\d)|(2[0-4]\d)|(25[0-5])
    [A-C]{1,2}
    [A-C]{0,3}
    [A-C]\s[A-C]\s[A-C]
    [A-C]\s?[A-C][A-C]
    [A-C]\s([A-C][A-C])
    [A-C]\s([A-C][A-C])?
    [A-C]{2}\d{2}
    @|TH[12]
    @(@|TH[12])?
    @(@|TH[12]|AL[12]|SP[123]|TB(1[0-9]?|20?|[3-9]))?
    @(@|TH[12]|AL[12]|SP[123]|TB(1[0-9]?|20?|[3-9])|OH(1[0-9]?|2[0-9]?|30?|[4-9]))?
    (([ECMP]|HA|AK)[SD]|HS)T
    [A-CV]{2}
    A[cglmrstu]|B[aehikr]?|C[adeflmorsu]?|D[bsy]|E[rsu]|F[emr]?|G[ade]|H[efgos]?|I[nr]?|Kr?|L[airu]|M[dgnot]|N[abdeiop]?|Os?|P[abdmortu]?|R[abefghnu]|S[bcegimnr]?|T[abcehilm]|Uu[bhopqst]|U|V|W|Xe|Yb?|Z[nr]
    (a|b)|(x|y)
    (a|b) (x|y)
    """.split('\n')

    for t in tests:
        t = t.strip()
        if not t: continue
        print '-'*50
        print t
        try:
            print count(invert(t))
            for s in invert(t):
                print s
        except ParseFatalException,pfe:
            print pfe.msg
            print
            continue
        print

if __name__ == "__main__":
    main()

4

Javascript中:

SyntaxError

在传递无效的正则表达式进行评估时,会抛出此异常。

// VALID ONE
> /yes[^]*day/
Out: /yes[^]*day/

// INVALID ONE
> /yes[^*day/
Out: VM227:1 Uncaught SyntaxError: Invalid regular expression: missing /

这是用于检查正则表达式字符串是否有效的函数:

步骤1: 正则表达式解析器

var RegexParser = function(input) {

    // Parse input
    var m = input.match(/(\/?)(.+)\1([a-z]*)/i);

    // Invalid flags
    if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
        return RegExp(input);
    }

    // Create the regular expression
    return new RegExp(m[2], m[3]);
};

步骤2:使用解析器

var RegexString = "/yes.*day/"

var isRegexValid = input => {
 try {
 const regex = RegexParser(input);
 }
 catch(error) {
   if(error.name === "SyntaxError") 
    {
      return false;
    }
    else 
    {
     throw error;
    }
 }
 return true;
}


0

使用此版本,您可以使用PHP检查正则表达式字符串 - 我从上面的示例中进行了修改:

$re = '/((?:(?:[^?+*{}()[\]\\\\|]+|\\\\.|\[(?:\^?\\\\.|\^[^\\\\]|[^\\\\^])(?:[^\]\\\\]+|\\\\.)*\]|\((?:\?[:=!]|\?<[=!]|\?>)??\)|\(\?(?:R|[+-]?\d+)\))(?:(?:[?+*]|\{\d*(?:,\d*)?\})[?+]?)?|\|)*)/';
$str = '[0-9]{1,}[a-z]';

preg_match($re, $str, $matches, PREG_OFFSET_CAPTURE, 0);

$length = strlen($str);
$length2 = strlen($matches[0][0]);

if($length == $length2) {

   echo "is regex";

} else {

   echo "is no regex";

}

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