JavaScript正则表达式中的命名捕获组?

293
据我所知,JavaScript 中没有命名捕获组这样的东西。那么获取类似功能的替代方法是什么?

1
在JavaScript中,捕获组是按编号进行的。$1是第一个捕获组,$2、$3一直到$99。但是听起来你想要另外一些东西——这种东西并不存在。 - Erik
26
你提到了“有编号的捕获组”,而问题发起者谈论的是“具有名称的捕获组”。它们确实存在,但我们想知道JS是否支持它们。 - Alba Mendez
5
有一个提案,旨在将命名正则表达式引入JavaScript(https://github.com/littledan/es-regexp-named-groups),但如果有的话,我们可能需要几年时间才能看到它。请注意,这并不是确定会实现的。 - fregante
Firefox因为我在一个网站上尝试使用命名捕获组而惩罚了我...这其实是我的错。https://dev59.com/K7Pma4cB1Zd3GeqPpWCc#58221254 - Nick Grealy
10个回答

238
ECMAScript 2018将命名捕获组引入JavaScript正则表达式。
示例:
  const auth = 'Bearer AUTHORIZATION_TOKEN'
  const { groups: { token } } = /Bearer (?<token>[^ $]*)/.exec(auth)
  console.log(token) // "AUTHORIZATION_TOKEN"

如果您需要支持旧版浏览器,使用普通(编号)捕获组可以完成与命名捕获组相同的所有操作,只需跟踪数字-如果捕获组在正则表达式中的顺序更改,则可能会很麻烦。
我只能想到两个命名捕获组的“结构”优势:
1. 在某些正则表达式语法(.NET和JGSoft,据我所知)中,您可以在正则表达式中为不同的组使用相同的名称(请参见此处的示例)。但是,大多数正则表达式语法都不支持此功能。
2. 如果您需要引用数字捕获组,并且它们被数字包围,则可能会遇到问题。假设您想要向数字添加零,因此希望使用 $ 10 替换(\ d)。在JavaScript中,这将起作用(只要您的正则表达式中捕获组少于10个),但Perl会认为您正在寻找反向引用号码 10 而不是号码 1 ,后跟 0 。在Perl中,您可以在这种情况下使用 $ {1} 0 。
除此之外,命名捕获组只是“语法糖”。仅在真正需要捕获组时使用它们,并在所有其他情况下使用非捕获组(?:...)有助于解决问题。
JavaScript 的一个更大的问题(在我看来)是它不支持冗长的正则表达式,这使得创建可读性强、复杂的正则表达式变得更加困难。 Steve Levithan 的 XRegExp 库 解决了这些问题。

5
许多编程语言的正则表达式中都可以允许同一个捕获组名在表达式中出现多次。但只有.NET和Perl 5.10版本以上的正则表达式特别实用,因为它们会保留最后一个同名捕获组所捕获的值。 - slevithan
114
巨大的优势是:您只需更改正则表达式,无需进行数字到变量的映射。非捕获组可以解决此问题,但有一种情况除外:如果组的顺序发生变化怎么办?此外,在其他组中添加这些额外的字符很麻烦... - Alba Mendez
65
所谓的“语法糖”确实有助于增加代码的可读性! - Mrchief
2
我认为命名捕获组还有另一个非常有价值的原因。例如,如果您想使用正则表达式从字符串中解析日期,您可以编写一个灵活的函数,该函数接受值和正则表达式。只要正则表达式具有年份、月份和日期的命名捕获组,您就可以通过最少的代码运行一系列正则表达式。 - Dewey Vozel
4
截至2019年10月,Firefox、IE 11和Microsoft Edge(Chromium之前的版本)不支持命名捕获组。但大多数其他浏览器(包括Opera和三星手机浏览器)都支持该功能。https://caniuse.com/#feat=mdn-javascript_builtins_regexp_named_capture_groups - JDB
显示剩余5条评论

72

另一种可能的解决方案:创建一个包含组名称和索引的对象。

var regex = new RegExp("(.*) (.*)");
var regexGroups = { FirstName: 1, LastName: 2 };

然后,使用对象的键来引用这些组:

var m = regex.exec("John Smith");
var f = m[regexGroups.FirstName];

这样可以通过正则表达式的结果来提高代码的可读性/质量,但不会提高正则表达式本身的可读性。


66

在ES6中,您可以使用数组解构来捕获您的分组:

let text = '27 months';
let regex = /(\d+)\s*(days?|months?|years?)/;
let [, count, unit] = regex.exec(text) || [];

// count === '27'
// unit === 'months'

注意:

  • 最后一个let中的第一个逗号会跳过结果数组中的第一个值,该值是整个匹配字符串。
  • .exec()之后的|| []将在没有匹配项(因为.exec()将返回null)时防止解构错误。

1
第一个逗号是因为match返回的数组中的第一个元素是输入表达式,对吧? - Emilio Grisolía
1
String.prototype.match 返回一个数组,其中包含整个匹配的字符串在位置0,然后是其后的任何分组。第一个逗号表示“跳过位置0上的元素”。 - fregante
2
我在这里为那些有转译或 ES6+ 目标的人提供了我的最爱答案。这并不一定能像命名索引那样防止不一致错误,例如如果一个被重复使用的正则表达式发生了变化,但我认为这里的简洁性很容易弥补这一点。我选择在字符串可能为 nullundefined 的地方使用 RegExp.prototype.exec 而不是 String.prototype.match - Mike Hill

63
您可以使用XRegExp,这是一个增强、可扩展的跨浏览器正则表达式实现,包括对其他语法、标记和方法的支持:
  • 添加新的正则表达式和替换文本语法,其中包括全面支持命名捕获
  • 添加两个新的正则表达式标记:s,使点匹配所有字符(也称为dotall或singleline模式),以及x,用于自由空间和注释(也称为extended模式)。
  • 提供一套函数和方法,使复杂的正则表达式处理变得轻而易举。
  • 自动修复最常见的跨浏览器不一致性,包括正则表达式行为和语法。
  • 让您轻松创建和使用插件,向XRegExp的正则表达式语言添加新的语法和标记。

26

更新:它最终已经被纳入JavaScript(ECMAScript 2018)了!


命名捕获组很快就可能被加入到JavaScript中。
该提案已经处于第三阶段。

使用(?<name>...)语法,可以在尖括号内为捕获组命名,其中名称可为任何标识符名称。日期的正则表达式可以写成 /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u。每个名称应该是唯一的并遵循ECMAScript IdentifierName的语法。

命名组可以通过正则表达式结果的groups属性的属性访问。与非命名组一样,也会创建对组的编号引用。例如:

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result = re.exec('2015-01-02');
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';

// result[0] === '2015-01-02';
// result[1] === '2015';
// result[2] === '01';
// result[3] === '02';

目前这是一个第4阶段的提案。 - GOTO 0
1
如果你正在使用'18,最好使用解构赋值; let {year, month, day} = ((result) => ((result) ? result.groups : {}))(re.exec('2015-01-02')); - Hashbrown
1
既然命名捕获组有望实现,不妨将 null-coalescing 进行全面升级:let {year, month, day} = {...re.exec('2015-01-02')?.groups}; - Robert

10
作为 Tim Pietzcker 所说,ECMAScript 2018 将命名捕获组引入了 JavaScript 正则表达式。 但是我在上面的答案中没有找到如何在正则表达式本身中使用命名捕获组。 你可以用这个语法使用命名捕获组:\k<name>。例如:
var regexObj = /(?<year>\d{4})-(?<day>\d{2})-(?<month>\d{2}) year is \k<year>/

正如Forivin所说,您可以按照以下方式在对象结果中使用捕获组:

let result = regexObj.exec('2019-28-06 year is 2019');
// result.groups.year === '2019';
// result.groups.month === '06';
// result.groups.day === '28';

  var regexObj = /(?<year>\d{4})-(?<day>\d{2})-(?<month>\d{2}) year is \k<year>/mgi;

function check(){
    var inp = document.getElementById("tinput").value;
    let result = regexObj.exec(inp);
    document.getElementById("year").innerHTML = result.groups.year;
    document.getElementById("month").innerHTML = result.groups.month;
    document.getElementById("day").innerHTML = result.groups.day;
}
td, th{
  border: solid 2px #ccc;
}
<input id="tinput" type="text" value="2019-28-06 year is 2019"/>
<br/>
<br/>
<span>Pattern: "(?<year>\d{4})-(?<day>\d{2})-(?<month>\d{2}) year is \k<year>";
<br/>
<br/>
<button onclick="check()">Check!</button>
<br/>
<br/>
<table>
  <thead>
    <tr>
      <th>
        <span>Year</span>
      </th>
      <th>
        <span>Month</span>
      </th>
      <th>
        <span>Day</span>
      </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <span id="year"></span>
      </td>
      <td>
        <span id="month"></span>
      </td>
      <td>
        <span id="day"></span>
      </td>
    </tr>
  </tbody>
</table>


6

给捕获组命名的唯一作用是减少在复杂正则表达式中的混乱。

这真的取决于您的使用情况,但也许漂亮地打印您的正则表达式可以帮助您。

或者您可以尝试定义常量来引用您的捕获组。

注释可能还有助于向阅读您的代码的其他人展示您所做的事情。

对于剩下的部分,我必须同意Tim的答案。


5
有一个名为 named-regexp 的 node.js 库可以在你的 node.js 项目中使用(或者通过将库与 browserify 或其他打包脚本一起打包来在浏览器中使用)。但是,该库无法用于包含非命名捕获组的正则表达式。
如果您计算正则表达式中开放捕获大括号的数量,您可以创建命名捕获组和您的 regex 中编号的捕获组之间的映射,并且可以自由混合和匹配。使用正则表达式之前,只需删除组名即可。我编写了三个函数来演示这一点,请参阅此代码片段: https://gist.github.com/gbirke/2cc2370135b665eee3ef

这非常轻巧,我会试一下。 - fregante
复杂正则表达式中的嵌套命名组是否能够正常工作? - ElSajko
它并不完美。当输入getMap("((a|b(:<foo>c)))")时,会出现错误。 其中foo应该是第三组而非第二组。/((a|b(c)))/g.exec("bc"); ["bc", "bc", "bc", "c"] - ElSajko

3

没有ECMAScript 2018吗?

我的目标是使其尽可能类似于我们使用命名组的方式。在ECMAScript 2018中,您可以在组内放置?<groupname>以指示命名组,在我为旧版javascript提供的解决方案中,您可以在组内放置(?!=<groupname>)以实现同样的效果。因此,这是额外的一组括号和一个额外的!=。相当接近!

我将所有内容都包装到了一个字符串原型函数中

特点

  • 适用于旧版javascript
  • 不需要额外的代码
  • 非常简单易用
  • 正则表达式仍然有效
  • 组文档记录在正则表达式本身中
  • 组名可以有空格
  • 返回结果对象

说明

  • 在要命名的每个组中放置(?!={groupname})
  • 请记得通过在该组开头放置?:来消除任何非捕获组()。这些不会被命名。

arrays.js

// @@pattern - includes injections of (?!={groupname}) for each group
// @@returns - an object with a property for each group having the group's match as the value 
String.prototype.matchWithGroups = function (pattern) {
  var matches = this.match(pattern);
  return pattern
  // get the pattern as a string
  .toString()
  // suss out the groups
  .match(/<(.+?)>/g)
  // remove the braces
  .map(function(group) {
    return group.match(/<(.+)>/)[1];
  })
  // create an object with a property for each group having the group's match as the value 
  .reduce(function(acc, curr, index, arr) {
    acc[curr] = matches[index + 1];
    return acc;
  }, {});
};    

使用方法

function testRegGroups() {
  var s = '123 Main St';
  var pattern = /((?!=<house number>)\d+)\s((?!=<street name>)\w+)\s((?!=<street type>)\w+)/;
  var o = s.matchWithGroups(pattern); // {'house number':"123", 'street name':"Main", 'street type':"St"}
  var j = JSON.stringify(o);
  var housenum = o['house number']; // 123
}

o的结果

{
  "house number": "123",
  "street name": "Main",
  "street type": "St"
}

即使结果非常酷,但修改JavaScript全局String类的原型是一个非常糟糕的想法,这是义务性的。 - brainkim

2

虽然你不能用普通的JavaScript实现这个功能,但是你可以使用一些Array.prototype函数,例如Array.prototype.reduce,通过一些神奇的方式将索引匹配转换为命名匹配。

显然,以下解决方案需要匹配按顺序出现:

// @text Contains the text to match
// @regex A regular expression object (f.e. /.+/)
// @matchNames An array of literal strings where each item
//             is the name of each group
function namedRegexMatch(text, regex, matchNames) {
  var matches = regex.exec(text);

  return matches.reduce(function(result, match, index) {
    if (index > 0)
      // This substraction is required because we count 
      // match indexes from 1, because 0 is the entire matched string
      result[matchNames[index - 1]] = match;

    return result;
  }, {});
}

var myString = "Hello Alex, I am John";

var namedMatches = namedRegexMatch(
  myString,
  /Hello ([a-z]+), I am ([a-z]+)/i, 
  ["firstPersonName", "secondPersonName"]
);

alert(JSON.stringify(namedMatches));


这非常酷。我在想…是否可能创建一个接受自定义正则表达式的regex函数?这样,您可以像这样进行操作: var assocArray = Regex("hello alex, I am dennis", "hello ({hisName}.+), I am ({yourName}.+)"); - Forivin
@Forivin 显然,你可以进一步开发这个功能。让它正常工作并不难 :D - Matías Fidemraizer
你可以通过向 RegExp 对象的原型添加函数来扩展它。 - Mr. TA
据我所知,不建议扩展内置对象。 - Matías Fidemraizer

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