替换多个(3个以上)大写字母之间的空格

15

我有一些文本,人们在其中使用大写字母并在它们之间加上空格以突出子字符串。我想替换这些子字符串之间的空格。此模式的规则为:“至少有3个连续的大写字母,每个字母之间有一个空格”。

我很好奇如何纯粹使用正则表达式或者使用gsubfn包来实现这个目标,因为我认为这对于它来说应该是一个很容易的任务,但在下面的MWE示例中我却失败了,并且出现了额外的字母(我很好奇为什么会发生这种情况)。

MWE

x <- c(
    'Welcome to A I: the best W O R L D!',
    'Hi I R is the B O M B for sure: we A G R E E indeed.'
)

## first to show I have the right regex pattern
gsub('(([A-Z]\\s+){2,}[A-Z])', '<FOO>', x)
## [1] "Welcome to A I: the best <FOO>!"               
## [2] "Hi I R is the <FOO> for sure: we <FOO> indeed."

library(gsubfn)
spacrm1 <- function(string) {gsub('\\s+', '', string)}
gsubfn('(([A-Z]\\s+){2,}[A-Z])', spacrm1, x)
## Error in (function (string)  : unused argument ("L ")
## "Would love to understand why this error is happening"

spacrm2 <- function(...) {gsub('\\s+', '', paste(..., collapse = ''))}
gsubfn('(([A-Z]\\s+){2,}[A-Z])', spacrm2, x)
## [1] "Welcome to A I: the best WORLDL!"               
## [2] "Hi I R is the BOMBM for sure: we AGREEE indeed."
## "Would love to understand why the extra letter is happening"

期望的输出

[1] "Welcome to A I: the best WORLD!"                 
[2] "Hi I R is the BOMB for sure: we AGREE indeed."

1
正则表达式有两个捕获组,但是第一个 gsubfn 调用中的函数只有一个参数。它应该为每个捕获组提供一个参数,即两个参数。尝试使用以下代码查看它正在传递什么:gsubfn('(([A-Z]\\s+){2,}[A-Z])', ~ print(list(...)), x) - G. Grothendieck
是的,看起来参数数量不匹配了。如果你使用 spacrm2gsubfn('((?:[A-Z]\\s+){2,}[A-Z])', spacrm2, x) ,结果就会如预期一样。 - Wiktor Stribiżew
@WiktorStribiżew能否给出一个答案。 - Tyler Rinker
1
啊,我终于想起来了:要想通过整个匹配,你需要传递 backref=0 参数。 - Wiktor Stribiżew
https://ideone.com/dx1ZGJ - SamWhan
@ClasG,你能把那个作为正式答案吗...很好的逻辑。 - Tyler Rinker
4个回答

10

概述

在R中有一种使用正则表达式完全实现的方法,但它不太美观(尽管我认为它看起来非常棒!)此答案还可根据您的需求进行自定义(最少两个大写字母,最少三个等等),即可扩展,并且可以匹配多个水平空格字符(不使用需要固定宽度的后顾断言)。


代码

在此查看正则表达式

(?:(?=\b(?:\p{Lu}\h+){2}\p{Lu})|\G(?!\A))\p{Lu}\K\h+(?=\p{Lu})

替换:空字符串

编辑 1(非ASCII字符)

我的原始模式使用了\b,这可能无法处理Unicode字符(例如É)。以下替代方法可能是更好的方法。它检查确保第一个大写字母之前的内容不是字母(来自任何语言/脚本)。它还确保如果在大写系列的末尾有一个大写字符,并且后面跟着任何其他字母,则不匹配该大写字符。

如果您还需要确保数字不在大写字母之前,请在\P{L}的位置使用[^\p{L}\p{N}]

在此处查看使用的正则表达式

(?:(?<=\P{L})(?=(?:\p{Lu}\h+){2}\p{Lu})|\G(?!\A))\p{Lu}\K\h+(?=\p{Lu}(?!\p{L}))

使用方法

在此处查看代码

