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

时间:2012-10-17 18:56:37

标签: regex

是否可以使用正则表达式替换来增加数字?当然不使用evaluated/function-based substitution

这个问题的灵感来自another one, where the asker wanted to increment numbers in a text editor。可能有更多的文本编辑器支持正则表达式替换,而不是支持全脚本编写的文本编辑器,因此如果存在一个正则表达式可能很方便浮动。

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

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

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

  • 如果没有,是否至少可以给出上限,例如数字高达9999?

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

6 个答案:

答案 0 :(得分:41)

哇,事实证明这是可能的(尽管很丑陋)!

如果您没有时间或无法阅读整个解释,请执行以下代码:

$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字符序列,它们绝对不会出现在你的目标中string。

$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就足够了。数字成功递增,没有结转。

对于数字18,接下来的8个替代方案完全相同。然后我们处理两个特殊情况。

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

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

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

如果我们遇到一个前面没有数字的~(因此是负面的后观),它必须一直通过一个数字,因此我们只需用{{1}替换它}。我认为我们甚至不需要负面的背后(因为这是检查的最后一个选择),但这样的感觉更安全。

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

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

这使得一个相当简单的替换字符串。我们首先编写s(其中我们捕获了后继者,可能还有结转标记),然后我们将我们匹配的所有其他内容放回到$2

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

$1

所以我们可以在正则表达式中完全执行此操作。唯一的循环我们总是使用相同的正则表达式。我相信这是在不使用$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);

答案 1 :(得分:36)

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

我的实施环境是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。所以这意味着匹配所有后跟9s的数字,直到'〜'标记然后转到查找表并找到该数字后面的数字。替换为查找表中的下一个数字。因此,当正则表达式引擎解析数字时,“09~”变为“19~”然后变为“10~”。

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

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

答案 2 :(得分:12)

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

<强> TL;博士

s/$/ ~0123456789/

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

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

<强>解释

~成为特殊字符而不是,预计会出现在文字的任何位置。

  1. 如果在文本中找不到某个角色,那么就没有办法让它看起来神奇。所以首先我们在最后插入我们关心的字符。

    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
    
  2. 接下来,对于每个号码,我们(1)增加最后一个非9(如果所有1,则前置9 s),和(2)“标记”9 s的每个尾随组。

    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
    
  3. 最后,我们(1)用9 s替换0 s的每个“标记”组,(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
    
  4. 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
    

答案 3 :(得分:6)

  

是否可以进行单次替换?

没有

  

如果不是,至少可以在给定上限的单个替换中,例如,数字高达9999?

没有

你甚至不能用它们各自的继承者替换0到8之间的数字。匹配后,将此数字分组:

/([0-8])/

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

即使你做了一件事(愚蠢),因为:

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

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

答案 4 :(得分:0)

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

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

修改

正则表达式定义,作为 Single Unix Specification 的一部分,并未提及支持评估aritmethic表达式或执行aritmethic操作的功能的正则表达式。


尽管如此,我知道一些风格(TextPad,Windows编辑器)允许你使用\i作为替换术语,这是一个增量计数器,它是搜索字符串被找到的次数的增量,但它没有&#39 ; t将已找到的字符串计算或解析为数字,也不允许向其中添加数字。

答案 5 :(得分:0)

我需要从无法修改的管道中将输出文件的索引加1。经过一些搜索后,我在此页面上获得了成功。尽管读数很有意义,但实际上并不能为问题提供可读的解决方案。是的,仅使用正则表达式是可行的;不,它不那么容易理解。

在这里,我想使用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 = '{prev}{index}{after}'.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。我的文件有点大,所以我想避免重复。您还可以重构作为输入参数移位的数量。我没有打扰,因为用例大多数是一个原因。