如何在JavaScript中将长的正则表达式拆分成多行?

194
我有一个非常长的正则表达式,我希望将它拆分为多行以便于阅读。 我想让每行代码长度在80个字符以内,符合JSLint规则。以下是样例模式:
var pattern = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

5
看起来你正在(尝试)验证电子邮件地址。为什么不直接使用 /\S+@\S+\.\S+/ - Bart Kiers
1
你应该尝试找到一种无需正则表达式或使用多个较小的正则表达式来实现它的方法。这比一个很长的正则表达式更易读。如果你的正则表达式超过约20个字符,那么可能有更好的方法来实现它。 - ForbesLindesay
2
现在宽屏幕普及,80个字符不是有点过时了吗? - Oleg V. Volkov
9
@OlegV.Volkov 不行。一个人可能会在vim中使用分割窗口,在服务器机房中使用虚拟终端。假设每个人都会在与你相同的视口中编码是错误的。此外,将行限制为80个字符会强迫你将代码分解为更小的函数。 - synic
我当然理解你想在这里这样做的动机 - 当这个正则表达式被分成多行,就像Koolilnc所演示的那样,它立即成为可读性强、自我记录的代码的完美例子。 ¬_¬ - Mark Amery
@OlegV.Volkov,将宽屏幕分成几个窗口仍然是非常方便的。例如,在一个窗口中您可以使用文本编辑器,在另一个窗口中运行单元测试。 - Dmitry Koroliov
11个回答

