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

polhcujo  于 2023-01-03  发布在  其他
关注(0)|答案(7)|浏览(139)

可以用正则表达式替换来增加数字吗?当然不能用evaluated/function-based substitution
这个问题是受another one, where the asker wanted to increment numbers in a text editor的启发,支持正则表达式替换的文本编辑器可能比支持完全脚本的文本编辑器多,所以正则表达式可能很方便浮动(如果存在的话)。
而且,我经常从实际无用的问题的聪明解决方案中学到一些有趣的东西,所以我很好奇。

  • 假设我们只讨论非负十进制整数,即\d+。*
  • 在一次置换中可能吗?或者,在有限次置换中可能吗?
  • 如果不是,是否至少有可能 * 给定一个上限 *,例如数字高达9999?

当然,给定while-loop(替换while matched)是可行的,但我们在这里采用无循环的解决方案。

ui7jx7zq

ui7jx7zq1#

这个问题的主题让我觉得很有趣,因为我之前做了一个特殊的实现。我的解决方案恰好是两个替换,所以我会把它贴出来。
我的实现环境是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

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

iszxjhcz

iszxjhcz2#

哇,事实证明这是可能的(尽管很丑陋)!
如果您没有时间或不想阅读整个说明,下面的代码可以做到这一点:

$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);

由于我们将一次替换每个数字的一个数字(从右到左),因此我们将在每个完整数字后添加该标记字符。
现在主要的技巧来了,我们在字符串的末尾添加一个"lookup"(也是用一个在字符串中没有出现的唯一字符分隔;为了简单起见,我使用了#)。

$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);

好吧,这是怎么回事?匹配模式对每个可能的数字都有一个替代项。这将数字Map到后继项。以第一个替代项为例:

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

这将匹配后面跟着增量标记~的任何0,然后匹配所有内容,直到我们的欺骗分隔符和相应的后继符(这就是为什么我们把每个数字都放在那里)。如果你看一眼替换,这将被$2$1替换(它将是1,然后是我们匹配的~之后的所有内容,以将其放回原处)。注意,我们在此过程中删除了~。从01递增一个数字就足够了。该数字已成功递增,没有结转。
接下来的8个选择对于数字18是完全相同的,然后我们考虑两种特殊情况。

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

当我们替换9时,我们不删除增量标记,而是把它放在结果0的左边。(结合周围的循环)就足以实现结转传播。现在还剩下一个特殊情况。对于所有仅由9组成的数字,我们将以数字前面的~结束。这就是最后一种选择的目的:

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

如果我们遇到一个前面没有数字的~(因此是负后看),那么它一定是一直通过一个数字来携带的,因此我们只需要用一个1来替换它。我认为我们甚至不需要负后看(因为这是最后一个被检查的选择),但是这样感觉更安全。
关于整个模式的(?|...)的一个简短说明,这确保了我们总是在相同的引用$1$2中找到两个匹配的替代项(而不是字符串中更大的数字)。
最后,我们添加了DOTALL修饰符(s),以使其适用于包含换行符的字符串(否则,只有最后一行中的数字会递增)。
这使得替换字符串变得相当简单,我们只需要先写入$2(在其中我们捕获了后继字符串,也可能捕获了carry-over标记),然后将匹配到的所有其他字符串放回$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

gzjq41n4

gzjq41n43#

我设法让它在3个替代工作(没有循环)。
左,右

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

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

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

例如,我们的示例变为:
1.最后,我们(1)用0替换每个"标记"的9组,(2)删除~,(3)删除末尾的字符集。

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

例如,我们的示例变为:

    • 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
vlju58qv

vlju58qv4#

有可能在一次置换中实现吗?
没有。
如果不是,是否至少在给定上限的单个替换中是可能的,例如,直到9999的数字?
没有。
您甚至不能将0到8之间的数字替换为它们各自的后继数字。匹配并分组此数字后:

/([0-8])/

你需要替换它。然而,regex不对数字操作,而是对字符串操作。所以你可以替换“number”(或者更好:digit),但regex引擎不知道它正在复制一个包含数值的字符串。
即使你会做一些(愚蠢的)事情,比如:

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

这样正则表达式引擎“知道”如果组1匹配,数字'0'匹配,它仍然不能进行替换。您不能指示正则表达式引擎用数字'1'替换组1,用数字'2'替换组'2',等等。当然,有些工具(如PHP)允许您定义几个不同的模式以及相应的替换字符串,但我觉得这不是你想的。

cbeh67ev

cbeh67ev5#

仅通过正则表达式搜索和替换是不可能的。
你必须使用其他东西来帮助实现这一点。你必须使用手头的编程语言来增加数字。

    • 编辑:**

正则表达式定义(作为**Single Unix Specification**的一部分)未提及支持算术表达式求值的正则表达式或执行算术运算的功能。
尽管如此,我知道有些风格(TextPad,Windows编辑器)允许您使用\i作为替换项,它是搜索字符串被找到次数的增量计数器,但它不会将找到的字符串求值或解析为数字,也不允许向其添加数字。

fiei3ece

fiei3ece6#

我已经找到了一个两步解决方案(Javascript),但它依赖于无限的lookaheads,这是一些regex引擎拒绝:

const incrementAll = s =>
        s.replaceAll(/(.+)/gm, "$1\n101234567890")
         .replaceAll(/(?:([0-8]|(?<=\d)9)(?=9*[^\d])(?=.*\n\d*\1(\d)\d*$))|(?<!\d)9(?=9*[^\d])(?=(?:.|\n)*(10))|\n101234567890$/gm, "$2$3");

关键是在第一步中在字符串末尾按顺序添加一个数字列表,在第二步中找到与位置相关的数字并通过前瞻捕获其右侧的数字。第二步中还有另外两个分支,一个用于处理前9,另一个用于删除数字序列。
编辑:我刚刚在Safari中测试了它,它抛出了一个错误,但它在Firefox中绝对有效。

e3bfsja2

e3bfsja27#

我需要从一个我不能修改的管道中增加输出文件的索引。经过一些搜索后,我在这个页面上找到了一个命中。虽然阅读是有意义的,但他们确实没有给予一个可读的解决问题的方法。是的,只用regex是可以做到的;不,这不是那么容易理解。
在这里我想给予一个使用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从零开始,所需的索引Map从1开始。首先,我们准备输入和输出目录以避免覆盖文件

$ 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。我的文件有点大,所以我想避免复制它们。你也可以重构你移位多少作为输入参数。我没有麻烦,因为移位一是我的大多数用例。

相关问题