【问题标题】:Edit within multi-line sed match在多行 sed 匹配中编辑
【发布时间】:2015-09-20 05:47:33
【问题描述】:

我有一个非常大的文件,其中包含以下几行:

start :234
modify 123 directory1/directory2/file.txt
delete directory3/file2.txt
modify 899 directory4/file3.txt

每个块都以模式“start : #”开始,并以空行结束。在块内,每一行都以“修改#”或“删除”开头。

我需要修改每一行的路径,特别是在前面附加一个目录。我会使用一个通用的正则表达式来覆盖整个文件以进行“修改#”或“删除”,但由于该文件中有大量其他数据,可能会有其他匹配这个有点模糊的模式。所以我需要使用多行匹配来查找整个块,然后在该块内执行编辑。这可能会导致单次修改超过 10,000 次,因此我也在尝试将执行时间缩短到 30 分钟以内。

我目前的尝试是 sed 单线:

sed '/^start :[0-9]\+$/ { :a /^[modify|delete] .*$/ { N; ba }; s/modify [0-9]\+ /&Appended_DIR\//g; s/delete /&Appended_DIR\//g }' file_to_edit

它旨在找到“开始”行,在这些行以“修改”或“删除”开头时循环,然后应用 sed 替换。

但是,当我执行这个命令时,并没有做任何改变,输出和原来的文件一样。

我形成的命令有问题吗?在 perl 中这样做会更容易/更有效吗?任何帮助将不胜感激,我会澄清我能做到的地方。

【问题讨论】:

  • 你为什么关心块?如果可以针对所有以“修改”或“删除”开头的行,您将获得良好的性能。如果你正在寻找性能,如果认为 sed 会比 perl 更好。但我怀疑一种 awk 方式会更快。
  • 由于“删除”模式的简单性,以及分散在文件其余部分的数千行源代码,至少有可能另一行将以相同的模式开始,不在这些区块之一内。
  • 好的,所以主要原因是该文件可能包含其他以“delete”开头的行或“modify”开头的行,这些行位于块之外并且您想保留?
  • @CasimiretHippolyte 是的,完全正确

标签: regex perl replace sed


【解决方案1】:

好伤心。 sed 用于在单个行上进行简单替换,仅此而已。一旦开始使用 s、g 和 p(带 -n)以外的结构,您就使用了错误的工具。只需使用 awk:

awk '
    /^start :[0-9]+$/ { inBlock=1 }
    inBlock { sub(/^(modify [0-9]+|delete) /,"&Appended_DIR/") }
    /^$/ { inBlock=0 }
    { print }
' file
start :234
modify 123 Appended_DIR/directory1/directory2/file.txt
delete Appended_DIR/directory3/file2.txt
modify 899 Appended_DIR/directory4/file3.txt

您可以通过多种方式在 awk 中执行上述操作,但为了简洁起见,我以上述风格编写了它,因为我假设您不熟悉 awk,但遵循它应该没有问题,因为它重用了您自己的 sed 脚本正则表达式和替换文本。

