使用RegExp.exec提取字符串中的所有匹配项的正则表达式

246

我正在尝试解析以下类型的字符串:

[key:"val" key2:"val2"]

在这里有任意数量的key:"val"对。我想要获取key名称和对应的value值。

为了好奇,我正在尝试解析Task Warrior数据库格式的字符串。

这里是我的测试字符串:

[description:"aoeu" uuid:"123sth"]

这个代码片段旨在强调键或值中可能包含除空格以外的任何字符,冒号周围不能有空格,值总是用双引号括起来。

在Node环境下,我的输出如下:

[deuteronomy][gatlin][~]$ node
> var re = /^\[(?:(.+?):"(.+?)"\s*)+\]$/g
> re.exec('[description:"aoeu" uuid:"123sth"]');
[ '[description:"aoeu" uuid:"123sth"]',
  'uuid',
  '123sth',
  index: 0,
  input: '[description:"aoeu" uuid:"123sth"]' ]

但是description:"aoeu"也符合这种模式。如何获得所有匹配项?


1
可能是我的正则表达式有误和/或我在JavaScript中错误地使用了正则表达式功能。这似乎有效:> var s =“十五是15,八是8”;> var re = / \ d + / g; > var m = s.match(re); m = ['15','8'] - gatlin
8
JavaScript现在有一个.match()函数:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/match使用方法如下:"some string".match(/regex/g) - Stefnotch
现在你可以使用str.matchAll(regex)来迭代获取所有匹配项,包括组的元信息。因此,这个应该是被接受的答案,因为它现在得到了很好的支持。 - undefined
19个回答

282

继续在循环中调用re.exec(s)以获得所有匹配项:

var re = /\s*([^[:]+):\"([^"]+)"/g;
var s = '[description:"aoeu" uuid:"123sth"]';
var m;

do {
    m = re.exec(s);
    if (m) {
        console.log(m[1], m[2]);
    }
} while (m);

可以通过这个JSFiddle进行尝试:https://jsfiddle.net/7yS2V/


10
为什么不用while而使用do...while - Gumbo
16
使用 while 循环会使初始化 m 稍微有些麻烦。你要么得写成 while(m = re.exec(s)),但我认为这是一种反模式;或者你需要写成 m = re.exec(s); while (m) { ... m = re.exec(s); }。我更喜欢使用 do ... if ... while 的习惯用法,但其他方法也可以实现。 - lawnsea
19
在Chromium中进行这个操作导致了我的标签页崩溃。 - EdgeCaseBerg
53
你需要设置 g 标志,否则内部指针不会向前移动。文档 - Tim
13
另外一点是,如果正则表达式可以匹配空字符串,那么它将会导致无限循环。 - FabioCosta
显示剩余5条评论

243
str.match(pattern)如果pattern有全局标志g,则会将所有匹配项作为数组返回。
例如:

const str = 'All of us except @Emran, @Raju and @Noman were there';
console.log(
  str.match(/@\w*/g)
);
// Will log ["@Emran", "@Raju", "@Noman"]


26
注意:这些匹配结果不是匹配对象,而是匹配的字符串。例如,在 "All of us except @Emran:emran26, @Raju:raju13 and @Noman:noman42".match(/@(\w+):(\w+)/g) 中无法访问组(该方法将返回 ["@Emran:emran26", "@Raju:raju13", "@Noman:noman42"])。 - madprog
4
@madprog,对的,这是最简单的方法,但当群组价值至关重要时不适用。 - Anis
2
这对我不起作用。我只得到了第一个匹配。 - Anthony Roberts
13
@AnthonyRoberts,你需要添加"g"标志。/@\w/g或者new RegExp("@\\w", "g") - Aruna Herath

101
为了循环遍历所有匹配项,可以使用 replace 函数:
var re = /\s*([^[:]+):\"([^"]+)"/g;
var s = '[description:"aoeu" uuid:"123sth"]';

s.replace(re, function(match, g1, g2) { console.log(g1, g2); });

