在Google表格公式中多个正则表达式匹配

23

我正在尝试使用 Google Sheets 的正则表达式公式,在给定的字符串中(比如单元格 A1 中)获取破折号前面的所有数字列表:

=REGEXEXTRACT(A1, "\d-")

我的问题是它只返回第一个匹配项... 如何获取所有匹配项

示例文本:

"A1-Nutrition;A2-ActPhysiq;A2-BioMeta;A2-Patho-jour;A2-StgMrktg2;H2-Bioth2/EtudeCas;H2-Bioth2/Gemmo;H2-Bioth2/Oligo;H2-Bioth2/Opo;H2-Bioth2/Organo;H3-Endocrino;H3-Génétiq"

我的公式返回1-,而我想得到1-2-2-2-2-2-2-2-2-2-3-3-(可以是数组或拼接的文本)。

我知道我可以使用脚本或另一个函数(如SPLIT)来实现所需的结果,但我真正想知道的是如何让re2正则表达式在“REGEX.*” Google Sheets公式中返回这样的多个匹配项。 类似于regex101.com上的“全局 - 不要在第一次匹配后返回”选项。

我还尝试使用REGEXREPLACE删除不需要的文本,但也没有成功(我无法摆脱不在连字符之前的其他数字)。

任何帮助都将不胜感激! 谢谢:)


1
我认为你在这方面运气不佳。文档中说只返回第一个... - dawg
1
@dawg 谢谢,我同意,但我认为必须有一种方法来定义正确的正则表达式以获取所有匹配项;例如,如果我使用 ((\d-)),我会得到前两个匹配项,使用 (((\d-))) 我会得到前三个,但如何获取它们所有,不知道有多少个?也许使用组名 \1,允许任何匹配重复,用 .* 分隔...或者至少通过组合多个 REGEXEXTRACT 和/或 REGEXREPLACE 公式。 - flo5783
1
我尝试使用(?g),它适用于某些re flavor但不适用于re2。这是一个很棒的问题。 - dawg
2
我喜欢使用捕获组的两个答案。更简单的是,=regexreplace(A1,"(\d-)|.","$1") 似乎也可以工作。 - Tom Sharpe
3
我认为 =regexreplace(A1,"(\d-)|.","$1") 到目前为止是最好的,谢谢!简单高效 :) - flo5783
8个回答

24
你实际上可以使用regexreplace在单个公式中完成这个操作,将所有值用一个捕获组括起来而不是替换文本:
=join("",REGEXEXTRACT(A1,REGEXREPLACE(A1,"(\d-)","($1)")))

基本上它所做的是将所有\d-实例用"捕获组"包围起来,然后使用正则表达式提取,它会整洁地返回所有捕获内容。如果您想将其合并回单个字符串,只需使用join将其打包回单个单元格即可:

输入图像描述