【讨论】:

    【解决方案2】:

    我认为perl你会更好

    特别是因为您可以通过设置 $/ 来“按记录”工作 - 如果您的记录由空行分隔,请将其设置为 \n\n

    类似这样的:

    #!/usr/bin/env perl
    use strict;
    use warnings;
    
    local $/ = "\n\n";
    while (<>) {
    
        #multi-lines of text one at a time here.
        if (m/^start :\d+/) {
            s/(modify \d+)/$1 Appended_DIR\//g;
            s/(delete) /$1 Appended_DIR\//g;
        }
        print;
    }
    

    循环的每次迭代都会挑选出一个以空行分隔的块,检查它是否以模式开头,如果是这样,如果应用一些转换。

    它将通过管道从STDINmyscript.pl somefile 获取数据。

    输出到STDOUT,你可以用正常的方式重定向它。

    您以这种方式处理文件的限制因素通常是:

    • 从磁盘传输数据
    • 模式复杂度

    模式越复杂,特别是如果它有变量匹配,正则表达式引擎必须做的回溯越多,这可能会变得昂贵。您的转换很简单,因此打包它们并没有太大区别,您的限制因素可能是磁盘 IO。

    (如果您想进行就地编辑,可以使用这种方法)

    如果 - 如前所述 - 你不能依赖记录分隔符,那么你可以使用 perls range operator (其他答案已经这样做了,我只是将其扩展一点:

    #!/usr/bin/env perl
    use strict;
    use warnings;
    
    while (<>) {
    
        if ( /^start :/ .. /^$/)
            s/(modify \d+)/$1 Appended_DIR\//g;
            s/(delete) /$1 Appended_DIR\//g;
        }
        print;
    }
    

    我们不再更改$/,因此它保持默认的“每一行”。我们添加的是一个范围运算符,用于测试“我当前是否在这两个正则表达式中”,当您点击“开始”时切换true,当您点击空白行时切换false(假设这是您想要的停下来?)。

    如果此条件为真,则应用模式转换,否则...忽略并继续打印。

    【讨论】:

    • 很棒的解释,这真的很有帮助,因为我是 perl 的新手,它有时可以在我的大脑中做一些事情。 + 不需要转义,我不相信使用“&”来获取匹配的字符串在 perl 中是有效的......但我可能是错的。所以我改用 () 和 $1,效果很好!在大约 10 秒内完成了约 2000 万行文件
    • perl 名声不好,因为它真的很容易制作出难以理解的复杂杂乱的代码 sn-ps。在我看来,一个衬里通常没有帮助 - 它们很好,但实际上你需要先拆开它们,手写它们,如果你真的需要,再把它打包。
    • 编辑初始评论以包含对解决方案的修改
    • 另外,这假设“start:”上方总是有一个空行,但情况并非总是如此。我想这可以用这个来纠正吗?本地 $/ = "\n";
    • 不能使用单个换行符作为记录分隔符,因为这是每行,所以你不会将“其余”作为“块”的一部分。我将使用替代方法进行编辑。
    【解决方案3】:

    这在 Perl 中非常简单,并且可能比等效的 sed 快得多

    这个单行程序在任何出现的modify 999delete 之后插入Appended_DIR/。它使用范围运算符将这些更改限制为以start :999 开头并以不包含可打印字符的行结尾的文本块

    perl -pe"s<^(?:modify\s+\d+|delete)\s+\K><Appended_DIR/> if /^start\s+:\d+$/ .. not /\S/" file_to_edit
    

    【讨论】:

      【解决方案4】:

      使用 gnu sed(使用 BRE 语法):

      sed '/^start :[0-9][0-9]*$/{:a;n;/./{s/^\(modify [0-9][0-9]* \|delete \)/\1NewDir\//;ba}}' file.txt
      

      这里的方法不是存储整个块并继续进行替换。这里,当找到块的开头时,下一行被加载到模式空间中,如果该行不为空,则执行替换并加载下一行,等等,直到块的结尾。

      注意:gnu sed 具有交替功能| 可用,其他一些 sed 版本可能没有这种情况。

      awk 的一种方式:

      awk '/^start :[0-9]+$/,/^$/{if ($1=="modify"){$3="newdirMod/"$3;} else if ($1=="delete"){$2="newdirDel/"$2};}{print}' file.txt
      

      【讨论】:

        【解决方案5】:

        我还建议使用perl。尽管我会尝试将其保持为单行形式:

        perl -i -pe 'if ( /^start :/ .. /^$/){s/(modify [0-9]+ )/$1Append_DIR\//;s/(delete )/$1Append_DIR\//; }' file_to_edit
        

        或者你可以使用标准输出的重定向:

        perl -pe 'if ( /^start :/ .. /^$/){s/(modify [0-9]+ )/$1Append_DIR\//;s/(delete )/$1Append_DIR\//; }' file_to_edit > new_file
        

        【讨论】:

        • 需要()在“delete”周围进行附加,并在目录后转义/: perl -i -pe 'if ( /^start :/ .. /^$ /){s/(修改[0-9]+)/$1Append_DIR\//;s/(删除)/$1Append_DIR\//; }' file_to_edit 效果很好!编辑约 2000 万行文件仅需约 20 秒
        【解决方案6】:

        sed 的模式范围是你的朋友:

        sed -r '/^start :[0-9]+$/,/^$/ s/^(delete |modify [0-9]+ )/&prepended_dir\//' filename
        

        这个技巧的核心是/^start :[0-9]+$/,/^$/,它被解读为一个条件,在它之后的s命令被执行。如果 sed 当前发现自己位于第一行匹配开始模式 ^start:[0-9]+$ 并且最后一个匹配结束模式 ^$(空行)的行范围内,则条件为真。 -r 用于扩展正则表达式语法(-E 用于旧 BSD seds),这使得正则表达式更易于编写。

        【讨论】:

          猜你喜欢
          • 2015-05-21
          • 2020-09-21
          • 2011-08-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多