JavaScript 在正确出现的位置映射具有多个匹配项的正则表达式

JavaScript map a regex with multiple matches at the right occurrence

我有一个 array 个要映射的标记,以及一个获取输入句子中每个标记的开始和结束位置的正则表达式。当令牌出现一次时,这可以正常工作。当token多次出现时,贪心算法Regex会获取该token在文本中的所有匹配位置,因此第i个token出现的结果位置将映射到最后找到的位置。

例如,给定文本

var text = "Steve down walks warily down the street down\nWith the brim pulled way down low";

标记 down 的第一次出现被映射到与 RegExp 匹配的文本中的最后一个位置,因此我有:

 {
    "index": 2,
    "word": "down",
    "characterOffsetBegin": 70,
    "characterOffsetEnd": 73
  }

这就清楚了运行这个例子:

var text = "Steve down walks warily down the street down\nWith the brim pulled way down low";
var tokens = text.split(/\s+/g);
var annotations = tokens.map((word, tokenIndex) => { // for each token
  let item = {
    "index": (tokenIndex + 1),
    "word": word
  }
  var wordRegex = RegExp("\b(" + word + ")\b", "g");
  var match = null;
  while ((match = wordRegex.exec(text)) !== null) {
    var wordStart = match.index;
    var wordEnd = wordStart + word.length - 1;
    item.characterOffsetBegin = wordStart;
    item.characterOffsetEnd = wordEnd;
  }
  return item;
});
console.log(annotations)

第一次出现的标记 down 应该是第一个匹配位置:

 {
    "index": 2,
    "word": "down",
    "characterOffsetBegin": 6,
    "characterOffsetEnd": 9
  }

所以假设我已经为文本中每次出现的标记映射了标记位置,即第一次出现 down 与第一个匹配项,第二个与第二个匹配项等。我可以重建文本相应地 charOffsetBegincharOffsetEnd 因此这样做:

                var newtext = '';
                results.sentences.forEach(sentence => {
                    sentence.tokens.forEach(token => {
                        newtext += text.substring(token.characterOffsetBegin, token.characterOffsetEnd + 1) + ' ';
                    });
                    newtext += '\n';
                });

问题不在于表达式是贪婪的,而是您正在使用 while 循环寻找输入字符串中标记的 每个 匹配项。

你必须做两件事:

  • 找到匹配项后停止迭代。
  • 跟踪以前的比赛以便您可以忽略它们。

我相信这就是你想要的:

var text = "Steve down walks warily down the street down\nWith the brim pulled way down low";
var tokens = text.split(/\s+/g);
const seen = new Map();

var annotations = tokens.map((word, tokenIndex) => { // for each token
  let item = {
    "index": (tokenIndex + 1),
    "word": word
  }
  var wordRegex = RegExp("\b(" + word + ")\b", "g");
  var match = null;
  while ((match = wordRegex.exec(text)) !== null) {
    if (match.index > (seen.get(word) || -1)) {
      var wordStart = match.index;
      var wordEnd = wordStart + word.length - 1;
      item.characterOffsetBegin = wordStart;
      item.characterOffsetEnd = wordEnd;

      seen.set(word, wordEnd);
      break;
    }
  }
  return item;
});
console.log(annotations)

seen 地图跟踪标记的最近匹配的结束位置。

由于无法告诉正则表达式引擎忽略特定位置之前的所有内容,我们仍在使用 while 循环,但忽略了上一次匹配之前发生的任何匹配,使用 if (match.index > (seen.get(word) || -1)).

@Felix 的回答涵盖了您问题的原因,但我想更进一步。

我会将所有内容都放在 class(或构造函数)中以保持其包含,并将用于从每个标记的文本中提取匹配项的逻辑与标记的迭代分开。

class Annotations {
  constructor(text) {
    if(typeof text !== 'string') return null
    const opt = { enumerable: false, configurable: false, writeable: false }
    Object.defineProperty(this, 'text', { value: text, ...opt })
    Object.defineProperty(this, 'tokens', { value: text.split(/\s+/g), ...opt })
    for(let token of this.tokens) this[token] = Array.from(this.matchAll(token))
  }
  * matchAll(token) {
    if(typeof token === 'string' && this.text.indexOf(token) > -1) {
      const expression = new RegExp("\b" + token + "\b", "g")
      let match = expression.exec(this.text)

      while(match !== null) {
        const start = match.index
        const end = start + token.length - 1
        yield { start, end }
        match = expression.exec(this.text)
      }
    }
  }
}

const annotations = new Annotations("Steve down walks warily down the street down\nWith the brim pulled way down low")

console.log(annotations.text)
console.log(annotations.tokens)
console.log(annotations)
console.log(Array.from(annotations.matchAll('foo'))) // []
.as-console-wrapper { max-height: 100% !important }