65.9K
CodeProject 正在变化。 阅读更多。
Home

一个用于分解正则表达式的工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (7投票s)

2024年9月2日

CPOL

11分钟阅读

viewsIcon

9610

downloadIcon

183

一个用于将正则表达式分解为基本组件的 JavaScript 函数。

引言

对于那些精通常规表达式基础知识的人来说,提升技能可以极大地提高他们精确高效地分析和操作字符串的能力。正则表达式虽然强大,但由于其感知到的复杂性而经常未被充分利用。然而,掌握高级正则表达式技术可以为解决复杂的编程挑战开辟许多可能性。
通过高级量词、环视和原子组扩展您的技能,可以完善您的模式匹配。例如,使用惰性量词(*? +?)可以实现最小匹配,这在抓取任务或处理大型数据集时至关重要,可以防止过多的回溯。先行断言和后行断言(?=...)、(?<=...)提供条件匹配能力,而不消耗字符,使您能够构建既强大又高效的正则表达式。
高级分组技术,包括非捕获组(?:...)和命名捕获组(?...),增强了正则表达式模式的组织性和可读性。命名捕获组尤其有助于维护复杂模式并提高代码清晰度,尤其是在正则表达式用于大规模搜索和替换操作或作为更大编程结构的一部分时。通过掌握这些技术,您可以在编码任务中感觉更有条理和高效。
了解各种正则表达式构造的性能影响可以带来更优化、执行更快的模式。诸如字符类交集和减法以及使用锚点和单词边界等技术可确保您的正则表达式高效运行。此外,探索特定于引擎的优化和功能可以提供显著的性能优势。
正则表达式在不同的编程环境中实现略有不同,这可能会影响它们的行为和效率。随时了解以下之间的差异
Python、JavaScript 和 PHP 等语言以及 grep 和 sed 等工具中的正则表达式实现可以帮助您避免常见陷阱并在各种上下文中充分利用正则表达式的强大功能。
正则表达式是强大的模式匹配和文本操作工具,广泛用于不同的编程环境。然而,理解复杂的正则表达式需要时间和精力。decomposingRegex() 函数旨在通过将任何正则表达式分解为其组成部分来揭示其奥秘,从而使其更易于理解、调试和优化。
 

更改日志

2024年10月9日。将 JavaScript 函数 parseRegex() 的名称更改为 decomposingRegex()。添加了下载按钮。

2024年9月16日 修正了一些小错误并重组了一些部分。

目录

  • 摘要
  • 引言
  • 正则表达式分解的核心概念
  • 深入了解 decomposingRegex()
  • 代码解释
  • decomposingRegex() 函数:详细解释
  • 摘要
  • 实际示例
  • 结论
  • 有用的在线工具。
  • 1. Regex101
  • 2. RegExr
  • 3. RegexBuddy
  • 4. Regex Pal
  • 5. Regex Tester
  • 6. Regular expression tester
  • 参考

 

 

正则表达式分解的核心概念

正则表达式分解涉及将正则表达式分解为基本元素和结构,例如文字字符、字符类、量词和组。此过程对于必须调试或修改复杂正则表达式模式的开发人员至关重要。它阐明了正则表达式的每个部分的作用以及它如何影响匹配行为。由于正则表达式语法复杂且并非总是有逻辑,因此通过首先建立语法的正式 BNF 描述,它对我有所帮助。

正则表达式的 BNF 语法

<regexp> ::= "/" <pattern> "/" [<flags>]
<pattern> ::= <concatenation> ("|" <concatenation>)*
<concatenation> ::= <element>*
<element> ::= <group> 
            | <character-class> 
            | <quantified> 
            | <anchor> 
            | <escaped-character> 
            | <character>
<group> ::= "(" <pattern> ")" 
          | "(?<identifier>" <pattern> ")" 
          | "(?:" <pattern> ")" 
          | "(?=" <pattern> ")" 
          | "(?!" <pattern> ")" 
          | "(?<=" <pattern> ")" 
          | "(?<!" <pattern> ")"