x <- c(
    "Welcome to A I: the best W O R L D!",
    "Hi I R is the B O M B for sure: we A G R E E indeed."
)
gsub("(?:(?=\\b(?:\\p{Lu}\\h+){2}\\p{Lu})|\\G(?!\\A))\\p{Lu}\\K\\h+(?=\\p{Lu})", "", x, perl=TRUE)

结果

输入

Welcome to A I: the best W O R L D!
Hi I R is the B O M B for sure: we A G R E E indeed.

输出

Welcome to A I: the best WORLD!
Hi I R is the BOMB for sure: we AGREE indeed.

解释

  • (?:(?=(?:\b\p{Lu}\h+){2}\p{Lu})|\G(?!\A)) 匹配以下任一内容
    • (?=\b(?:\p{Lu}\h+){2}\p{Lu}) 正向前瞻,确保后面的内容匹配(在本例中用作断言,以查找字符串中格式为A A A的所有位置)。您还可以在此正向前瞻的末尾添加\b,以确保不匹配类似于I A Name的内容
      • \b 断言单词边界的位置
      • (?:\p{Lu}\h+){2} 精确匹配以下内容两次
        • \p{Lu} 匹配任何语言(Unicode)中的大写字符
        • \h+ 匹配一个或多个水平空格字符
      • \p{Lu} 匹配任何语言(Unicode)中的大写字符
    • \G(?!\A) 断言上一个匹配的结束位置
  • \p{Lu} 匹配任何语言(Unicode)中的大写字符
  • \K 重置报告的匹配的起始点。之前已消耗的字符不再包含在最终匹配中
  • \h+ 匹配一个或多个水平空格字符
  • (?=\p{Lu}) 正向前瞻,确保后面是任何语言(Unicode)中的大写字符

Edit 2 (python)

以下是上述内容的Python等效代码(需要PyPi regex运行)。由于PyPi regex目前不支持\h,因此我将其替换为[ \t]

在此处查看可工作的代码

import regex
a = [
    "Welcome to A I: the best W O R L D!",
    "Hi I R is the B O M B for sure: we A G R E E indeed."
]

r = regex.compile(r"(?:(?=\b(?:\p{Lu} +){2}\p{Lu})|\G(?!\A))\p{Lu}\K +(?=\p{Lu})")
for i in a:
    print(r.sub('',i))

以上正则表达式基于第一个正则表达式。如果您想使用第二个正则表达式,请使用以下内容:
(?:(?<=\P{L})(?=(?:\p{Lu}[ \t]+){2}\p{Lu})|\G(?!\A))\p{Lu}\K[ \t]+(?=\p{Lu}(?!\p{L}))

使用回调函数

有关回调函数的信息,请参见Wiktor的原始答案,这只是将他的R程序移植到Python。它不使用PyPi正则表达式库,因此无法匹配。此外,它也无法匹配Unicode。

import re
a = [
    "Welcome to A I: the best W O R L D!",
    "Hi I R is the B O M B for sure: we A G R E E indeed."
]

def repl(m):
    return re.sub(r"\s+",'',m.group(0))

for i in a:
    print(re.sub(r"(?:[A-Z]\s+){2,}[A-Z]", repl, i))

1
太棒了...我自己想不到这个。在这里学到了很多东西。 - Tyler Rinker
我一直在尝试用Python复制这个,但不幸的是没有取得太多进展。@ctwheels,非常感谢您的帮助。 - Raqib
1
@Raqib 我已经编辑了我的答案,加入了Python变量。 - ctwheels
@chtwheels。非常感谢您的帮助。另外一个问题,有没有一种方法可以纯粹使用Python内置的re库来完成这个任务?我尽可能地避免使用第三方库,但如果那是唯一的解决方案,那么我愿意使用该库。再次感谢您,您为我节省了很多麻烦。 - Raqib
1
@Raqib 你可以尝试使用回调函数,我在Edit 2中添加了一个新的编辑,但它不能处理Unicode。你需要为Unicode创建自己的解析器,因为默认的re库没有Unicode类,所以\P{Lu}无法工作。你需要使用islower()isdigit()来识别Unicode变体,并将正则表达式更改为(?:[^\W_]\s+){2,}[^\W_],并使用re.UNICODE来确保匹配Unicode字符。 - ctwheels