27
这是一个反直觉的代码。从任何有意义的角度来说,你并没有“替换”任何东西。只是利用某个函数实现了不同的目的。 - Luke Maurer
8
如果工程师只是遵循规则而没有超越常规思维,我们现在可能甚至都不会想到探索其他行星的可能性。;-) - Christophe
2
@dudewad 抱歉,我没有看到这里有什么懒惰的部分。如果完全相同的方法被称为“process”而不是“replace”,你会接受它。恐怕你只是固执于术语。 - Christophe
4
@Christophe,我绝对不固执于术语。我卡在了清晰的代码上。将本意为一目的的东西用于另一个目的被称为“hacky”,这会产生令人困惑的代码,难以理解,并且往往性能不佳。事实上您回答了这个问题而没有使用正则表达式,这使其成为一个无效的答案,因为OP正在寻求如何使用正则表达式来完成它。然而,我认为保持社区高标准是很重要的,这就是为什么我坚持我上面说的话。 - dudewad
1
有些人不会爬到桌子上去换灯泡,因为桌子是用来吃饭的。他们只会使用经过认证的梯子来换灯泡。其他任何方法都是“hacky”,不是一个干净利落的方式。 - capr
显示剩余6条评论

61

这是一个解决方案

var s = '[description:"aoeu" uuid:"123sth"]';

var re = /\s*([^[:]+):\"([^"]+)"/g;
var m;
while (m = re.exec(s)) {
  console.log(m[1], m[2]);
}

这是基于lawnsea的答案,但更简短。

