返回

Apps Script RE2 正则表达式不兼容问题及解决方案

javascript

Apps Script 中 RE2 正则表达式问题及解决方案

在 Google Apps Script 中使用 DocumentApp.findText() 函数配合正则表达式查找文档内容时,开发者有时会遇到 RE2 引擎的兼容性问题。 RE2 是一种高效、安全的正则表达式引擎,但它不支持某些高级特性,比如 Lookaround 断言(包括前瞻和后顾)。 这篇文章将深入探讨这个问题,并提供可行的解决方案。

问题分析

问题中,目标是匹配被 $$ 包裹且中间不包含 $ 字符的文本。 开发者尝试使用了包含否定前瞻断言 (?!\\$) 的正则表达式 \$\$.*(?!\$)\$\$,但 RE2 引擎不支持此语法,导致正则表达式无效。

RE2 引擎之所以不支持 Lookaround 断言,是因为其设计目标是保证线性时间复杂度,以防止正则表达式造成的拒绝服务攻击(ReDoS)。 Lookaround 断言会导致回溯,从而可能使匹配过程耗时过长,甚至失控。

解决方案

既然无法使用 Lookaround 断言,我们可以通过以下两种方法曲线救国:

1. 贪婪匹配与分组

这种方法的核心思想是利用 RE2 支持的贪婪匹配和分组捕获特性。 我们可以构造一个正则表达式,首先匹配 $$ ,然后匹配尽可能多的非 $ 字符,最后再匹配 $$。 同时使用分组捕获中间的文本。

正则表达式: \$\$([^$]+)\$\$

原理:

  • \$\$: 匹配起始的 $$
  • ([^$]+): 这是一个捕获组。
    • [^$]: 匹配除 $ 以外的任意字符。
    • +: 表示匹配一个或多个 [^$], 保证贪婪匹配到下一个 $$ 之前的内容。
  • \$\$: 匹配结束的 $$

代码示例:

