在字符串中查找(/替换)多个字符实例的正则表达式

3
我有一个(可能非常基础的)问题,关于如何构建一个(perl)正则表达式:perl -pe 's///g;',它可以在指定的字符串中查找/替换给定字符/字符集的多个实例。最初,我认为g“全局”标志会做到这一点,但显然我对某些非常核心的东西有误解。:/
例如,我想消除特定字符串中的任何非字母数字字符(在更大的文本语料库中)。仅举例说明,该字符串以[开头,后跟@,可能在两者之间有一些字符。
[abc@def"ghi"jkl'123]

以下是正则表达式:
s/(\[[^\[\]]*?@[^\[\]]*?)[^a-zA-Z0-9]+?([^\[\]]*?)/$1$2/g;

如果我运行三次,将找到第一个 ",这样我就有了所有的三个。 同样地,如果我想用其他字符替换非字母数字字符,比如说 X。

s/(\[[^\[\]]*?@[^\[\]]*?)[^a-zA-Z0-9]+?([^\[\]]*?)/$1X$2/g; 

对于一个实例,这个方法可以解决问题。但是如何一次性找出所有实例呢?


你期望从 [abc@def"ghi"jkl'123] 中得到什么输出? - Nick
1
你的理解是正确的,然而每个多个实例都是你的整个匹配。由于你的第一个实例消耗了整个示例字符串...在一次迭代后就完成了。所以这就是问题所在。在你的情况下,描述起来比修复容易。它可以通过变长回溯来解决,但Perl不支持它,或者通过递归来解决,它支持递归。希望比我聪明的人会提出更直接的方法。 - zzxyz
@Nick,在第一种情况下它将是[abc@defghijkl123],在第二种情况下将是[abc@defXghiXjklX123](当用“X”替换时)。 - jan
1
@zzxyz 这很有道理...即使对于像我这样的非专家也是如此...当你写下"更容易描述而不是修复"时,这让我想到至少我的问题并不像我想象的那么基础 :-P - jan
3个回答

3
你的代码不能正常工作的原因在于 /g 在替换后不会重新扫描字符串。它找到给定正则表达式的所有非重叠匹配项,然后进行替换。
[abc@def"ghi"jkl'123] 中,只有一个匹配项(即字符串的 [abc@def" 部分,其中 $1 = '[abc@def'$2 = ''),因此只删除第一个 "
在第一次匹配后,Perl 会扫描剩余的字符串(ghi"jkl'123])以查找另一个匹配项,但它没有找到另一个 [(或 @)。
我认为最直观的解决方案是使用嵌套的搜索/替换操作。外部匹配标识要替换的字符串,内部匹配执行实际替换。
代码如下:
s{ \[ [^\[\]\@]* \@ \K ([^\[\]]*) (?= \] ) }{ $1 =~ tr/a-zA-Z0-9//cdr }xe;

或者用X替换每个匹配项:

s{ \[ [^\[\]\@]* \@ \K ([^\[\]]*) (?= \] ) }{ $1 =~ tr/a-zA-Z0-9/X/cr }xe;

我们匹配一个前缀[,后面跟着0个或多个不是[]@的字符,最后跟着@

\K用于标记匹配的虚拟起始位置(即到目前为止匹配的所有内容都不包括在匹配字符串中,这简化了替换过程)。

我们匹配并捕获0个或多个不是[]的字符。

最后,我们在一个前瞻中匹配后缀](因此它也不是匹配字符串的一部分)。

替换部分作为代码而不是字符串执行(由/e标志表示)。在这里,我们可以使用$1 =~ s/[^a-zA-Z0-9]//gr$1 =~ s/[^a-zA-Z0-9]/X/gr,但由于每个内部匹配只是一个单个字符,也可以使用转译。

我们返回修改后的字符串(由/r标志表示),并将其用作外部s操作的替换部分。


这确实似乎是最直接的解决方案...感谢您提供详细的解释,非常有帮助。 - jan
快速问题,正如我正在学习的一样:\[ [^\[\]\@]* \@\[ [^\[\]]*? \@之间有什么区别?这两者之间的空格主要是为了可读性还是有其他功能?(后者是否与使用{}{}而不是///有关?) - jan
1
@jan 空格是为了可读性。/x 标志允许您以任何想要的方式格式化正则表达式(甚至可以使用多行并添加注释)。当替换部分是代码块时,我认为 {} 分隔符比 // 更好看(从技术上讲,您可以使用任何标点符号)。一个不错的副作用是它释放了 / 以在代码内部使用(例如,在我的情况下用于 tr///)。 - melpomene
1
@jan 关于[^\[\]]*?,我通常不信任非贪婪量词。尽可能地,我会以一种使量词是贪婪还是非贪婪无关紧要的方式编写我的正则表达式。在这个具体的例子中,差别不大,但你必须小心:例如,当应用于字符串[foo@bar@A时,\[ [^\[\]\@]* \@ A\[ [^\[\]]*? \@ A的行为不同;前者的正则表达式将不匹配,而后者的正则表达式将匹配整个字符串。这是因为[...]*?部分在[...]*? X中如果必要可以匹配X以使整个正则表达式成功,而[^X]*永远不会匹配X - melpomene
1
与其不信任非贪婪量词(它们的行为与贪婪量词一样确定),不如记住正则表达式引擎从左到右运行,然后在必要时回溯尝试替代方案,并且对于它来说,“尽可能匹配不是X的字符”比“尽可能少地匹配字符,同时仍然有X之后”的匹配更容易(尝试次数更少)。事实上,使用非贪婪量词会很常见,但通常存在一种更容易(对引擎而言)的替代方案。 - Grinnz
@Grinnz 我所说的“distrust”并不是指它们不可靠。我知道它们的作用。只是每当我看到一个时,它就像一面红旗;我必须花费额外的几秒钟来思考代码到底在做什么,因为通常它与作者预期的不匹配(如上所示),而这取决于其余的正则表达式。 - melpomene

1
所以...我要建议一个非常计算效率低下的方法。虽然非常低效,但可能仍然比可变长度回溯更快...而且对你来说很简单:使用\K,可以导致其之前的所有内容被删除....因此只有它后面的字符实际上被替换。
perl -pe 'while (s/\[[^]]*@[^]]*\K[^]a-zA-Z0-9]//){}' file

基本上,我们只有一个空循环,直到搜索和替换不再替换任何内容为止。
稍微改进的版本:
perl -pe 'while (s/\[[^]]*?@[^]]*?\K[^]a-zA-Z0-9](?=[^]]*?])//){}' file
(?=) 验证匹配后面的内容,但不包括在匹配结果中。这是一种变长前瞻(逆向匹配时缺少的部分)。我还使用了?来使*变成非贪婪模式,以便得到最短的匹配结果。

1
太好了。我根本不知道存在着 \K,它让很多事情变得更容易(总的来说)……我一直在学习。只是出于好奇:当你写 [^]a-zA-Z0-9] 时,为什么你不必像这样转义 ],即 [^\]a-zA-Z0-9] - jan
1
因为 [] 集合至少需要一个字符,所以 Perl 将其中的 ] 解释为字面量而非非法字符。在我没有反引号的手机上,希望这样说得通。 - zzxyz
@jan - 这绝对是我曾经遇到的最令人愉悦的未被接受的答案方式,我想我将来会效仿。Melpomene的答案绝对更优秀,所以你做得很好。 - zzxyz
1
有时候很难,我经常从多个答案/评论中受益...当然,可以给赞,但有时候接受多个答案会更好。 - jan

1
这里有另一种方法。精确捕获需要处理的子字符串,在替换部分运行一个正则表达式,将其中的非字母数字字符清除。
use warnings;
use strict;
use feature 'say';

my $var = q(ah [abc@def"ghi"jkl'123] oh); #'
say $var;

$var =~ s{ \[ [^\[\]]*? \@\K ([^\]]+) }{
    (my $v = $1) =~ s{[^0-9a-zA-Z]}{}g;
    $v
}ex;

say $var;

在这里,只需要一个孤独的$v,以便返回它而不是匹配项的数量,这就是s/操作符本身返回的内容。可以通过使用/r修饰符来改进此操作,该修饰符返回更改后的字符串并且不更改原始字符串(因此不会尝试更改$1,这是不允许的)。

$var =~ s{ \[ [^\[\]]*? \@\K ([^\]]+) }{
    $1 =~ s/[^0-9a-zA-Z]//gr;
}ex;
\K 的作用是放弃它之前的所有匹配,这些匹配不需要被捕获以便放回。/e 修饰符使替换部分被评估为代码。
问题中的代码无法正常工作,因为所有匹配都被消耗掉了,在 /g 下,搜索会继续从上一个匹配后面的位置开始,试图在字符串中进一步查找整个模式。这将失败,只有第一个出现的匹配项被替换。
我们想要保留在字符串中的匹配项的问题通常可以通过使用 \K(在所有当前答案中都使用)来解决,这样它之前的所有匹配项都不会被消耗。

那也会去掉[abc@这部分。 - melpomene
@melpomene 哎呀,谢谢你 - 已修复(连同另一部分一起修复) - zdim
你的代码没有在 "[\n\@a.b]" 中找到匹配项,而 OP 的代码找到了。 - melpomene
@melpomene 我并不担心“_可能在中间加入一些字符_”的确切性质,所以使用了 . ... 现在已经用 OP 的代码替换了它。谢谢。 - zdim

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