164
扩展@KooiInc的答案,您可以通过使用RegExp对象的source属性来避免手动转义每个特殊字符。
例如:
var urlRegex = new RegExp(
  /(?:(?:(https?|ftp):)?\/\/)/.source       // protocol
  + /(?:([^:\n\r]+):([^@\n\r]+)@)?/.source  // user:pass
  + /(?:(?:www.)?([^/\n\r]+))/.source       // domain
  + /(\/[^?\n\r]+)?/.source                 // request
  + /(\?[^#\n\r]*)?/.source                 // query
  + /(#?[^\n\r]*)?/.source                  // anchor
);

或者,如果你想避免重复使用 .source 属性,可以使用 Array.map() 函数:

var urlRegex = new RegExp([
  /(?:(?:(https?|ftp):)?\/\/)/,     // protocol
  /(?:([^:\n\r]+):([^@\n\r]+)@)?/,  // user:pass
  /(?:(?:www.)?([^/\n\r]+))/,       // domain
  /(\/[^?\n\r]+)?/,                 // request
  /(\?[^#\n\r]*)?/,                 // query
  /(#?[^\n\r]*)?/,                  // anchor
].map(function (r) { return r.source; }).join(''));

在ES6中,map函数可以简化为:.map(r => r.source)

3
非常符合我的期望,非常干净清爽。谢谢! - Marian Zagoruiko
13
这对于在长的正则表达式中添加注释非常方便。但是,由于匹配括号必须在同一行上,因此有一定的限制。 - Nathan S. Watson-Haigh
绝对没错,这个功能非常棒,可以在每个子正则表达式上进行注释。 - GaryO
谢谢,将源代码放入正则表达式函数中确实有所帮助。 - Code
1
非常聪明。谢谢,这个想法帮了我很多。 顺便说一下:我将整个内容封装在一个函数中,以使其更加简洁:combineRegex = (...regex) => new RegExp(regex.map(r => r.source).join("")) 用法:combineRegex(/regex1/, /regex2/, ...) - Scindix
另外,你可能需要添加该方法的限制条件。(例如匹配括号等) - Scindix

141
[2022/08 编辑] 创建了一个小的GitHub 仓库,用于创建带有空格、注释和模板的正则表达式。
您可以将其转换为字符串,并通过调用 new RegExp() 来创建表达式:
var myRE = new RegExp (['^(([^<>()[\]\\.,;:\\s@\"]+(\\.[^<>(),[\]\\.,;:\\s@\"]+)*)',
                        '|(\\".+\\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.',
                        '[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\\.)+',
                        '[a-zA-Z]{2,}))$'].join(''));

注意事项:

  1. 表达式字面量 转换为字符串时,需要转义所有反斜杠,因为在解析 字符串字面量 时会自动消耗掉反斜杠。 (详见 Kayo 的评论.)

  2. RegExp 函数接受一个修饰符作为第二个参数。

    /regex/g => new RegExp('regex', 'g')

[新增 ES20xx 特性 (模板标签)]

在 ES20xx 中,你可以使用模板标签。请查看代码片段。

注意:

  • 缺点在于正则表达式字符串中不能使用普通的空格(应该使用 \s\s+\s{1,x}, \t, \n 等)。

(() => {
  const createRegExp = (str, opts) => 
    new RegExp(str.raw[0].replace(/\s/gm, ""), opts || "");
  const yourRE = createRegExp`
    ^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|
    (\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|
    (([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`;
  console.log(yourRE);
  const anotherLongRE = createRegExp`
    (\byyyy\b)|(\bm\b)|(\bd\b)|(\bh\b)|(\bmi\b)|(\bs\b)|(\bms\b)|
    (\bwd\b)|(\bmm\b)|(\bdd\b)|(\bhh\b)|(\bMI\b)|(\bS\b)|(\bMS\b)|
    (\bM\b)|(\bMM\b)|(\bdow\b)|(\bDOW\b)
    ${"gi"}`;
  console.log(anotherLongRE);
})();


5
“新的RegExp”是用于多行正则表达式的好方法。不必连接数组,您可以使用字符串连接操作符:“var reg = new RegExp('^([a-' + 'z]+)$','i');”。 - dakab
49
注意:一个较长的“正则表达式文字”可以通过上面的答案分成多行。但是需要小心,因为你不能简单地复制“正则表达式文字”(使用//定义)并将其粘贴为RegExp构造函数的字符串参数。这是因为在评估“字符串文字”时反斜杠字符会被消耗。例如:/Hey\sthere/ 不能被替换为 new RegExp("Hey\sthere")。而应该替换为 new RegExp("Hey\\sthere")注意额外的反斜杠!因此,我更喜欢将长的正则表达式文字留在一行上。 - Kayo
5
更清晰的方法是创建带有有意义子部分名称的变量,然后将它们作为字符串或数组连接起来。这样可以更轻松地构建RegExp,让整个过程更易于理解。 - Chris Krycho
MDN也建议在正则表达式保持不变时使用字面量表示法,而在正则表达式可能会改变时使用构造函数表示法。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp - Akin Hwan
.replace(/\s/gm, "")替换为.replace(/( #.*|\s)/gm, "")也将启用#注释(例如ruby),其中在#之前至少需要一个空格。 - Míng
@AkinHwan 我不相信它还会这样说。但无论如何,我几乎总是优化可维护性。创建看起来像象形文字的正则表达式非常容易(而且几乎已经标准化)。这几乎是一种荣誉徽章。最好编写一些可以轻松被未来的我甚至其他开发人员解析的东西。 - acjay

33

使用 new RegExp 中的字符串很麻烦,因为必须转义所有反斜杠。您可以编写较小的正则表达式并将它们连接起来。

让我们拆分这个正则表达式。

/^foo(.*)\bar$/

稍后我们将使用一个函数来使事情变得更加美观

function multilineRegExp(regs, options) {
    return new RegExp(regs.map(
        function(reg){ return reg.source; }
    ).join(''), options);
}

现在让我们开始摇滚吧

var r = multilineRegExp([
     /^foo/,  // we can add comments too
     /(.*)/,
     /\bar$/
]);

由于构建正则表达式需要成本,因此尝试仅构建一次真实的正则表达式,然后重复利用。


1
这非常酷 - 不仅您不必进行额外的转义,而且还可以保留子正则表达式的特殊语法高亮显示! - quezak
1
一个注意点是:你需要确保你的子正则表达式是自包含的,或者将每个子正则表达式都包裹在新的括号组中。例如:multilineRegExp([/a|b/, /c|d]) 的结果是 /a|bc|d/,而你想要的是 (a|b)(c|d) - quezak
这使得像@quezak提到的那样,不可能将一个大而复杂的正则表达式组分成多行,因为你不能使用multilineRegExp([/a (/, /cold/, /|hot/, /) drink/]) - ProblemsLoop

19
感谢模板字面量神奇的世界,你现在可以在ES6中编写大型、多行、注释良好,甚至是语义嵌套的正则表达式。
//build regexes without worrying about
// - double-backslashing
// - adding whitespace for readability
// - adding in comments
let clean = (piece) => (piece
    .replace(/((^|\n)(?:[^\/\\]|\/[^*\/]|\\.)*?)\s*\/\*(?:[^*]|\*[^\/])*(\*\/|)/g, '$1')
    .replace(/((^|\n)(?:[^\/\\]|\/[^\/]|\\.)*?)\s*\/\/[^\n]*/g, '$1')
    .replace(/\n\s*/g, '')
);
window.regex = ({raw}, ...interpolations) => (
    new RegExp(interpolations.reduce(
        (regex, insert, index) => (regex + insert + clean(raw[index + 1])),
        clean(raw[0])
    ))
);

使用这个工具,你现在可以像这样编写正则表达式:

let re = regex`I'm a special regex{3} //with a comment!`;

输出

/I'm a special regex{3}/

或者多行文本呢?

'123hello'
    .match(regex`
        //so this is a regex

        //here I am matching some numbers
        (\d+)

        //Oh! See how I didn't need to double backslash that \d?
        ([a-z]{1,3}) /*note to self, this is group #2*/
    `)
    [2]

输出 hel,很整洁!
如果我需要实际搜索一个换行符怎么办?那就使用 \n 呗!
正在处理我的 Firefox 和 Chrome 浏览器。


好的,“那么复杂一点的呢?”
当然,这是我正在开发的一个对象解构JS解析器的一部分

regex`^\s*
    (
        //closing the object
        (\})|

        //starting from open or comma you can...
        (?:[,{]\s*)(?:
            //have a rest operator
            (\.\.\.)
            |
            //have a property key
            (
                //a non-negative integer
                \b\d+\b
                |
                //any unencapsulated string of the following
                \b[A-Za-z$_][\w$]*\b
                |
                //a quoted string
                //this is #5!
                ("|')(?:
                    //that contains any non-escape, non-quote character
                    (?!\5|\\).
                    |
                    //or any escape sequence
                    (?:\\.)
                //finished by the quote
                )*\5
            )
            //after a property key, we can go inside
            \s*(:|)
      |
      \s*(?={)
        )
    )
    ((?:
        //after closing we expect either
        // - the parent's comma/close,
        // - or the end of the string
        \s*(?:[,}\]=]|$)
        |
        //after the rest operator we expect the close
        \s*\}
        |
        //after diving into a key we expect that object to open
        \s*[{[:]
        |
        //otherwise we saw only a key, we now expect a comma or close
        \s*[,}{]
    ).*)
$`

它输出/^\s*((\})|(?:[,{]\s*)(?:(\.\.\.)|(\b\d+\b|\b[A-Za-z$_][\w$]*\b|("|')(?:(?!\5|\\).|(?:\\.))*\5)\s*(:|)|\s*(?={)))((?:\s*(?:[,}\]=]|$)|\s*\}|\s*[{[:]|\s*[,}{]).*)$/

可以用一个小示例来运行它吗?

let input = '{why, hello, there, "you   huge \\"", 17, {big,smelly}}';
for (
    let parsed;
    parsed = input.match(r);
    input = parsed[parsed.length - 1]
) console.log(parsed[1]);

成功输出{{}}

{why
, hello
, there
, "you   huge \""
, 17
,
{big
,smelly
}
}

注意成功捕获引用字符串。
我在Chrome和Firefox上测试过,效果很好!

如果你好奇我在做什么, 可以查看演示
虽然它只适用于Chrome,因为Firefox不支持反向引用或命名组。因此请注意,本答案中给出的示例实际上是一个被削弱的版本,可能会轻易地被欺骗而接受无效的字符串。


4
你应该考虑将这个作为 NodeJS 包进行导出,这很棒。 - rmobis
1
虽然我自己从未尝试过,但这里有一个非常详细的教程:https://zellwk.com/blog/publish-to-npm。我建议在页面末尾检查np。我从未使用过它,但Sindre Sorhus是这些东西的魔术师,所以我不会放弃这个建议。 - rmobis
1
@Siddharth 加油。我似乎还没有开始做这件事。Hashbrown777 在 Github 上也有。 - Hashbrown
1
@Siddharth 我已经在实践中使用它了 - Hashbrown
1
你会在这里如何添加正则表达式标志? - ProblemsLoop
显示剩余2条评论

11

这里有很好的答案,但为了完整性,应该提到JavaScript通过原型链继承的核心特性。类似这样的东西说明了这个想法:

RegExp.prototype.append = function(re) {
  return new RegExp(this.source + re.source, this.flags);
};

let regex = /[a-z]/g
.append(/[A-Z]/)
.append(/[0-9]/);

console.log(regex); //=> /[a-z][A-Z][0-9]/g


2
这是这里最好的答案。 - parttimeturtle
1
每次使用.append时都会创建编译RegExp对象,因此其他答案一次性编译给定的组合数组略好。我想差异微不足道,但值得注意。 - ProblemsLoop
1
@ProblemsLoop 这是真的。在我的测试中,使用6行多行正则表达式,在我8年前的工作站上,它比已接受的解决方案慢了约80%。尽管如此,我的电脑仍然达到了约220,000个操作/秒 https://jsbench.me/sfkz4e7mjf/2 - Jamesfo

6

上述正则表达式缺少一些反斜杠,导致其无法正常工作。因此,我编辑了该正则表达式。请考虑使用此正则表达式进行电子邮件验证,其有效率高达99.99%。

let EMAIL_REGEXP = 
new RegExp (['^(([^<>()[\\]\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\.,;:\\s@\"]+)*)',
                    '|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.',
                    '[0-9]{1,3}\])|(([a-zA-Z\\-0-9]+\\.)+',
                    '[a-zA-Z]{2,}))$'].join(''));

1
以上的投票和排序可以改变什么是“上面的”。 - Kimball Robinson

2
为了避免使用数组的join方法,你也可以使用以下语法:
var pattern = new RegExp('^(([^<>()[\]\\.,;:\s@\"]+' +
  '(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@' +
  '((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|' +
  '(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$');

2

我尝试通过封装所有内容并实现支持拆分捕获组和字符集的方法来改进korun的答案-使这种方法更加通用。

要使用此代码段,您需要调用可变参数函数combineRegex,其参数是您需要组合的正则表达式对象。它的实现可以在底部找到。

直接拆分捕获组是不行的,因为这样会让一些部分只有一个括号。您的浏览器将出现异常。

相反,我只是将捕获组的内容传递到数组中。当combineRegex遇到数组时,括号会自动添加。

此外,量词需要跟在某些内容之后。如果由于某种原因需要在量词之前拆分正则表达式,则需要添加一对括号。这些将自动删除。重点是空捕获组相当无用,通过这种方式,量词就有了参考物。同样的方法也可以用于像非捕获组(/(?:abc)/成为[/()?:abc/])之类的东西。

以下是一个简单的示例:

var regex = /abcd(efghi)+jkl/;

would become:

var regex = combineRegex(
    /ab/,
    /cd/,
    [
        /ef/,
        /ghi/
    ],
    /()+jkl/    // Note the added '()' in front of '+'
);

如果必须拆分字符集,您可以使用对象({"":[regex1, regex2, ...]})代替数组([regex1, regex2, ...])。键的内容可以是任何内容,只要对象只包含一个键即可。请注意,如果第一个字符可能被解释为量词符号,则必须使用]作为虚拟开头,而不是()。例如,/[+?]/变成{"":[/]+?/]}

Here is the snippet and a more complete example:

function combineRegexStr(dummy, ...regex)
{
    return regex.map(r => {
        if(Array.isArray(r))
            return "("+combineRegexStr(dummy, ...r).replace(dummy, "")+")";
        else if(Object.getPrototypeOf(r) === Object.getPrototypeOf({}))
            return "["+combineRegexStr(/^\]/, ...(Object.entries(r)[0][1]))+"]";
        else 
            return r.source.replace(dummy, "");
    }).join("");
}
function combineRegex(...regex)
{
    return new RegExp(combineRegexStr(/^\(\)/, ...regex));
}

//Usage:
//Original:
console.log(/abcd(?:ef[+A-Z0-9]gh)+$/.source);
//Same as:
console.log(
  combineRegex(
    /ab/,
    /cd/,
    [
      /()?:ef/,
      {"": [/]+A-Z/, /0-9/]},
      /gh/
    ],
    /()+$/
  ).source
);


你能发布一个npm包或其他什么吗?这是一个很棒的概念,可以让代码检查器/格式化工具帮助保持代码可读性... - Kimball Robinson

2
你可以简单地使用字符串操作。
var pattenString = "^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|"+
"(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|"+
"(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$";
var patten = new RegExp(pattenString);

1

个人而言,我会选择一个不那么复杂的正则表达式:

/\S+@\S+\.\S+/

当然,这种模式比您当前的模式不够准确,但您想要实现什么目标呢?您是想捕捉用户可能输入的意外错误,还是担心用户可能尝试输入无效地址?如果是前者,我建议使用更简单的模式。如果是后者,通过响应发送到该地址的电子邮件进行一些验证可能是更好的选择。
然而,如果您想使用当前的模式,通过构建由较小的子模式组成的模式(如下所示),会更容易阅读(和维护!):
var box1 = "([^<>()[\]\\\\.,;:\s@\"]+(\\.[^<>()[\\]\\\\.,;:\s@\"]+)*)";
var box2 = "(\".+\")";

var host1 = "(\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])";
var host2 = "(([a-zA-Z\-0-9]+\\.)+[a-zA-Z]{2,})";

var regex = new RegExp("^(" + box1 + "|" + box2 + ")@(" + host1 + "|" + host2 + ")$");

23
降分 - 虽然您有关减少正则表达式复杂性的评论是合理的,但OP特别询问如何“将长时间的正则表达式分成多行”。因此,尽管您的建议是正确的,但是出于错误的原因提供了它。例如,改变业务逻辑以解决编程语言问题。此外,您给出的代码示例相当丑陋。 - SleepyCal
6
@sleepycal,我认为Bart已经回答了这个问题。请查看他回答中的最后一部分。他不仅回答了问题,还提供了一个替代方案。 - Nidhin David

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