function findDollarDollarText() {
  const doc = DocumentApp.getActiveDocument();
  const body = doc.getBody();
  const re2 = new RegExp("\\$\\$([^$]+)\\$\\
function findDollarDollarText() {
  const doc = DocumentApp.getActiveDocument();
  const body = doc.getBody();
  const re2 = new RegExp("\\$\\$([^$]+)\\$\\$", "g");
  let match;
  while ((match = re2.exec(body.getText())) !== null) {
    Logger.log("找到匹配: " + match[0]);  // 输出完整匹配的字符串 (包含$$)
    Logger.log("  内部文本: " + match[1]);  // 输出分组捕获的文本(不包含$$)
    let element = body.findText(match[0]).getElement();
    let startIndex = body.getChildIndex(element);
    let matchIndex = element.getText().indexOf(match[0]);
    let absIndex = doc.getBody().getChild(startIndex).getStartOffset()+matchIndex
    Logger.log("  在文档中的起始位置: " + absIndex) //计算绝对位置,这里仅统计当前Element相对文档起始位置
    Logger.log("  在文档中的结束位置: " +  (absIndex + match[0].length))
  }
}
quot;
, "g"); let match; while ((match = re2.exec(body.getText())) !== null) { Logger.log("找到匹配: " + match[0]); // 输出完整匹配的字符串 (包含$) Logger.log(" 内部文本: " + match[1]); // 输出分组捕获的文本(不包含$) let element = body.findText(match[0]).getElement(); let startIndex = body.getChildIndex(element); let matchIndex = element.getText().indexOf(match[0]); let absIndex = doc.getBody().getChild(startIndex).getStartOffset()+matchIndex Logger.log(" 在文档中的起始位置: " + absIndex) //计算绝对位置,这里仅统计当前Element相对文档起始位置 Logger.log(" 在文档中的结束位置: " + (absIndex + match[0].length)) } }

操作步骤:

  1. 打开 Google Apps Script 编辑器。
  2. 将以上代码复制粘贴到编辑器中。
  3. 测试文本 替换为你的文档内容进行测试。
  4. 保存脚本并运行 findDollarDollarText 函数。
  5. 查看执行日志,即可看到匹配结果。

安全建议: 尽管这个方案在大多数情况下有效,但如果输入文本中存在大量的 $ 字符, 仍然可能会导致性能下降。 考虑对输入文本长度进行限制, 或增加更严格的过滤条件,避免潜在的性能问题。

2. 分步处理

另一种方法是将匹配过程分为两个步骤:

  1. 先使用简单的正则表达式 \$\$[^\$]*\$\$ 粗略匹配可能包含 $ 的文本段。 这个正则表达式匹配 $$ 之间包含任意数量非 $ 字符的文本段。
  2. 然后对每个匹配结果进行二次校验, 检查其中是否包含 $ 。 如果不包含,则认为这是一个有效的匹配。

代码示例:

function findDollarDollarTextStepwise() {
  const doc = DocumentApp.getActiveDocument();
  const body = doc.getBody();
  const re2_1 = new RegExp("\\$\\$[^$]*\\$\\
function findDollarDollarTextStepwise() {
  const doc = DocumentApp.getActiveDocument();
  const body = doc.getBody();
  const re2_1 = new RegExp("\\$\\$[^$]*\\$\\$", "g"); //宽松匹配
  let text = body.getText()
  let match1;
  while ((match1 = re2_1.exec(text)) !== null) {
      let match = match1[0]
      if(!match.includes("$",2,match.length-2)){   //检查内部是否包含$ ,排除开头和结尾的$$
        Logger.log("找到匹配: " + match);
        let element = body.findText(match).getElement();
        let startIndex = body.getChildIndex(element);
        let matchIndex = element.getText().indexOf(match);
        let absIndex = doc.getBody().getChild(startIndex).getStartOffset()+matchIndex
        Logger.log("  在文档中的起始位置: " + absIndex); //计算绝对位置
        Logger.log("  在文档中的结束位置: " +  (absIndex + match.length));
        }

  }
}
quot;
, "g"); //宽松匹配 let text = body.getText() let match1; while ((match1 = re2_1.exec(text)) !== null) { let match = match1[0] if(!match.includes("
function findDollarDollarTextStepwise() {
  const doc = DocumentApp.getActiveDocument();
  const body = doc.getBody();
  const re2_1 = new RegExp("\\$\\$[^$]*\\$\\$", "g"); //宽松匹配
  let text = body.getText()
  let match1;
  while ((match1 = re2_1.exec(text)) !== null) {
      let match = match1[0]
      if(!match.includes("$",2,match.length-2)){   //检查内部是否包含$ ,排除开头和结尾的$$
        Logger.log("找到匹配: " + match);
        let element = body.findText(match).getElement();
        let startIndex = body.getChildIndex(element);
        let matchIndex = element.getText().indexOf(match);
        let absIndex = doc.getBody().getChild(startIndex).getStartOffset()+matchIndex
        Logger.log("  在文档中的起始位置: " + absIndex); //计算绝对位置
        Logger.log("  在文档中的结束位置: " +  (absIndex + match.length));
        }

  }
}
quot;
,2,match.length-2)){ //检查内部是否包含$ ,排除开头和结尾的$ Logger.log("找到匹配: " + match); let element = body.findText(match).getElement(); let startIndex = body.getChildIndex(element); let matchIndex = element.getText().indexOf(match); let absIndex = doc.getBody().getChild(startIndex).getStartOffset()+matchIndex Logger.log(" 在文档中的起始位置: " + absIndex); //计算绝对位置 Logger.log(" 在文档中的结束位置: " + (absIndex + match.length)); } } }

原理:

  • \$\$[^$]*\$\$ : 这个表达式匹配以 $$ 开始和结束,中间包含任意数量非 $ 字符的文本段。它是一个相对宽松的匹配,会包含一些不符合最终条件的文本段。
  • !match.includes("$",2,match.length-2): 对每一个粗略匹配的结果,排除掉开头结尾的 $$ 字符, 检查其中是否包含 $。 只有不包含 $ 的匹配结果才是我们真正想要的。

操作步骤:

  1. 打开 Google Apps Script 编辑器。
  2. 将以上代码复制粘贴到编辑器中。
  3. 保存脚本并运行 findDollarDollarTextStepwise 函数。
  4. 查看执行日志,即可看到匹配结果。

安全建议: 分步处理的方式可以有效避免性能问题。 通过第一步的粗略匹配,可以缩小需要二次校验的文本范围,从而提高效率。

总结

处理 Google Apps Script 中 RE2 正则表达式问题时,关键在于理解 RE2 引擎的限制,并找到合适的替代方案。 本文介绍的两种方法:贪婪匹配与分组、分步处理, 都能有效地解决问题。 开发者可以根据实际情况选择合适的方法, 并注意潜在的性能问题,采取相应的安全措施。 通过巧妙地运用 RE2 的特性,我们可以在 Apps Script 中编写出高效且安全的正则表达式代码。

相关资源