Apps Script RE2 正则表达式不兼容问题及解决方案
2024-12-17 03:30:00
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))
}
}
操作步骤:
- 打开 Google Apps Script 编辑器。
- 将以上代码复制粘贴到编辑器中。
- 将
测试文本
替换为你的文档内容进行测试。 - 保存脚本并运行
findDollarDollarText
函数。 - 查看执行日志,即可看到匹配结果。
安全建议: 尽管这个方案在大多数情况下有效,但如果输入文本中存在大量的 $
字符, 仍然可能会导致性能下降。 考虑对输入文本长度进行限制, 或增加更严格的过滤条件,避免潜在的性能问题。
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)
: 对每一个粗略匹配的结果,排除掉开头结尾的$$
字符, 检查其中是否包含$
。 只有不包含$
的匹配结果才是我们真正想要的。
操作步骤:
- 打开 Google Apps Script 编辑器。
- 将以上代码复制粘贴到编辑器中。
- 保存脚本并运行
findDollarDollarTextStepwise
函数。 - 查看执行日志,即可看到匹配结果。
安全建议: 分步处理的方式可以有效避免性能问题。 通过第一步的粗略匹配,可以缩小需要二次校验的文本范围,从而提高效率。
总结
处理 Google Apps Script 中 RE2 正则表达式问题时,关键在于理解 RE2 引擎的限制,并找到合适的替代方案。 本文介绍的两种方法:贪婪匹配与分组、分步处理, 都能有效地解决问题。 开发者可以根据实际情况选择合适的方法, 并注意潜在的性能问题,采取相应的安全措施。 通过巧妙地运用 RE2 的特性,我们可以在 Apps Script 中编写出高效且安全的正则表达式代码。
相关资源
- Google Apps Script 文档: https://developers.google.com/apps-script/
- RE2 正则表达式语法: https://github.com/google/re2/wiki/Syntax
- Google Docs API: https://developers.google.com/docs/api