无法让它正常工作。regexextract 的第二段期望一个字符串,而 regexreplace 的输出与 A1 不匹配。 - Pacerier
这太棒了。@Pacerier 这里有个提示:在单独的单元格中执行RegexReplace操作。确保输出与原始字符串完全相同,只是需要捕获的项目周围加上括号()。一旦达到这个目标,就可以像Aurielle提到的那样将结果放入RegexReplace中。 - JDG
顺便提一下,如果这个方法不起作用,那很可能是因为你的字符串中有一些额外的字符让正则表达式函数感到困惑,比如“[”,“(”,“?”,“+”等。首先尝试去掉它们! - 190290000 Ruble Man
迄今为止最佳答案!有没有什么办法可以排除完全不匹配的单元格?使用当前的公式,如果完全没有匹配项,则整个单元格将被返回。 - Kostanos
这很棒。我可以通过将另一个单元格指定为正则表达式的输入,使其适用于几乎任何内容。例如,如果我想要查找我的单元格是否包含类似于foo、bar、hong或dong的任何/所有实例,我只需要确保我在此公式的副本中引用的单元格包含(foo|bar|hong|dong)。通过这种方式,我可以使用填充向下功能,在多个行上进行多次检查/提取所需内容。 - colorful-shirts
显示剩余2条评论

11

您可以在脚本编辑器中创建自己的自定义函数:

function ExtractAllRegex(input, pattern,groupId) {
  return [Array.from(input.matchAll(new RegExp(pattern,'g')), x=>x[groupId])];
}

或者,如果您需要返回单个单元格中用某个分隔符连接的所有匹配项:

function ExtractAllRegex(input, pattern,groupId,separator) {
  return Array.from(input.matchAll(new RegExp(pattern,'g')), x=>x[groupId]).join(separator);
}

然后,只需像这样调用它:=ExtractAllRegex(A1, "\d-", 0, ", ")

描述

  • input - 当前单元格的值
  • pattern - 正则表达式模式
  • groupId - 您想要提取的捕获组ID
  • separator - 用于连接匹配结果的文本。

1
感谢您的努力和清晰的回答,尽管我在原帖中提到我并不寻求基于脚本的解决方案。 - flo5783
1
@flo5783 你写道:“我知道我可以使用脚本”,但是你没有提供这个脚本。这是为了向那些想要遵循这个解决方案的人展示这种替代方法。由于 REGEXEXTRACT 不支持多重匹配,所有那些基于 REGEXREPLACE 的解决方案都是变通方法,每次都需要调整以适应每种情况。这个小函数是一个通用解决方案,弥补了缺失的功能。 - Wiktor Stribiżew
3
谢谢,这就做到了。即使 OP 不想要一个脚本,对于其他任何遇到这个问题的人来说,这似乎是唯一的“真正”解决方案。 - degreesightdc
@WiktorStribiżew 当我尝试将您的函数添加到脚本编辑器时,我遇到了以下错误:TypeError: Cannot read property 'matchAll' of undefined (line 2, file "Code")Dismiss您认为这个问题应该如何解决?谢谢! - linguist_at_large
再次感谢@WiktorStribiżew,但现在我遇到了以下问题:SyntaxError: Unexpected token ')' (line 4, file "Code.gs") - linguist_at_large
1
@linguist_at_large matchAll 在旧的 JS 环境中不受支持,因此您可以使用以下代码(https://jsfiddle.net/wiktor_stribizew/eughbf5L/2/):`function ExtractAllRegex(input, pattern,groupId,separator) { var m, results = []; var rx = new RegExp(pattern,'g'); while (m=rx.exec(input)) { results.push(m[groupId]); } return results.join(separator); }` - Wiktor Stribiżew

7

编辑

我提出了一个更通用的解决方案:

=regexreplace(A1,"(.)?(\d-)|(.)","$2")

它将除第二组匹配项(\d-)之外的任何文本替换为第二组 $2

"(.)?(\d-)|(.)"
  1    2    3  
  Groups are in ()
  ---------------------------------------
 "$2" -- means return the group number 2

学习正则表达式:https://regexone.com


尝试这个公式:

=regexreplace(regexreplace(A1,"[^\-0-9]",""),"(\d-)|(.)","$1")

它可以处理这样的字符串:

"A1-Nutrition;A2-ActPhysiq;A2-BioM---eta;A2-PH3-Généti***566*9q"

并输出:

1-2-2-2-3-


有人能解释一下这个公式吗? - Fabian
@Fabian 我已经添加了一个简短的描述。如果想深入学习,我建议使用正则表达式进行学习。 - Max Makhrov
谢谢@Max Makhrov! - Fabian
2
为什么要捕获第1组和第3组?简化版:=regexreplace(A1,".?(\d-)|.", "$1") - Peter Thoeny
@Lod:我已经添加了我的答案和解释:https://dev59.com/31cQ5IYBdhLWcg3wCfWS#75465316 - Peter Thoeny
显示剩余3条评论

5
我可以帮忙翻译这段中文。以下是需要翻译的内容:

我没能使被接受的答案适用于我的情况。虽然我想用那种方法,但我需要一个快速解决方案,所以我选择了以下方式:

输入:

1111 days, 123 hours 1234 minutes and 121 seconds

预期输出:

1111 123 1234 121

公式:

=split(REGEXREPLACE(C26,"[a-z,]"," ")," ")

1
不同的问题,不同的解决方案。 你的情况比较简单。 而在我的情况下,当其没有前缀连字符时,其他数字将被忽略。 - flo5783

4

最短的正则表达式:

=regexreplace(A1,".?(\d-)|.", "$1")

对于 "A1-Nutrition;A2-ActPhysiq;A2-BioMeta;A2-Patho-jour;A2-StgMrktg2;H2-Bioth2/EtudeCas;H2-Bioth2/Gemmo;H2-Bioth2/Oligo;H2-Bioth2/Opo;H2-Bioth2/Organo;H3-Endocrino;H3-Génétiq",应返回 1-2-2-2-2-2-2-2-2-2-3-3-
正则表达式的解释如下:
  • .? -- 可选字符
  • (\d-) -- 捕获第一组数字后跟破折号的内容 (使用(\d+-)指定多个数字)
  • | -- 或者
  • . -- 任意字符
  • 替换部分"$1" 只使用第一组捕获内容,并丢弃其他内容。
了解更多关于正则表达式的知识: https://twiki.org/cgi-bin/view/Codev/TWikiPresentation2018x10x14Regex

非常感谢您的帮助。在您的评论的帮助下,我终于在半天的搜索后找到了适用于我的用例的代码 https://stackoverflow.com/a/75452891/10789707。祝一切顺利! - Lod
1
我的项目已经晚了5年,但这种答案对于人生仍然很有用。谢谢! :-) - flo5783

2

这似乎有效,我已经尝试过验证。

逻辑是:

(1)用空格替换连字号后面的字母。

(2)用空格替换没有连字号跟随的任何数字。

(3)用空格替换除了数字或连字符之外的所有内容。

=regexreplace(A1,"[a-zA-Z]-|[0-9][^-]|[a-zA-Z;/é]","")

结果

1-2-2-2-2-2-2-2-2-2-3-3-

分析

我必须逐步进行这些过程,以确信它是正确的。根据这个参考资料,当有用管道符号分隔的替代项时,正则表达式应该按照从左到右的顺序匹配它们。上面的公式如果规则1不在最前面就不能正常工作(否则在规则1生效之前会将除数字或连字符外的所有字符都转换为空值,从“Patho-jour”得到一个额外的连字符)。

以下是一些我认为它必须处理文本的示例:

enter image description here


谢谢,我仍然希望有一种更优雅或通用的解决方案来适配 re2 中的所有匹配,但在我的情况下,这确实是一个很好的解决方法,我非常感谢详细的解释和研究! - flo5783
谢谢,非常感谢。 - Tom Sharpe

1

有两种一般的(“Excel”/“本地”/非应用脚本)解决方案来返回正则表达式匹配项的数组,以REGEXEXTRACT的方式:

方法1)

在匹配项周围插入分隔符,删除垃圾,并调用SPLIT

正则表达式通过从左到右迭代字符串并“消耗”来工作。如果我们小心地消耗垃圾值,我们可以将它们丢弃。

(这避免了当前接受的解决方案面临的问题,即如Carlos Eduardo Oliveira所提到的,如果语料库文本包含特殊的正则表达式字符,则显然会失败。)

首先,我们要选定一个分隔符,它必须在文本中不存在。正确的做法是解析文本并临时替换我们的分隔符为“临时分隔符”,例如如果我们要使用逗号",",我们首先将所有现有的逗号替换为"<<QUOTED-COMMA>>",然后再取消替换。但是,为了简单起见,我们将从私人使用的Unicode块(private-use unicode blocks)中选择一个随机字符作为我们的特殊分隔符,例如(注意它是2个字节...谷歌电子表格可能无法以一致的方式计算字形中的字节,但我们稍后会小心处理)。

=SPLIT(
  LAMBDA(temp,
    MID(temp, 1, LEN(temp)-LEN(""))
  )(
    REGEXREPLACE(
      "xyzSixSpaces:[      ]123ThreeSpaces:[   ]aaaa 12345",".*?(   |$)",
      "$1"
    )
  ),
  ""
)

我们只需使用lambda定义temp="match1match2match3",然后使用它将最后一个分隔符删除为"match1match2match3",然后SPLIT它。

对结果进行COLUMNS操作将证明返回了正确的结果,即{" ", " ", " "}

这是一个特别好的函数可以转换为命名函数,并将其命名为REGEXGLOBALEXTRACT(text,regex)REGEXALLEXTRACT(text,regex),例如:

=SPLIT(
  LAMBDA(temp,
    MID(temp, 1, LEN(temp)-LEN(""))
  )(
    REGEXREPLACE(
      text, 
      ".*?("&regex&"|$)", 
      "$1"
    )
  ),
  ""
)

方法二) 使用递归
使用LAMBDA(即像其他编程语言一样定义函数)可以使用lambda演算和函数式编程中的一些技巧:你可以访问递归。定义递归函数很困惑,因为没有简单的方式让它引用自身,所以必须使用一个技巧/约定:
递归函数的技巧:实际上定义一个需要引用其自身的函数f,而是定义一个具有参数itself并返回您实际想要的函数的函数;将此“约定”传递给Y-combinator,使其转化为实际递归函数。
管道采取这种功能的技术被称为Y组合子。如果您具有一些编程背景,这是一个好的文章good article to understand it

