【问题标题】:Replace every 4th occurence of char "_" with "@" in multiple files在多个文件中将每 4 次出现的字符“_”替换为“@”
【发布时间】:2022-03-26 07:24:06
【问题描述】:

我正在尝试用 bash 将多个文件中每 4 次出现的“_”替换为“@”。

例如

foo_foo_foo_foo_foo_foo_foo_foo_foo_foo.. 

会变成

foo_foo_foo_foo@foo_foo_foo_foo@foo_foo...

#perl -pe 's{_}{++$n % 4 ? $& : "@"}ge' *.txt 

我已经尝试过 perl,但问题是它会替换从最后一个文件开始的每 4 个 _。因此,例如,某些文件的第一个 _ 被替换,因为它不是以计数 0 开始每个新文件,而是从前一个文件继续。

我试过了:

#awk '{for(i=1; i<=NF; i++) if($i=="_") if(++count%4==0) $i="@"}1' *.txt 

但这也行不通。

使用 sed 我无法找到一种方法来继续替换每 4 次出现,因为每个文件中有不同数量的 _。有些文件有 20 _,有些有 200 _。因此,我无法指定范围。

我真的不知道该怎么办,有人可以帮忙吗?

【问题讨论】:

  • 假设您的文件每个包含多于 1 行 - 您要替换每行中每 4 次出现的位置还是整个文件?请提供minimal reproducible example,其中包含多于 1 行的样本输入(即没有省略号)和相关的预期输出,以真正展示您的要求。还要说明您的输入文件是否足够小以适合内存,或者我们不能假设。

标签: bash perl awk sed


【解决方案1】:

您只需要使用eof 重置 perl 中的计数器,以告知它何时完成读取每个文件:

perl -pe 's{_}{++$n % 4 ? "_" : "@"}ge; $n = 0 if eof' *.txt

