是否可以使用正则表达式替换来增加数字?

Posted

技术标签:

【中文标题】是否可以使用正则表达式替换来增加数字?【英文标题】:Is it possible to increment numbers using regex substitution? 【发布时间】:2012-10-17 18:56:37 【问题描述】:

当然不使用evaluated/function-based substitution。

这个问题的灵感来自another one, where the asker wanted to increment numbers in a text editor。支持正则表达式替换的文本编辑器可能比支持完整脚本的文本编辑器多,因此如果存在正则表达式,它可能会方便浮动。

另外,我经常从巧妙的解决方案中学到一些巧妙的东西,解决实际上无用的问题,所以我很好奇。

假设我们只讨论非负十进制整数,即\d+

是否可以单次替换?或者,有限数量的替换?

如果不是,至少有可能给定一个上限,例如最多 9999 个数字?

当然,给定一个while循环是可行的(替换while match),但我们在这里寻求一个无循环的解决方案。

【问题讨论】:

PCRE 库的 C API 实际上没有任何“替代”的概念;相反,它只允许您获取有关匹配项的详细信息,并且您可以根据需要对这些信息进行任何操作。我不清楚什么样的字符串替换甚至可以增加一个 one 数字;例如,它必须有某种方法将1 转换为22 转换为3,但在Perl 中,唯一的方法是使用s/.../.../e,或者使用插值在替换字符串中:s/\d+/@[$&+1]/. 你必须使用 evaluator/function based substitution.. 将所有内容放入正则表达式会使其更加复杂,是的,这将是一件愚蠢的事情.. @ruakh - 嗯,鉴于我见过人们用正则表达式做的疯狂的事情,我认为将1 转换为223 等将是微不足道的,但也许不是!也许我们应该从那里开始。 如果我明白你在问什么,这是一个很好的问题并且有一个简单的问题:想想 99:你只有一个可能的替换标记(例如用 2 我认为使用二进制数会更容易。 @Gabber - 很棒的评论!正如你发布的那样,我开始意识到这就是我被卡住的地方。所以目前,我正在尝试解决问题假设0123456789 在“文档”末尾可用(通过前瞻获取我需要的数字)。一旦我以这种方式解决了问题,那么我可以看看是否有一些聪明的方法可以让“不可用”的数字通过魔法出现...... 【参考方案1】:

这个问题的主题让我很开心,因为我之前做了一个特定的实现。我的解决方案恰好是两个替换,所以我会发布它。

我的实现环境是solaris,完整示例:

echo "0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909" |
perl -pe 's/\b([0-9]+)\b/0$1~01234567890/g' |
perl -pe 's/\b0(?!9*~)|([0-9])(?=9*~[0-9]*?\1([0-9]))|~[0-9]*/$2/g'

1 2 3 4 8 9 10 11 20 100 110 200 910 1000 1100 1910

拆开解释一下:

s/\b([0-9]+)\b/0$1~01234567890/g

对于每个数字 (#),将其替换为 0#~01234567890。第一个 0 是在需要将 9 舍入到 10 的情况下。 01234567890 块用于递增。 “9 10”的示例文本是:

09~01234567890 010~01234567890

下一个正则表达式的各个部分可以单独描述,它们通过管道连接以减少替换次数:

s/\b0(?!9*~)/$2/g

选择所有不需要四舍五入的数字前面的“0”位并丢弃。

s/([0-9])(?=9*~[0-9]*?\1([0-9]))/$2/g

(?=) 是正向前瞻,\1 是匹配组 #1。所以这意味着匹配所有后面跟着 9 的数字,直到 '~' 标记然后转到查找表并找到该数字后面的数字。替换为查找表中的下一个数字。因此,当正则表达式引擎解析数字时,“09~”变为“19~”,然后变为“10~”。

s/~[0-9]*/$2/g

此正则表达式删除 ~ 查找表。

【讨论】:

非常好。我认为你在技术上是这里的赢家。我会将您的答案标记为已接受。在我尝试一次性完成之后;-) 由于@BradKiers 的原因,不可能一次性完成。恭喜!通过一次性删除查找表来携带(“四舍五入”)9的独特方式,以及很好的压实。太好了,它甚至不使用lookbehinds,因此与javascript兼容。 @AndrewCheong 有一个想法,我认为可以进行 1 次替换,前提是查找表可以作为垃圾留在解决方案中并且正则表达式引擎支持后向引用。不幸的是,据说只有 .NET 正则表达式引擎支持这一点,而我没有 .NET 编译器。 好吧,也许不是,它还依赖于引擎进行就地替换和重新匹配。在 perl 上测试,结果流与源流不同。一个例子:echo "0" | perl -pe 's/((?=0)|(0))(? @nanmaniac 如果你有一种带有数字的语言,你应该使用它。如果您真的需要在正则表达式中增加 2,您可以这样做。我要添加第二个查找表: !024680135791 制作原始表达式 s/\b([0-9]+)\b/0$1!024680135791~01234567890/g 然后分别处理结转步骤: perl -pe 的/\b0(?!9*[8-9]!)|([0-9])(?=9*[8-9]![0-9]*?\1([0-9]) )/$2/g' | perl-pe '([0-9])(?=~[0-9]*![0-9]*?\1([0-9]))|![0-9]~[0- 9]*/$2/g' 【参考方案2】:

哇,原来它是可能的(虽然很丑)!

如果您没有时间或懒得通读整个解释,这里是执行此操作的代码:

$str = '0 1 2 3 4 5 6 7 8 9 10 11 12 13 19 20 29 99 100 139';
$str = preg_replace("/\d+/", "$0~", $str);
$str = preg_replace("/$/", "#123456789~0", $str);
do

$str = preg_replace(
    "/(?|0~(.*#.*(1))|1~(.*#.*(2))|2~(.*#.*(3))|3~(.*#.*(4))|4~(.*#.*(5))|5~(.*#.*(6))|6~(.*#.*(7))|7~(.*#.*(8))|8~(.*#.*(9))|9~(.*#.*(~0))|~(.*#.*(1)))/s",
    "$2$1",
    $str, -1, $count);
 while($count);
$str = preg_replace("/#123456789~0$/", "", $str);
echo $str;

现在让我们开始吧。

首先,正如其他人提到的那样,即使您循环它也不可能在单个替换中进行(因为您如何将相应的增量插入单个数字)。但是,如果您先准备字符串,则可以循环使用单个替换。这是我使用 php 的演示实现。

我使用了这个测试字符串:

$str = '0 1 2 3 4 5 6 7 8 9 10 11 12 13 19 20 29 99 100 139';

首先,让我们通过附加一个标记字符来标记我们想要增加的所有数字(我使用~,但您可能应该使用一些绝对不会出现在您的目标字符串中的疯狂的 Unicode 字符或 ASCII 字符序列。

$str = preg_replace("/\d+/", "$0~", $str);

由于我们将一次替换每个数字的一​​位数字(从右到左),因此我们只需在每个完整数字后添加该标记字符。

现在主要的技巧来了。我们在字符串的末尾添加了一个小“查找”(也用您的字符串中没有出现的唯一字符分隔;为简单起见,我使用了#)。

$str = preg_replace("/$/", "#123456789~0", $str);

我们将使用它来替换相应的后继数字。

现在是循环:

do

$str = preg_replace(
    "/(?|0~(.*#.*(1))|1~(.*#.*(2))|2~(.*#.*(3))|3~(.*#.*(4))|4~(.*#.*(5))|5~(.*#.*(6))|6~(.*#.*(7))|7~(.*#.*(8))|8~(.*#.*(9))|9~(.*#.*(~0))|(?<!\d)~(.*#.*(1)))/s",
    "$2$1",
    $str, -1, $count);
 while($count);

好的,怎么回事?匹配模式对每个可能的数字都有一个替代方案。这会将数字映射到继任者。以第一种方案为例:

0~(.*#.*(1))

这将匹配任何0,后跟我们的增量标记~,然后匹配直到我们的作弊分隔符和相应的继任者(这就是我们将每个数字放在那里的原因)。如果您看一下替换,它将被$2$1 替换(然后将是1,然后我们在~ 之后匹配的所有内容将其放回原位)。请注意,我们在此过程中删除了~。从0 增加一个数字到1 就足够了。数字已成功增加,没有结转。

接下来的 8 个选项对于数字 18 完全相同。然后我们处理两种特殊情况。

9~(.*#.*(~0))

当我们替换9 时,我们不会删除增量标记,而是将其放在生成的0 的左侧。这(结合周围的循环)足以实现结转传播。现在还剩下一种特殊情况。对于仅由9s 组成的所有数字,我们将在数字前面加上~。这就是最后一个替代方案的用途:

(?<!\d)~(.*#.*(1))

如果我们遇到一个前面没有数字的~(因此是否定的lookbehind),它一定是一直通过一个数字,因此我们只需将它替换为1。我认为我们甚至不需要消极的lookbehind(因为这是检查的最后一个替代方案),但这样感觉更安全。

关于整个模式周围的(?|...) 的简短说明。这样可以确保我们始终在相同的引用 $1$2 中找到两个匹配项(而不是字符串中的更大数字)。

最后,我们添加了 DOTALL 修饰符 (s),以使其适用于包含换行符的字符串(否则,只会增加最后一行中的数字)。

这是一个相当简单的替换字符串。我们只需先写$2(我们在其中捕获了后继标记,可能还有结转标记),然后我们将匹配到的其他所有内容用$1 放回原处。

就是这样!我们只需要从字符串末尾删除我们的 hack,我们就完成了:

$str = preg_replace("/#123456789~0$/", "", $str);
echo $str;
> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 20 21 30 100 101 140

所以我们可以完全在正则表达式中做到这一点。我们唯一的循环总是使用相同的正则表达式。我相信这是我们在不使用preg_replace_callback() 的情况下所能达到的最接近的结果。

当然,如果我们的字符串中有带小数点的数字,这会造成可怕的后果。但这可能可以通过第一次准备更换来解决。

更新:我刚刚意识到,这种方法会立即扩展到任意增量(不仅仅是+1)。只需更改第一个替换。您附加的~ 的数量等于您应用于所有数字的增量。所以

$str = preg_replace("/\d+/", "$0~~~", $str);

会将字符串中的每个整数递增3

【讨论】:

啊哈,把 TL;DR 放在开头!!太棒了!这是一个非常好的解决方案! +1 解释(尚未测试) 非常好!也快回答。您可以通过使用前瞻来使模式更漂亮:您可以使用(?=.*?(1)) 而不是(.*#.*(1)) - 它仍然可以捕获并避免您替换为$2$1(另外-您并不关心1是否来自您的查找)。我很确定您可以使用类似的方法来添加两个数字("1234 + 5678"),但这种模式会很丑(100 次查找?至少 45,我认为) - 编写脚本可能是一个很好的练习生成该模式。 好点!但是,前瞻中的# 并未用于仅从# 后面获取继任者。如果有人以不同的顺序使用该hack-appendix(例如~0123456879),~ 将在该位中被替换。这就是为什么我在每个备选方案中都保留了#,这样无论附录的顺序如何,它都能正常工作。如果你把它留在里面,我认为前瞻并不会让它更漂亮。 明白了——好点子。您可以删除#[^#]*$。再说一次,我的建议是 10 个字符 - 和你的一样。小问题,无论如何:) @BradGilbert 问题的重点不是使用回调实现,而是仅使用正则表达式匹配和字符串替换。否则,它也是 PHP 中的单行代码......【参考方案3】:

我设法让它在 3 次替换中工作(无循环)。

tl;dr

s/$/ ~0123456789/

s/(?=\d)(?:([0-8])(?=.*\1(\d)\d*$)|(?=.*(1)))(?:(9+)(?=.*(~))|)(?!\d)/$2$3$4$5/g

s/9(?=9*~)(?=.*(0))|~| ~0123456789$/$1/g

说明

~ 成为一个特殊字符预计会出现在文本中的任何位置。

    如果一个字符在文本中找不到,那么就没有办法让它神奇地出现。所以首先我们在最后插入我们关心的字符。

    s/$/ ~0123456789/
    

    例如,

    0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909
    

    变成:

    0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909 ~0123456789
    

    接下来,对于每个数字,我们 (1) 递增最后一个非 9(如果 all 都是 9s,则在前面加上 1),以及 (2) “标记" 9s 的每个尾随组。

    s/(?=\d)(?:([0-8])(?=.*\1(\d)\d*$)|(?=.*(1)))(?:(9+)(?=.*(~))|)(?!\d)/$2$3$4$5/g
    

    例如,我们的例子变成:

    1 2 3 4 8 9 19~ 11 29~ 199~ 119~ 299~ 919~ 1999~ 1199~ 1919~ ~0123456789
    

    最后,我们 (1) 将 9s 的每个“标记”组替换为 0s,(2) 删除 ~s,以及 (3) 删除末尾的字符集。

    s/9(?=9*~)(?=.*(0))|~| ~0123456789$/$1/g
    

    例如,我们的例子变成:

    1 2 3 4 8 9 10 11 20 100 110 200 910 1000 1100 1910
    

PHP 示例

$str = '0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909';
echo $str . '<br/>';
$str = preg_replace('/$/', ' ~0123456789', $str);
echo $str . '<br/>';
$str = preg_replace('/(?=\d)(?:([0-8])(?=.*\1(\d)\d*$)|(?=.*(1)))(?:(9+)(?=.*(~))|)(?!\d)/', '$2$3$4$5', $str);
echo $str . '<br/>';
$str = preg_replace('/9(?=9*~)(?=.*(0))|~| ~0123456789$/', '$1', $str);
echo $str . '<br/>';

输出:

0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909
0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909 ~0123456789
1 2 3 4 8 9 19~ 11 29~ 199~ 119~ 299~ 919~ 1999~ 1199~ 1919~ ~0123456789
1 2 3 4 8 9 10 11 20 100 110 200 910 1000 1100 1910

【讨论】:

@m.buettner - 我想将我的答案标记为已接受的答案,因为我觉得它是最紧凑的。但是,您比我早​​几个小时发布了第一个解决方案(我们的解决方案都不是单一替换,这被发现是不可能的),所以我想适当地感谢您。您(或任何人)是否会反对或对我开放并授予您赏金有任何道德疑虑,只要我能够(在 2 天内)? (你接受这个安排,还是我接受你的回答?我对任何选择都很满意。) 酷,这也是一个不错的解决方案!我不介意赏金,但我对您的解决方案有一些疑问。首先,(?=\d) 是干什么用的?其次,为什么步骤 2 中的第一个替代方案与 10 中的 1 不匹配?第三,我只是尝试用 PHP(它也使用 PCRE)来实现它,但我无法让它工作(只有 9 的字符串正确递增)。最后,你也需要添加DOTALL修饰符,否则它只会在字符串的最后一行起作用。 @m.buettner - 嗯,我能够在 PHP 中实现它。在这里,我在答案中添加了一个示例。如果我遗漏了什么,请告诉我!不过,关于多行输入的 DOTALL 修饰符很好。我认为通常文本编辑器会在每行的基础上运行替换(或暗示DOTALL),但这些都是特殊情况。 @m.buettner - 为了回答您的问题,(?=\d) 是防止空字符串匹配的几种方法之一(您可以看到,如果没有它,表达式将始终回退到|(?=.*(1))|) 两个交替的后半部分,我觉得这是最简单的。 (其他方法涉及lookbehinds,JavaScript 不支持。)而且,第2 步中的第一个替代方案与10 中的1 不匹配,因为我在最后断言(?!\d);我只对[0-8]last9s 的trailing 字符串感兴趣。 @m.buettner - 啊,这样就行了。很多次我都把我的表达扭曲成椒盐脆饼,然后才意识到我没有逃脱反斜杠。【参考方案4】:

可以一次替换吗?

没有。

如果不是,是否至少可以在给定上限的单个替换中进行,例如最多 9999 个数字?

没有。

您甚至不能将 0 到 8 之间的数字替换为它们各自的后继数字。匹配并分组此号码后:

/([0-8])/

您需要更换它。但是,正则表达式不是对数字进行操作,而是对字符串进行操作。所以你可以用这个数字的两倍替换“数字”(或更好:数字),但正则表达式引擎不知道它正在复制一个包含数值的字符串。

即使你会这样做(愚蠢):

/(0)|(1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)/

以便正则表达式引擎“知道”如果匹配组 1,则数字 '0' 匹配,它仍然无法进行替换。您不能指示正则表达式引擎将组 1 替换为数字 '1',将组 '2' 替换为数字 '2' 等等。当然,像 PHP 这样的一些工具可以让您定义几个不同的模式并进行相应的替换字符串,但我得到的印象不是您所想的。

【讨论】:

啊,我明白了。感谢您的解释。我同意:只要在正则表达式(或至少 PCRE)中,不能让未遇到的实体神奇地出现,就没有希望。我还在想办法;)但与此同时,+1。 好点..regex 对字符串而不是其他类型进行操作..不支持将它用于差异类型,这将是愚蠢的事情 2 做 @acheong87,我很想知道你对自己的看法。尽管我对 regex 非常熟悉,并且很确定在这种情况下没有简洁的解决方案,但我有时会对那些比我更熟悉 regex 的人想出的东西感到困惑。 我放弃了让角色出现魔法的尝试,但我确实遇到了a more compact solution。无论如何,非常感谢您的初步观察,到目前为止没有受到挑战! @BradGilbert,你一定错过了 OP 在他问题的第一部分的评论“当然不使用基于评估/基于函数的替换”【参考方案5】:

仅靠正则表达式搜索和替换是不可能的。

您必须使用其他东西来帮助实现这一目标。您必须使用手头的编程语言来增加数字。

编辑:

作为 Single Unix Specification 一部分的正则表达式定义没有提及支持计算算术表达式或执行算术运算的能力的正则表达式。


尽管如此,我知道一些风格(TextPad,Windows 编辑器)允许您使用 \i 作为替代术语,它是找到搜索字符串多少次的增量计数器,但它不评估或将找到的字符串解析为数字,也不允许向其添加数字。

【讨论】:

这是一个有效的答案,但它是对肯定问题的否定答案,没有证据就不能接受它(而对否定/肯定问题的否定/肯定答案通过更简单的方法证明: 一个例子)。所以,我非常想知道为什么它绝对不可能的证据(甚至是一个大纲)。 @acheong87 我为你改进了一点答案。【参考方案6】:

我需要从无法修改的管道中将输出文件的索引增加一个。经过一些搜索后,我在此页面上获得了成功。虽然这些读数是有意义的,但它们确实没有为问题提供可读的解决方案。是的,只用正则表达式就可以;不,它不是那么容易理解。

在这里我想给出一个使用Python 的可读解决方案,这样其他人就不需要重新发明***了。我可以想象你们中的许多人可能最终得到了类似的解决方案。

想法是将文件名分成三组,并格式化匹配字符串,使递增的索引是中间组。然后可以只增加中间组,然后我们再次将三个组拼在一起。

import re
import sys
import argparse
from os import listdir
from os.path import isfile, join



def main():
    parser = argparse.ArgumentParser(description='index shift of input')
    parser.add_argument('-r', '--regex', type=str,
            help='regex match string for the index to be shift')
    parser.add_argument('-i', '--indir', type=str,
            help='input directory')
    parser.add_argument('-o', '--outdir', type=str,
            help='output directory')

    args = parser.parse_args()
    # parse input regex string
    regex_str = args.regex
    regex = re.compile(regex_str)
    # target directories
    indir = args.indir
    outdir = args.outdir

    try:
        for input_fname in listdir(indir):
            input_fpath = join(indir, input_fname)
            if not isfile(input_fpath): # not a file
                continue

            matched = regex.match(input_fname)
            if matched is None: # not our target file
                continue
            # middle group is the index and we increment it
            index = int(matched.group(2)) + 1
            # reconstruct output
            output_fname = 'previndexafter'.format(**
                'prev'  : matched.group(1),
                'index' : str(index),
                'after' : matched.group(3)
            )
            output_fpath = join(outdir, output_fname)

            # write the command required to stdout
            print('mv i o'.format(i=input_fpath, o=output_fpath))
    except BrokenPipeError:
        pass



if __name__ == '__main__': main()

我有一个名为index_shift.py 的脚本。举一个用法示例,我的文件名为k0_run0.csv,用于使用参数k 引导机器学习模型运行。参数k 从零开始,所需的索引映射从一开始。首先我们准备输入和输出目录以避免覆盖文件

$ ls -1 test_in/ | head -n 5
k0_run0.csv
k0_run10.csv
k0_run11.csv
k0_run12.csv
k0_run13.csv
$ ls -1 test_out/

要查看脚本的工作原理,只需打印其输出:

$ python3 -u index_shift.py -r '(^k)(\d+?)(_run.+)' -i test_in -o test_out | head -n5
mv test_in/k6_run26.csv test_out/k7_run26.csv
mv test_in/k25_run11.csv test_out/k26_run11.csv
mv test_in/k7_run14.csv test_out/k8_run14.csv
mv test_in/k4_run25.csv test_out/k5_run25.csv
mv test_in/k1_run28.csv test_out/k2_run28.csv

它会生成 bash mv 命令来重命名文件。现在我们将这些行直接输入bash

$ python3 -u index_shift.py -r '(^k)(\d+?)(_run.+)' -i test_in -o test_out | bash

检查输出,我们已经成功地将索引移动了一个。

$ ls test_out/k0_run0.csv
ls: cannot access 'test_out/k0_run0.csv': No such file or directory
$ ls test_out/k1_run0.csv
test_out/k1_run0.csv

您也可以使用cp 代替mv。我的文件有点大,所以我想避免重复它们。您还可以重构作为输入参数转换的数量。我没有打扰,因为我的大部分用例都是移位。

【讨论】:

以上是关于是否可以使用正则表达式替换来增加数字?的主要内容,如果未能解决你的问题,请参考以下文章

sql server中对字段使用正则表达式替换???

sql正则匹配连续增加数字

notepad++正则表达式使数字自动增加

正则表达式

使用正则表达式替换仅保留正斜杠和数字

是否有正则表达式来删除“。”的第一个实例?