举个例子,要得到5的阶乘(即实现我们自己的FACT(5)),我们可以定义:

命名函数Y(f)=LAMBDA(f, (LAMBDA(x,x(x)))( LAMBDA(x, f(LAMBDA(y, x(x)(y)))) ) )(这是Y组合器,很神奇;你不必理解它就能使用它)

命名函数MY_FACTORIAL(n)=

Y(LAMBDA(self,

  LAMBDA(n, 
    IF(n=0, 1, n*self(n-1))
  )

))

MY_FACTORIAL(5)的结果为:120

Y组合子使得编写递归函数看起来相对容易,就像编程课程的入门一样。我使用命名函数以增加清晰度,但你也可以将它们全部混在一起,只是会牺牲一些理智...

=LAMBDA(Y,
  Y(LAMBDA(self, LAMBDA(n, IF(n=0,1,n*self(n-1))) ))(5)
)(
  LAMBDA(f, (LAMBDA(x,x(x)))( LAMBDA(x, f(LAMBDA(y, x(x)(y)))) ) )
)

这如何应用于手头的问题?递归解决方案如下所示:
在下面的伪代码中,我使用“function”代替LAMBDA,但它是相同的东西:
// code to get around the fact that you can't have 0-length arrays
function emptyList() {
  return {"ignore this value"}
}
function listToArray(myList) {
  return OFFSET(myList,0,1)
}

