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
与第一个匹配项,第二个与第二个匹配项等。我可以重建文本相应地 charOffsetBegin
和 charOffsetEnd
因此这样做:
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 }
我有一个 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
与第一个匹配项,第二个与第二个匹配项等。我可以重建文本相应地 charOffsetBegin
和 charOffsetEnd
因此这样做:
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 }