【问题标题】:Efficient Regexp matching starting from given index within string从字符串中的给定索引开始的高效正则表达式匹配
【发布时间】:2017-01-18 15:43:26
【问题描述】:

我已经解析了一个字符串到索引idx。我的下一个解析步骤使用正则表达式。它需要匹配字符串的下一部分,即从位置idx 开始。如何有效地做到这一点?

例如:

let myString = "<p>ONE</p><p>TWO</p>"
let idx

// some code not shown here parses the first paragraph
// and updates idx
idx = 10

// next parse step must continue from idx 
let myRegex = /<p>[^<]*<\/p>/
let subbed = myString.substring(idx)
let result = myRegex.exec(subbed)
console.log(result) // "<p>TWO</p>", not "<p>ONE</p>"

myString.substring(idx) 似乎是一项相当昂贵的操作。

有没有像这样的正则表达式操作:result = myRegex.execFromIndex(idx, myString);

一般来说,我想从不同的索引开始正则表达式匹配,这样我就可以排除部分字符串并避免已经解析的匹配。所以一次它可以来自 myString[0] 另一次来自 myString[51] 等等。

有没有办法有效地做到这一点?我正在解析数十万行,并希望以尽可能便宜的方式执行此操作。

【问题讨论】:

  • 错字myString.length
  • 如果你真的很在意效率,尽量不要使用正则表达式。
  • 构造正则表达式实例,然后设置其.lastIndex 属性。 Read the documentation.
  • @Faibbus 你有什么建议呢?我很确定我自己写不出比正则表达式更有效的搜索。
  • 如果您提供您决定只需要从某个索引搜索的逻辑,这个问题可能会得到更好的答案。

标签: javascript regex parsing


【解决方案1】:

JavaScript 正则表达式有一个lastIndex 属性,在Regexp.exec() 中用作占位符,其中包含最后一个匹配项的索引,表明它知道下一步从哪里开始。所以设置myRegex.lastIndex = 3应该可以解决你的问题。

它比 substring 方法更有效,因为它不需要创建额外的变量,并且设置 lastIndex 属性可能比做 substring 更快。其他一切都与您所做的完全相同。

下面是一个测试,因为它表明设置 lastIndex 将产生与首先执行 substring 相同的结果。

var result1Elem = document.getElementById('result1');
var result2Elem = document.getElementById('result2');
var runBtn = document.getElementById('RunBtn');
runBtn.addEventListener("click", runTest);
function runTest() {
  var substrStart = +document.getElementById('substrStartText').value
  var myRegex1 = new RegExp(document.getElementById('regexText').value, 'g');
  myRegex1.lastIndex = substrStart;
  var myRegex2 = new RegExp(document.getElementById('regexText').value, 'g');

  var myString1 = document.getElementById('testText').value;
  var myString2 = myString1.substring(3);
  
  var result;
  
  var safety = 0;
  while ((result = myRegex1.exec(myString1)) !== null) {
    result1Elem.innerHTML += '<li>' + result[0] + ' at ' + result.index + '</li>';
    if (safety++ > 50) break;
  }
  
  safety = 0;
  while ((result = myRegex2.exec(myString2)) !== null) {
    result2Elem.innerHTML += '<li>' + result[0] + ' at ' + (result.index + substrStart)  + '</li>';
    if (safety++ > 50) break;
  }
}
<table>
<tr><td>Test </td><td> <input type="text" value="Hello World" id="testText" /></td></tr>
<tr><td>Regex </td><td> <input type="text" value="l." id="regexText" /></td></tr>
<tr><td>Substring Start </td><td> <input type="text" value="3" id="substrStartText" /></td></tr>
<tr><td colspan="2"><button id="RunBtn">Run</button></td></tr>
</table>

<table style="width:100%">
  <tr style="font-weight:bold; background:#ccc">
    <td>Results of Regex with lastIndex = 3</td>
    <td>Results of string substringged</td>
  </tr>
  <tr>
    <td><ul id="result1"></ul></td>
    <td><ul id="result2"></ul></td>
  </tr>
<table>

【讨论】:

  • 谢谢,但这不是我所需要的,因为我没有在行的开头进行任何正则表达式匹配,并且 lastIndex 将为 0
  • 我对出了什么问题感到有些困惑。从索引 51 做一个子字符串并将 lastIndex 设置为 51 将产生相同的结果
  • 是的,但它有效吗?由于我要解析数十万行,因此我需要它非常快速且内存高效。我的问题是是否有比使用子字符串更好的选择。
  • 奇怪的线程。这是一个非常有效的问题,几乎没有任何赞成票,这似乎是一个很好的答案,但提出问题的人没有意识到这一点?我的问题是如果lastIndex 是标准化的?但似乎肯定是:tc39.es/ecma262/#sec-lastindex
  • 哇,感谢@StijndeWitt 在这么多年后将我的注意力转向这个问题!看到我的初学者 cmets 有点畏缩 :) 我相信答案中的正则表达式示例将解析整个字符串输入,然后将 lastIndex 移动到起始位置,但我的目标是不必在开始保存之前解析部分一些表现。并且当使用子字符串时,它会创建一个字符串的副本,这也会降低性能。仅供参考,我现在使用 Rust,我可以完全控制并且可以使用字符串切片来访问字符串的任何部分,并且是零拷贝 :)
【解决方案2】:

使用Regex.execlastIndex

Regexp支持你想做的事情:

  1. 设置正则表达式y or g flag
    • 使用y 标志,匹配必须准确地从指定的开始索引开始
    • 使用g 标志,匹配可以发生在指定索引之后的任何位置
  2. 将其lastIndex 属性设置为起始索引
  3. 致电exec

我已将上述步骤应用于您的示例代码:

let myString = "<p>ONE</p><p>TWO</p>"
let idx

// some code not shown here parses the first paragraph
// and updates idx
idx = 10

// next parse step must continue from idx 
let myRegex = /<p>[^<]*<\/p>/y  // ?note the 'y' flag!?
myRegex.lastIndex = idx
let result = myRegex.exec(myString)
console.log(result) // "<p>TWO</p>", not "<p>ONE</p>"

另一件有用的事情是exec 将更新lastIndex 以指向字符串中的位置 返回的匹配项之后。这使您可以做很多事情,包括:

  1. 重新运行相同的正则表达式,它将自动找到最后一个匹配之后的下一个匹配。
  2. 如果您要解析的下一个内容具有不同的模式,请将 lastIndex 值转移到不同的正则表达式。
  3. lastIndex 值复制到非正则表达式解析所使用的变量中。
  4. lastIndex 返回给函数的调用者,以便调用者可以根据需要继续处理字符串的其余部分。

为什么string.slicesubstring 也是很好的解决方案

myString.substring(idx) 似乎是一项相当昂贵的操作。

不一定如此!尽管它们可能不会像 Rust 那样快,但所有领先的 Javascript 引擎(SpiderMonkey、V8、JavaScriptCore)都可以做到what you describe for Rust。他们在幕后优化string.slicesubstring,使用指向源字符串的指针而不是复制。

Adventures in the land of substrings and RegExps 有很多很棒的细节、图片和分析,但它已经有五年历史了,从那以后情况可能会变得更好。这个 StackOverflow 问题有答案:Is Javascript substring virtual?

【讨论】:

  • 这个答案做得很好
  • @StijndeWitt 谢谢!我刚刚添加了一些附加信息来解决 OP 对使用 substring 的担忧。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-09
相关资源
最近更新 更多