【讨论】:

    【解决方案2】:

    这可能是您想要的,使用 GNU awk 进行 RT:

    $ awk -v RS='_' '{ORS=(FNR%4 ? RT : "@")} 1' file
    foo_foo_foo_foo@foo_foo_foo_foo@foo_foo..
    

    它一次只将每个 _ 分隔的字符串读取到内存 1 中,因此无论您的输入文件有多大,都应该可以工作,假设其中有 _s。

    假设您想在整个文件中替换每 4 个 _,而不是在单个行中。

    【讨论】:

      【解决方案3】:

      一个简单的sed 可以处理这个问题:

      s='foo_foo_foo_foo_foo_foo_foo_foo_foo_foo'
      sed -E 's/(([^_]+_){3}[^_]+)_/\1@/g' <<< "$s"
      
      foo_foo_foo_foo@foo_foo_foo_foo@foo_foo
      

      说明:

      • (: 开始捕获组 #1
        • ([^_]+_){3}:匹配匹配 1+ 个非_ 字符,后跟_。重复此组 3 次以匹配以 _ 分隔的 3 个此类单词
        • [^_]+:匹配 1+ 个非_ 字符
      • ): 结束捕获组 #1
      • _:匹配一个_
      • 替换为\1@ 以用@ 替换第4 个_

      【讨论】:

      • 我认为 _ 被计入整个文件,而不是按行计
      • 我再次阅读了问题,但我并不完全清楚。我正在等待他的反馈来编辑或删除我的答案。
      • 是的,同意不清楚,也可能是单行...
      【解决方案4】:

      使用 GNU sed:

      sed -nsE ':a;${s/(([^_]*_){3}[^_]*)_/\1@/g;p};N;ba' *.txt
      

      -n 禁止自动打印,-s 单独处理每个文件,-E 使用扩展正则表达式。

      脚本是标签a (:a) 和分支到标签-a 命令(ba) 之间的循环。每次迭代都会将下一行输入附加到模式空间 (N)。这样,在读取最后一行之后,模式空间包含整个文件(*)。在最后一次迭代中,当最后一行被读取 ($) 时,替换命令 (s) 用 @ (s/(([^_]*_){3}[^_]*)_/\1@/g) 替换模式空间中的每 4 个 _ 并打印 (@ 987654336@) 结果。

      当您对结果感到满意时,您可以更改选项:

      sed -i -nE ':a;${s/(([^_]*_){3}[^_]*)_/\1@/g;p};N;ba' *.txt
      

      就地修改文件,或者:

      sed -i.bkp -nE ':a;${s/(([^_]*_){3}[^_]*)_/\1@/g;p};N;ba' *.txt
      

      就地修改文件,但保留每个文件的*.txt.bkp 备份。

      (*) 请注意,如果您有非常大的文件,这可能会导致内存溢出。

      【讨论】:

      • 别忘了 GNU sed 有 -z 选项,它也可以与 -s-i 选项结合使用。所以sed -Esz 's/(([^_]*_){3}[^_]*)_/\1@/g' file 也可以。
      • 是的,但是如果文件还包含 NUL 字符怎么办?
      【解决方案5】:

      使用您展示的示例,请尝试关注awk 程序。已经创建了一个名为 fieldNumawk 变量,我已将 4 分配给它,因为 OP 需要在每 4 个 _ 之后输入 @,您也可以根据需要保留它。

      awk -v fieldNum="4" '
      BEGIN{ FS=OFS="_" }
      {
        val=""
        for(i=1;i<=NF;i++){
          val=(val?val:"") $i (i%fieldNum==0?"@":(i<NF?OFS:""))
        }
        print val
      }
      '  Input_file
      

      【讨论】:

        【解决方案6】:

        GNU awk

        $ cat ip.txt
        foo_foo_foo_foo_foo_foo_foo_foo_foo_foo
        123_45678_90
        _
        
        $ awk -v RS='(_[^_]+){3}_' -v ORS= '{sub(/_$/, "@", RT); print $0 RT}' ip.txt
        foo_foo_foo_foo@foo_foo_foo_foo@foo_foo
        123_45678_90
        @
        
        • -v RS='(_[^_]+){3}_' 设置输入记录分隔符以覆盖四个_ 的序列(与此分隔符匹配的文本将通过RT 提供)
        • -v ORS=空输出记录分隔符
        • sub(/_$/, "@", RT) 将最后一个 _ 更改为 @
        • 使用-i inplace 进行就地编辑。

        【讨论】:

          【解决方案7】:

          如果每行都应该重置计数:

          perl -pe's/(?:_[^_]*){3}\K_/\@/g'
          
          $ cat a.txt
          foo_foo_foo_foo_foo_foo_foo_foo_foo_foo
          foo_foo_foo_foo_foo_foo_foo_foo_foo_foo
          
          $ perl -pe's/(?:_[^_]*){3}\K_/\@/g' a.txt a.txt
          foo_foo_foo_foo@foo_foo_foo_foo@foo_foo
          foo_foo_foo_foo@foo_foo_foo_foo@foo_foo
          foo_foo_foo_foo@foo_foo_foo_foo@foo_foo
          foo_foo_foo_foo@foo_foo_foo_foo@foo_foo
          

          如果不应该为每一行重置计数,而是应该为每个文件重置:

          perl -0777pe's/(?:_[^_]*){3}\K_/\@/g'
          

          -0777 导致整个文件被视为一行。这会导致计数跨行正常工作。

          但由于每个文件都使用了新的匹配项,因此文件之间的计数会重置。

          $ cat a.txt
          foo_foo_foo_foo_foo_foo_foo_foo_foo_foo
          foo_foo_foo_foo_foo_foo_foo_foo_foo_foo
          
          $ perl -0777pe's/(?:_[^_]*){3}\K_/\@/g' a.txt a.txt
          foo_foo_foo_foo@foo_foo_foo_foo@foo_foo
          foo_foo_foo@foo_foo_foo_foo@foo_foo_foo
          foo_foo_foo_foo@foo_foo_foo_foo@foo_foo
          foo_foo_foo@foo_foo_foo_foo@foo_foo_foo
          

          为避免一次读取整个文件,您可以继续使用相同的方法,但添加以下内容:

          $n = 0 if eof;
          

          注意eofeof() 不是一回事!见eof

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-06-13
            • 1970-01-01
            • 2012-04-26
            • 2021-11-24
            • 1970-01-01
            • 1970-01-01
            • 2021-10-21
            相关资源
            最近更新 更多