8

正如我在评论中指出的那样,在问题中第一个gsubfn调用中的问题是由于正则表达式中有两个捕获组,但只有一个参数。这些需要匹配 -- 两个捕获组意味着需要两个参数。我们可以通过运行以下代码并查看print语句的输出来查看gsubfn正在传递什么:

junk <- gsubfn('(([A-Z]\\s+){2,}[A-Z])', ~ print(list(...)), x)

我们可以通过以下任何方式解决这个问题: 1) 使用问题中的正则表达式,但使用一个接受多个参数的函数。实际上只有第一个参数在函数中被使用。
gsubfn('(([A-Z]\\s+){2,}[A-Z])', ~ gsub("\\s+", "", ..1), x)
## [1] "Welcome to A I: the best WORLD!"              
## [2] "Hi I R is the BOMB for sure: we AGREE indeed."

请注意,它将公式解释为函数:
function (...) gsub("\\s+", "", ..1)

我们可以这样查看从公式生成的函数:
fn$identity( ~ gsub("\\s+", "", ..1) )
## function (...) 
## gsub("\\s+", "", ..1)

2) 这个方法使用了问题中的正则表达式和函数,但是添加了 backref = -1 参数,这告诉它仅将第一个捕获组传递给函数 -- 负号表示不要传递整个匹配。

gsubfn('(([A-Z]\\s+){2,}[A-Z])', spacrm1, x, backref = -1)

(正如@Wiktor Stribiżew在他的答案中指出的那样,backref=0也可以起作用。)

3) 使用问题中的正则表达式另一种表达方式是:

gsubfn('(([A-Z]\\s+){2,}[A-Z])', x + y ~ gsub("\\s+", "", x), x)

请注意,它将公式解释为此函数:
function(x, y) gsub("\\s+", "", x)

所有的答案都很好...这是检查的第一个答案。 - Tyler Rinker

5
问题在于gsubfn将哪些项目传递给spacrm函数以及spacrm函数接受的参数数量与传递给它们的参数数量不匹配。请参见gsubfn文档中的backref参数:

要传递给函数的反向引用数。如果为零或正数,则将匹配作为第一个参数传递给替换函数,后跟所示数量的反向引用作为后续参数。如果为负,则仅传递该数量的反向引用,但未传递匹配本身。如果省略,则会自动确定,即如果没有反向引用,则为0,否则它将等于反向引用数的负值。它通过计算模式中非转义左括号的数量来确定这一点。

在你的情况下,省略了backref参数,spacrmX函数得到了W O R L D L 的值。 只接受单个参数的spacrm1函数得到了两个参数,因此会出现未使用的参数("L ")错误。
当使用spacrm2时,它得到了所有两个捕获值,并将它们连接在一起(在去除空格后)。
实际上,您可以使用backref=0告诉gsubfn仅处理整个匹配值并简化模式,删除捕获组并使用一个非捕获组。
spacrm1 <- function(string) {gsub('\\s+', '', string)}
x <- c(
     'Welcome to A I: the best W O R L D!',
     'Hi I R is the B O M B for sure: we A G R E E indeed.'
)
gsubfn('(?:[A-Z]\\s+){2,}[A-Z]', spacrm2, x, backref=0)
[1] "Welcome to A I: the best WORLD!"              
[2] "Hi I R is the BOMB for sure: we AGREE indeed."

1
您可以简单地匹配一个大写字母前面的空格,以及由一个空格分隔的两个大写字母后面跟随的空格(使用环视)。或者反过来 - 匹配由一个空格分隔的两个大写字母之前的空格,然后跟随一个大写字母。
(?<=[A-Z]) (?=[A-Z] [A-Z])|(?<=[A-Z] [A-Z]) (?=[A-Z])

R 代码:

x <- c(
    "Welcome to A I: the best W O R L D!",
    "Hi I R is the B O M B for sure: we A G R E E indeed."
)
gsub("(?<=[A-Z]) (?=[A-Z] [A-Z])|(?<=[A-Z] [A-Z]) (?=[A-Z])", "", x, perl=TRUE)

在ideone这里实时运行代码


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