<character-class> ::= "[" [ "^" ] <character-class-body> "]"
<character-class-body> ::= <character-range> | <character>*
<character-range> ::= <character> "-" <character>
<quantified> ::= <element> <quantifier> [ <lazy-modifier> ]   // Separating quantifier and lazy modifier
<quantifier> ::= "*" | "+" | "?" | "{" <digits> [ "," <digits> ] "}"
<lazy-modifier> ::= "?"                                      // Lazy quantifier modifier
<anchor> ::= "^" | "$" | "\b" | "\B"
<escaped-character> ::= "\\" <character>
<character> ::= any single character except special characters 
               | <escaped-character>
<identifier> ::= <letter> (<letter> | <digit> | "_")*
<flags> ::= <flag>*
<flag> ::= "g" | "i" | "m" | "s" | "u" | "y"

现在更容易看出正则表达式如何分解为更简单的组件。我开发的用于分解的 JavaScript 函数是 decomposingRegex()。

分解的主要组包括

  • 转义字符是那些前面带有反斜杠 \ 的字符,它会改变其后字符的通常含义。例如,\n 表示换行,\d 匹配任何数字。它们用于在正则表达式模式中包含特殊字符作为文字字符,或表示特殊的正则表达式函数(例如 \b 表示单词边界)。
  • 字符集,用方括号 [] 括起来,匹配一组字符中的任何一个字符。例如,[a-z] 匹配任何小写字母,[^a-z] 匹配任何不包含小写字母的字符。它们通过启用从定义集中匹配字符来使正则表达式更灵活和简洁,从而增强字符串中的模式匹配能力。
  • 量词决定了先行元素(如字符、组或字符类)的多少个实例才算匹配。标准量词包括 *(0 个或更多)、+(1 个或更多)和 ?(0 个或 1 个)。量词对于指定字符串中与先行元素匹配的出现次数至关重要,允许正则表达式模式匹配不同的文本长度。在其核心形式中,量词是贪婪的,这意味着它们尝试消耗尽可能多的输入进行匹配。它的对应物,惰性,意味着它将消耗最少的输入来满足匹配。惰性通过在前面提到的量词后面添加额外的 ? 来表示。
  • 大括号用作量词,用于指定模式元素必须出现的次数。它们可以定义固定的次数 {n}、范围 {n,m} 或模式必须出现的最小次数 {n,}。这种量词有助于匹配模式中精确的重复次数,从而精确控制组件的出现。这在匹配特定数据格式(如日期、序列号或代码部分)时特别有用。简单量词也可以用大括号表示为:* 为 {0,},+ 为 {1,},? 为 {0,1}。
  • 交替,由管道符号 | 表示,作用类似于布尔 OR。它匹配 | 之前或之后的模式。例如,cat|dog 匹配 cat 或 dog。交替允许匹配几个可能的部分之一,使其值得在单个正则表达式模式中包含多个选项,从而增强灵活性和范围。
  • 正则表达式中的组将多个字符视为一个单元。它们用括号 () 括起来。组有两种形式:捕获组和非捕获组。
    • 捕获组保存由括号内正则表达式部分匹配的字符串部分。这允许用户从字符串中提取信息,或在同一正则表达式中重用模式的一部分(反向引用)。有两种捕获组:无名组 (...) 和命名组 (...)。命名组是无名组的补充,无名组只能通过它们在正则表达式中的相对位置进行反向引用。命名组允许我们为组提供一个更具自解释性的名称,并由该名称引用。普通括号 () 中的任何正则表达式都用于捕获。例如,(abc) 捕获序列 abc,而 (?\d{4}) 捕获四个数字作为年份。
    • 有五种非捕获组。第一个是 (?:...),它匹配组,但它们不保存组中的文本。当您需要分组功能而无需捕获的开销时使用它们。在组前加上 ?:,例如 (?:abc) 以在不捕获的情况下对它进行分组。它将“abc”视为一个单元,而不记住匹配。
      第二个和第三个是两个先行组(正向先行和负向先行)。正向先行 (?= ... ) 匹配主表达式后面的组,但不将其包含在结果中。负向先行 (?! ... ) 断言指定的组不跟随主表达式。
      第四个和第五个是两个后行组(正向后行和负向后行)。正向后行 (?<= ... ) 断言该组先于主表达式并且必须匹配,但它不消耗任何字符。负向后行 (?

每个组件对于构建复杂高效的正则表达式以进行模式匹配和文本处理任务至关重要。

深入了解 decomposingRegex()

decomposingRegex() 函数自动化分解复杂的正则表达式。它识别并递归处理嵌套组和正则表达式的其他组件。这在教育环境或调试会话中特别有用,在这些环境中,理解正则表达式的确切行为是必要的。

代码解释

decomposingRegex() 的核心是递归识别和处理组的模式匹配逻辑。

decomposingRegex() 函数:详细解释

目的:该函数旨在将给定的正则表达式解析并分解为其组成部分,例如捕获组和非捕获组,并提供更简单的正则表达式模式(如量词和字符类)的详细分解。这有助于理解和调试复杂的正则表达式模式。

参数:

  • regex:作为字符串要解析的正则表达式。
  • depth:跟踪递归深度,初始设置为 0。
  • groupCounter:一个对象,用于计算捕获组,以便为每个组分配唯一的标识符。

子函数 findClosingBrackets(),它查找捕获组和非捕获组的开始和结束,以确保正确处理嵌套组。否则,正则表达式一次解析一个字符,并标记数量 +、* 和 {},字符类 [],由 \ 启动的转义字符,以及交替。如果进一步可以区分捕获组 ()、命名组 (?...) 和不同的非捕获组 (?:、(?=、(?!、(?<=、(?

JavaScript 函数代码:

function decomposingRegex(regex, depth = 0, groupCounter = { count: 0 }) {
    let str = ' '.repeat(depth * 2) + `Exploring level ${depth}: ${regex}\n`;
    let i = 0;
    let len = regex.length;
    let end=0;
    let description="";
    let quant='';

    function findClosingBracket(index, open, close) {
        let count = 1;
        while (index < len && count > 0) {
            const char = regex[index];
            if (char === open)
                count++;
            else if (char === close)
                count--;
            index++;
        }
        return index - 1;
    }
    
    function appendQuantifierInfo(regex, i, char, depth) {
        const isLazy = regex[i + 1] === '?';
        const quant = char + (isLazy ? '?' : '');
        const descriptions = {
            '+': 'one or more occurrences',
            '*': 'zero or more occurrences',
            '?': 'zero or one occurrence'
        };
        const description = (isLazy ? 'lazy ' : '') + descriptions[char];
        str += ' '.repeat((depth + 1) * 2) + `Found Quantifier: ${quant}\t# ${description}\n`;
        if (isLazy) 
            ++i;
        return i;
    }
    
    function handleEscapeCharacter(regex, i, depth) {
        const descriptions = {
            'd': "matches any decimal digit",
            'D': "matches any non-digit character",
            'w': "matches any word character (alphanumeric & underscore)",
            'W': "matches any non-word character",
            's': "matches any whitespace character",
            'S': "matches any non-whitespace character",
            'b': "matches a word boundary",
            'B': "matches a non-word boundary",
            'n': "matches a newline character",
            'r': "matches a carriage return character",
            't': "matches a tab character",
            'f': "matches a form feed character",
            'v': "matches a vertical tab character"
        };
        const char = regex[i + 1];
        const description = descriptions[char] || "special character";
        str += ' '.repeat((depth + 1) * 2) + `Found Escaped character: \\${char}\t# ${description}\n`;
        return i + 1; // Move past the escaped character
    }

    while (i < len) {
        const char = regex[i];

        switch (char) {
            case '\\':
                i = handleEscapeCharacter(regex, i, depth);
                break;
            case '[':
                end = findClosingBracket(i + 1, '[', ']');
                str += ' '.repeat((depth + 1) * 2) + `Found Character class: ${regex.substring(i, end + 1)}\n`;
                i = end;
                break;
            case '(':
                let next3=regex.substring(i, i + 3);
                let next4=regex.substring(i,i+4);
                if (next3 === '(?:' || next3 === '(?=' || next3==='(?!')  
                    {// Non-capturing group
                    let end = findClosingBracket(i + 1, '(', ')');
                    let content = regex.substring(i + 3, end); 
                    description="";
                    switch (next3) {
                        case '(?:':
                            description='Non capturing group'; break;
                        case '(?=':
                            description='Non capturing group for positive lookaheads '; break;
                        case '(?!':
                             description='Non capturing group for negative lookaheads'; break;
                          }
                    str += ' '.repeat((depth + 1) * 2) + `Found Non-capturing group: ${next3}${content})\t#${description}\n`;  
                    // skip to content
                    i += 2;}
                else if(next4==='(?<=' || next4==='(?<!')
                    {// Non-capturing group
                    let end = findClosingBracket(i + 1, '(', ')');
                    let content = regex.substring(i + 4, end);  
                    description="";
                    switch (next4) {
                        case '(?<=':
                            description='Non capturing group for positive lookbehind'; break;
                        case '(?<!':
                            description='Non capturing group for negative lookbehind '; break;
                          }   
                     str += ' '.repeat((depth + 1) * 2) + `Found Non-capturing group: ${next4}${content})\t#${description}\n`;    
                    // skip to content
                    i += 3;}
                else {
                    // Check for name capturing group and bypass it
                    let name="";
                    if (next3 === '(?<'){
                        const closeTagIndex = regex.indexOf('>', i);
                        if (closeTagIndex !== -1) 
                           {
                            name=regex.substring(i+3,closeTagIndex);
                            i = closeTagIndex; // Move the index to the position right after '>'
                           }
                    }
                    groupCounter.count++;
                    let end = findClosingBracket(i + 1, '(', ')');
                    let content = regex.substring(i + 1, end);   
                    str += ' '.repeat((depth + 1) * 2) + `Found Capturing group ${groupCounter.count}: (${content})\n`;
                    if(name!="") 
                        str += ' '.repeat((depth + 1) * 2) + `Is also a named Capturing group: ${name}\n`;
                    str += decomposingRegex(content, depth + 1, groupCounter);
                    i = end;
                    }
                break;
            case '{':
                end = findClosingBracket(i + 1, '{', '}');
                quant = regex.substring(i, end + 1);
                description = "";
                if(regex[end+1]==='?')
                   { description="lazy "; ++end; }
                if (quant.match(/^\{\d+\}$/)) {
                    description += "matches exactly " + quant.match(/\d+/)[0] + " times";
                } else if (quant.match(/^\{\d+,\}$/)) {
                    description += "matches at least " + quant.match(/\d+/)[0] + " times";
                } else if (quant.match(/^\{\d+,\d+\}$/)) {
                    let numbers = quant.match(/\d+/g);
                    description += "matches between " + numbers[0] + " and " + numbers[1] + " times";
                }                    
                str += ' '.repeat((depth + 1) * 2) + `Found Quantifier: ${quant} \t# ${description}\n`;
                i = end;
                break;
            case '+':
            case '*':
            case '?':
                i = appendQuantifierInfo(regex, i, char, depth);
                break;
            case '|':
                str += ' '.repeat((depth + 1) * 2) + `Found Alternation: |\n`;
                break;
            case '.':
               str += ' '.repeat((depth + 1) * 2) + `Found Any character: .\n`;
               break;
                
        }
        i++;
    }

    return str;
}

 

摘要

decomposingRegex() 函数提供了一种分解和理解正则表达式的全面方法。递归解析组并详细说明更简单的部分,为复杂正则表达式模式的结构和功能提供了宝贵的见解,使其成为处理项目中正则表达式的开发人员的必备工具。

实际示例

例如,考虑正则表达式

             /([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)/g

这是一个匹配带符号浮点数的正则表达式。使用 decomposingRegex(),可以分解它以揭示其结构:一个可选的符号,一个带可选小数部分的数字,后跟一个可选的指数。分解阐明了每个组件的作用和语法。

分解结果如下所示

Exploring level 0: /([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)/g
  Found Capturing group 1: ([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)
  Exploring level 1: [+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?
    Found Character class: [+-]
    Found Quantifier: ?    # zero or one occurrence
    Found Capturing group 2: (\d+(\.\d*)?|\.\d+)
    Exploring level 2: \d+(\.\d*)?|\.\d+
      Found Escaped character: \d    # matches any decimal digit
      Found Quantifier: +    # one or more occurrences
      Found Capturing group 3: (\.\d*)
      Exploring level 3: \.\d*
        Found Escaped character: \.    # special character
        Found Escaped character: \d    # matches any decimal digit
        Found Quantifier: *    # zero or more occurrences
      Found Quantifier: ?    # zero or one occurrence
      Found Alternation: |
      Found Escaped character: \.    # special character
      Found Escaped character: \d    # matches any decimal digit
      Found Quantifier: +    # one or more occurrences
    Found Capturing group 4: ([eE][+-]?\d+)
    Exploring level 2: [eE][+-]?\d+
      Found Character class: [eE]
      Found Character class: [+-]
      Found Quantifier: ?    # zero or one occurrence
      Found Escaped character: \d    # matches any decimal digit
      Found Quantifier: +    # one or more occurrences
    Found Quantifier: ?    # zero or one occurrence

请注意,组 1、组 2 等匹配 $1 和 $2 参数等。

This is displayed in the result window in the web app.
RegExp=/[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?/g
Input: -12.34e+56 => Matched: -12.34e+56
            $1=12.34 $2=.34 $3=e+56

结论

通过分解理解正则表达式不仅有助于调试和开发,还提高了编写更高效、更无错误的正则表达式的能力。像 decomposingRegex() 这样的工具在此教育过程中发挥着重要作用。作者的正则表达式工具是提到的第六个工具。它有一个独特的功能,允许您同时测试两个正则表达式。当尝试为给定问题构建正则表达式时,这会变得很有用,然后可以动态地构建表达式以连续匹配更多所需的模式。

有用的在线工具

有几个用于测试和调试正则表达式的在线工具。以下是一些广泛使用的正则表达式工具及其相关详细信息,您可以在文章中引用它们

1. Regex101

  • 网站Regex101
  • 描述:Regex101 是一个强大的在线工具,用于测试和调试正则表达式。它支持多种编程语言,包括 JavaScript、Python、PHP 和 Go。该工具在您输入时提供每个正则表达式部分的详细解释,以及快速参考指南和用户提交的正则表达式模式库。
  • 主要特点:
    • 实时正则表达式解析和测试。
    • 正则表达式构造的详细解释。
    • 各种语言的代码生成器。
    • 用户模式库。

2. RegExr

  • 网站RegExr
  • 描述:RegExr 是另一个流行的在线工具,用于学习、构建和测试正则表达式。它提供了一个干净的界面,并提供有关正则表达式模式匹配的实时视觉反馈。
  • 主要特点:
    • 实时结果和高亮显示。
    • 丰富的社区模式和示例。
    • 详细的帮助和备忘录。
    • 您的正则表达式测试历史记录,便于回溯。

3. RegexBuddy

  • 网站RegexBuddy
  • 描述:RegexBuddy 是一个可下载的 Windows 工具,可作为您的正则表达式助手。它帮助您创建和理解复杂的正则表达式,并将其实现到源代码中。
  • 主要特点:
    • 正则表达式的详细分析。
    • 根据示例文本测试正则表达式。
    • 与各种编程环境集成。
    • 用于更容易组装的正则表达式构建块。

4. Regex Pal

  • 网站Regex Pal
  • 描述:Regex Pal 是一个简单直接的基于网络的工具,用于快速测试 JavaScript 正则表达式。它提供即时视觉反馈,但比 Regex101 或 RegExr 更简单,功能更少。
  • 主要特点:
    • 实时高亮显示快速测试。
    • 极简且快速。
    • 带有正则表达式标记和简短描述的侧边栏。

5. Regex Tester

  • 网站Regex Tester and Debugger Online - Javascript, PCRE, PHP
  • 描述:RegexPlanet 的 Regex Tester 支持测试和调试多种编程语言的正则表达式,包括 Java、.NET 和 Ruby。它提供了一个独特的环境来在不同的编程上下文中测试正则表达式。
  • 主要特点:
    • 支持多种编程语言。
    • 正则表达式库和社区示例。
    • 正则表达式测试和结果的高级选项。

6. Regular expression tester

  • 网站Testing Regular expression for matching specific text patterns (hvks.com)
  • 描述:Regular Expression Tester 支持测试和调试 JavaScript 编程语言的正则表达式。它提供了一个独特的环境来在不同的编程上下文中测试正则表达式。
  • 主要特点:
    • 支持大多数编程语言。
    • 大多数常见日常编码问题的正则表达式示例。
    • 提供正则表达式分解,以便于调试和理解。
    • 提供多行匹配。
    • 可以同时检查和测试两个正则表达式。
    • 提供打印和电子邮件发送结果。

参考

  1. J. Goyvaerts & S. Levithan, Regular Expression Cookbook, O’Reilly 2009年5月
  2. 正则表达式测试器。Testing Regular expression for matching specific text patterns (hvks.com)
© . All rights reserved.