function allMatches(text, regex) {
  allMatchesHelper(emptyList(), text, regex)
}

function allMatchesHelper(resultsToReturn, text, regex) {
  currentMatch = REGEXEXTRACT(...)
  if (currentMatch succeeds) {
    textWithoutMatch = SUBSTITUTE(text, currentMatch, "", 1)
    return allMatches(
      {resultsToReturn,currentMatch}, 
      textWithoutMatch, 
      regex
    )
  } else {
    return listToArray(resultsToReturn)
  }
}

很遗憾,递归方法的增长阶数是二次的(因为它一遍又一遍地将结果附加到自身上,同时使用较小的字符串片段重建巨大的搜索字符串,所以 1+2+3+4+5+... = big^2,这可能需要很长时间),所以如果你有很多匹配项的话,可能会变得很慢。为了速度起见,最好在正则表达式引擎内部保持,因为它很可能经过高度优化。

当然,你可以通过使用 LAMBDA(varName, expr)(varValue) 进行临时绑定来避免使用命名函数,如果你想在表达式中使用 varName。(你可以将该模式定义为一个命名函数 =cont(varValue),以倒转参数顺序以保持代码更清晰,也可以不这样做。)

  • 每当我使用 varName = varValue,就写成那样。
  • 要检查匹配是否成功,使用 ISNA(...)

它会看起来像这样:

命名函数 allMatches(resultsToReturn, text, regex): 未经测试:

LAMBDA(helper,
  OFFSET(
    helper({"ignore"}, text, regex),
  0,1)
)(

  Y(LAMBDA(helperItself,

  LAMBDA(results, partialText,
    LAMBDA(currentMatch,
      IF(ISNA(currentMatch),
        results,
        LAMBDA(textWithoutMatch,
          helperItself({results,currentMatch}, textWithoutMatch)
        )(
          SUBSTITUTE(partialText, currentMatch, "", 1)
        )
      )
    )(
      REGEXEXTRACT(partialText, regex)
    )
  )

  ))
)

0

使用RegexReplace捕获组,然后进行RegexExtract的解决方案在这里也适用,但有一个问题。

=join("",REGEXEXTRACT(A1,REGEXREPLACE(A1,"(\d-)","($1)")))

如果您尝试获取的单元格中包含特殊字符,例如括号“(”或问号“?”提供的解决方案将无法工作。

在我的情况下,我试图列出单元格中包含的所有“变量文本”。这些“变量文本”是以这种方式写入的:“{example_name}”。但是单元格的完整内容包含特殊字符,导致正则表达式公式出错。当我删除这些特殊字符时,我可以像解决方案一样列出所有捕获的组。


这听起来像是对被接受答案的评论? - Jeremy Caney
这并没有提供问题的答案。一旦您拥有足够的 声望,您就可以 评论任何帖子;相反,提供不需要询问者澄清的答案。- 来自审核 - Rabinzel
我已在新答案中解决了这个问题 https://dev59.com/31cQ5IYBdhLWcg3wCfWS#74683624。不需要回复,但我想在这里发表评论,以防将来有用。干杯。 - ninjagecko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接