请注意,必须设置`g'标志,以便在调用之间向前移动内部指针。


34
str.match(/regex/g)

以数组形式返回所有匹配项。

如果出于某种神秘原因,您需要附带exec的其他信息,作为先前答案的替代方案,您可以使用递归函数而不是循环来完成它,如下所示(这看起来更酷 :)。

function findMatches(regex, str, matches = []) {
   const res = regex.exec(str)
   res && matches.push(res) && findMatches(regex, str, matches)
   return matches
}

// Usage
const matches = findMatches(/regex/g, str)

正如之前评论中所述,重要的是在正则表达式定义的末尾添加g以便在每次执行中向前移动指针。


1
是的。递归看起来更优雅、更酷。迭代循环更直接,更易于维护和调试。 - Andy N
4
我喜欢递归解决方案,因为我喜欢递归解决方案。 - Ben Winding
递归深度的限制在Node.js中是多少?1_000_000次匹配会导致堆栈溢出吗? - Элёржон Кимсанов

29
我们终于开始看到内置的matchAll函数了,详情和兼容性表请参见此处。截至2020年5月,Chrome、Edge、Firefox和Node.js(12+)受支持,但IE、Safari和Opera不支持。看起来它是在2018年12月起草的,所以需要一些时间才能到达所有浏览器,但我相信它会到达那里的。
内置的matchAll函数很好,因为它返回一个可迭代对象,而且对于每个匹配项都返回捕获组!所以你可以做像这样的事情:
// get the letters before and after "o"
let matches = "stackoverflow".matchAll(/(\w)o(\w)/g);

for (match of matches) {
    console.log("letter before:" + match[1]);
    console.log("letter after:" + match[2]);
}

arrayOfAllMatches = [...matches]; // you can also turn the iterable into an array

似乎每个匹配对象都使用与match()相同的格式。因此,每个对象都是匹配和捕获组的数组,以及三个附加属性indexinputgroups,看起来像这样:

[<match>, <group1>, <group2>, ..., index: <match offset>, input: <original string>, groups: <named capture groups>]

如需了解有关 matchAll 的更多信息,还可以访问Google 开发者页面。此外,还提供Polyfill/Shim


我真的很喜欢这个功能,但它还没有完全适配Firefox 66.0.3。Caniuse上也还没有关于它的支持列表。不过我很期待它的到来。我在Chromium 74.0.3729.108中看到它已经可以使用了。 - Lonnie Best
1
@LonnieBest 是的,你可以查看我链接的MDN页面的兼容性部分。看起来Firefox在67版本中开始支持它。如果你想要发布一个产品,仍然不建议使用它。有一些polyfills/shims可用,我已经将它们添加到我的答案中。 - woojoo666
Polyfill每周在npm上有1100万次下载,因此它被广泛使用! - Drenai

18

如果你的系统支持ES9

(意思是如果你的系统支持Ecmascript 2019或更新版本的Chrome、Node.js、Firefox等)

yourString.matchAll( /your-regex/g ) // dont forget the "g"

MDN文档

如果您使用NPM

您可以使用官方的polyfill
npm install string.prototype.matchall

const matchAll = require('string.prototype.matchall')
console.log( [...  matchAll('blah1 blah2',/blah/g)  ] )
//[
//  [ 'blah', index: 0, input: 'blah1 blah2', groups: undefined ],
//  [ 'blah', index: 6, input: 'blah1 blah2', groups: undefined ]
//]

否则

这里有一些功能类似的复制粘贴版本

// returns an array, works on super old javascript (ES3 -- 1999)
function findAll(regexPattern, sourceString) {
    var output = []
    var match
    // auto-add global flag while keeping others as-is
    var regexPatternWithGlobal = regexPattern.global ? regexPattern : RegExp(regexPattern, regexPattern.flags+"g")
    while (match = regexPatternWithGlobal.exec(sourceString)) {
        // store the match data
        output.push(match)
        // zero-length matches will end up in an infinite loop, so increment by one char after a zero-length match is found
        if (match[0].length == 0) {
            regexPatternWithGlobal.lastIndex += 1
        }
    }
    return output
}

// this version returns an iterator, which is good for large results
// note: iterators require ES6 - 2015 standard
function* findAll(regexPattern, sourceString) {
    var match
    // auto-add global flag while keeping others as-is
    const regexPatternWithGlobal = regexPattern.global ? regexPattern : RegExp(regexPattern, regexPattern.flags+"g")
    while (match = regexPatternWithGlobal.exec(sourceString)) {
        // store the match data
        yield match
        // zero-length matches will end up in an infinite loop, so increment by one char after a zero-length match is found
        if (match[0].length == 0) {
            regexPatternWithGlobal.lastIndex += 1
        }
    }
    return output
}

使用示例:

console.log(   findAll(/blah/g,'blah1 blah2')   ) 

输出:

[ [ 'blah', index: 0 ], [ 'blah', index: 6 ] ]

由于大多数浏览器都支持 str.matchAll,因此这个答案应该在顶部列表中。 - Amit
我不建议使用那个findAll代码 - 有一个官方的polyfill可用。 - Drenai

11

基于Agus的函数,但是我更喜欢仅返回匹配值:

var bob = "&gt; bob &lt;";
function matchAll(str, regex) {
    var res = [];
    var m;
    if (regex.global) {
        while (m = regex.exec(str)) {
            res.push(m[1]);
        }
    } else {
        if (m = regex.exec(str)) {
            res.push(m[1]);
        }
    }
    return res;
}
var Amatch = matchAll(bob, /(&.*?;)/g);
console.log(Amatch);  // yeilds: [&gt;, &lt;]

8

可迭代对象更加友好:

const matches = (text, pattern) => ({
  [Symbol.iterator]: function * () {
    const clone = new RegExp(pattern.source, pattern.flags);
    let match = null;
    do {
      match = clone.exec(text);
      if (match) {
        yield match;
      }
    } while (match);
  }
});

循环中的使用:

for (const match of matches('abcdefabcdef', /ab/g)) {
  console.log(match);
}

或者如果你需要一个数组:

[ ...matches('abcdefabcdef', /ab/g) ]

1
笔误:if (m) 应该是 if (match) - Botje
数组已经是可迭代的了,所以每个返回匹配项数组的函数也都返回可迭代对象。更好的方法是,如果您在控制台记录一个数组,浏览器实际上可以打印出其内容。但是,仅记录通用可迭代对象会得到 [object Object] {...}。 - user5900250
所有的数组都是可迭代的,但并非所有的可迭代对象都是数组。如果您不知道调用者需要做什么,那么可迭代对象更为优越。例如,如果您只想要第一个匹配项,那么可迭代对象更为高效。 - sdgfsdh
你的梦想正在变成现实,浏览器正在推出支持内置的matchAll返回可迭代对象的功能:D。 - woojoo666
1
我看到了这个关于matchAll实现的回答。我写了一些支持它的浏览器JS代码,但是Node实际上不支持。这个行为与matchAll完全相同,所以我没有必要重写代码 - 干杯! - user37309

7

这是我的获取匹配项的函数:

function getAllMatches(regex, text) {
    if (regex.constructor !== RegExp) {
        throw new Error('not RegExp');
    }

    var res = [];
    var match = null;

    if (regex.global) {
        while (match = regex.exec(text)) {
            res.push(match);
        }
    }
    else {
        if (match = regex.exec(text)) {
            res.push(match);
        }
    }

    return res;
}

// Example:

var regex = /abc|def|ghi/g;
var res = getAllMatches(regex, 'abcdefghi');

res.forEach(function (item) {
    console.log(item[0]);
});

这个解决方案可以防止你忘记添加全局标志时出现无限循环。 - user68311

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