81

是否可以使用正则表达式替换来增加数字?当然,不使用评估/基于函数的替换

这个问题的灵感来自另一个问题,提问者想在文本编辑器中增加数字。支持正则表达式替换的文本编辑器可能比支持完整脚本的文本编辑器多,因此如果存在正则表达式,则可能更方便浮动。

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

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

  • 可以单次替换吗?或者,有限数量的替换?

  • 如果不是,是否至少有可能给出一个上限,例如高达 9999 的数字?

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

4

6 回答 6

48

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

我的实现环境是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

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

于 2015-07-23T23:27:58.970 回答
47

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

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

$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从to增加一个数字1就足够了。数字已成功增加,没有结转。

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

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

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

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

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

(?|...)关于整个图案的简短说明。这确保我们总是在相同的引用中找到两个匹配项的替代项$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.

于 2012-10-17T20:15:25.027 回答
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)增加最后一个非(如果都是s,则在9前面加上 a ),以及(2)“标记”每个尾随组s。199

    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) 用 s 替换每个“标记”的9s组0,(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
于 2012-10-18T02:17:25.683 回答
6

可以单次替换吗?

不。

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

不。

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

/([0-8])/

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

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

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

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

于 2012-10-17T19:30:27.210 回答
2

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

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

编辑:

作为单一 Unix 规范的一部分,正则表达式定义没有提到支持计算算术表达式或执行算术运算的能力的正则表达式。


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

于 2012-10-17T19:25:05.507 回答
0

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

在这里,我想给出一个可读的解决方案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

它生成 bashmv命令来重命名文件。现在我们将这些线直接导入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. 我的文件有点大,所以我想避免重复它们。您还可以重构作为输入参数转换的数量。我没有打扰,因为我的大部分用例都是移位。

于 2019-03-25T17:49:45.790 回答