是否可以使用正则表达式替换来增加数字?当然不使用evaluated/function-based substitution。
这个问题的灵感来自another one, where the asker wanted to increment numbers in a text editor。可能有更多的文本编辑器支持正则表达式替换,而不是支持全脚本编写的文本编辑器,因此如果存在一个正则表达式可能很方便浮动。
另外,我经常从聪明的解决方案中学到一些巧妙的东西,几乎无用的问题,所以我很好奇。
假设我们只讨论非负十进制整数,即\d+
。
单次替换是否可行?或者,有限数量的替换?
如果没有,是否至少可以给出上限,例如数字高达9999?
当然,给定一个while循环(在匹配时替换)是可行的,但我们在这里寻找一个无循环解决方案。
答案 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
就足够了。数字成功递增,没有结转。
对于数字1
到8
,接下来的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
<强>解释强>
让~
成为特殊字符而不是,预计会出现在文字的任何位置。
如果在文本中找不到某个角色,那么就没有办法让它看起来神奇。所以首先我们在最后插入我们关心的字符。
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
(如果所有为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
最后,我们(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
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
。我的文件有点大,所以我想避免重复。您还可以重构作为输入参数移位的数量。我没有打扰,因为用例大多数是一个原因。