【问题标题】:Multiline perl regex replace on large file without slurp多行 perl 正则表达式替换大文件而无需 slurp
【发布时间】:2020-01-26 06:14:02
【问题描述】:

我有一个文件,它比需要运行此脚本的服务器上的可用内存量大得多。

在该文件中,我需要运行一个基本的正则表达式,它一次在两行之间进行查找和替换。我已经研究过使用 sed、awk 和 perl,但在这种情况下,我无法让它们中的任何一个按照我的需要工作。

在一个较小的文件上,下面的行可以满足我的需要: perl -0777 -i -pe 's/,\s+\)/\n\)/g' inputfile.txt

本质上,只要一行以逗号结尾,而下一行以右括号开头,请删除逗号。

当我尝试在我的生产文件上运行它时,几分钟后我在终端中收到“Killed”消息,文件内容被完全删除。在那期间我一直在观察内存使用情况,正如预期的那样,它以 100% 的速度运行并广泛使用交换空间。

有没有办法让 perl 命令一次在两行上运行,或者替代 bash 命令可以达到相同的结果?

如果通过保持文件大小相同来更容易,那么我还可以选择用空格字符替换逗号。

【问题讨论】:

  • 我不确定确切的问题陈述——所以:如果一行以逗号结尾,而下一行以)开始,则删除逗号,否则什么也不做。这是对问题的完整陈述吗?
  • 是的,没错。
  • 带左括号的行也可以以逗号结尾吗?
  • 包含左括号和/或右括号的行也可以以逗号结尾。
  • 只是想明白:所以你只对文件中,\n)的实例感兴趣,并且你想将所有这些都转换为\n)

标签: bash perl awk sed


【解决方案1】:

一个相当直接的逻辑:

  • 打印一行,除非它以逗号结尾(需要检查下一行,也许删除它)

  • 如果有逗号则打印上一行 ($p),如果当前行以 ) 开头则不打印它

perl -ne'
    if ($p =~ /,$/) { $p =~ s/,$// if /^\s*\)/; print $p }; 
    print unless /,$/; 
    $p = $_
' file

可以通过丢失一个正则表达式(因此引擎启动开销)和一些数据副本来提高效率,但代价是代码更笨拙,需要额外的逻辑和检查。

file测试

你好 这是一个逗号, 这很好 (但这是另一个, ) 必须去, 那是另一个很好的选择。 结尾

如果最后一行以逗号结尾,则无法打印。一种解决方法是在END block 中检查我们的缓冲区(上一行$p),以便在末尾添加

END { print $p if $p =~ /,$/}

这是检查-n/-p one-liners 中的尾随缓冲区或条件的相当常用的方法。

另一个效率较低但代码可能更简洁的解决方法是替换语句

print unless /,$/;

print if (not /,$/ or eof);

这确实对文件的每一行运行eof 检查,而END 运行一次

【讨论】:

  • 如果最后一行以逗号结尾会不会打印失败?
  • 感谢您的详尽解释,这正是我所追求的,并且在全尺寸文件上测试得很好。
  • @bdx 欢迎您。如果出现问题,请告诉我
【解决方案2】:

延迟打印尾随逗号和换行符,直到您知道可以打印为止。

perl -ne'
   $_ = $buf . $_;
   s/^,(?=\n\))//;
   $buf = s/(,\n)\z// ? $1 : "";
   print;
   END { print $buf; }
'

更快:

perl -ne'
   print /^\)/ ? "\n" : ",\n" if $f;
   $f = s/,\n//;
   print;
   END { print ",\n" if $f; }
'

Specifying file to process to Perl one-liner

【讨论】:

    【解决方案3】:

    如果使用\n 换行符作为记录分隔符很尴尬,请使用其他内容。在这种情况下,您对序列,\n) 特别感兴趣,我们可以让 Perl 在我们读取文件时为我们找到它:

    perl -pe 'BEGIN{ $/ = ",\n)" } s/,\n\)/\n)/' input.txt >output.txt
    

    这部分:$/ = ",\n)" 告诉 Perl 不是遍历文件的行,而是遍历以序列 ,\n) 结尾的记录。这有助于我们确保每个块最多有一个这样的序列,但更重要的是,这个序列不会跨越块(或记录或文件读取)。每个读取的块都将以,\n) 结尾,或者在最终记录的情况下,可能没有记录终止符(根据我们对终止符的定义)。

    接下来,我们只需使用替换来消除 ,\n) 记录分隔符序列中的逗号。

    这里的关键是,通过将记录分隔符设置为我们感兴趣的序列,我们保证序列不会在文件读取中被破坏。

    正如 cmets 中所提到的,只有当,\n) 序列之间的跨度不超过您愿意为该问题投入的内存量时,此解决方案才最有用。换行符本身很可能比,\n) 序列更频繁地出现在文件中,因此,这将读取更大的块。您比我们更了解您的数据集,因此可以更好地判断此解决方案的简单性是否超过了它在内存中消耗的空间。

    【讨论】:

    • 好主意,但最终可能会读取 huge 块(甚至整个文件)...
    • 是的,它确实做出了这样的假设。但另一方面,\n 作为记录分隔符对行长做出了假设。
    • 再次,我同意这将读取与,\n) 出现之间的跨度一样大的记录。我会在答案中提到这一点。但是,尽管我们在 OP 的问题中被告知文件很大,但我们没有被告知触发序列是否稀疏。如果是,是的,这可能会出现问题,就像具有大行的文件可能会出现问题一样。与仅在换行符上拆分相比,我们很可能会读取更大的块。
    • 对我来说唯一的问题是不可预测性:如果文件没有这样的序列(或很少)并且程序被杀死怎么办?
    【解决方案4】:

    这可以通过 awk 更简单地完成。

    awk 'BEGIN{RS=".\n."; ORS=""} {gsub(",\n)", "\n)", RT); print $0 RT}'
    

    解释:

    awk 与 Perl 不同,它允许使用正则表达式作为记录分隔符,这里是 .\n.,它“捕获”每个换行符周围的两个字符。

    ORS 设置为空可防止print 输出额外的换行符。换行符都在RS/RT中捕获。

    RT代表RS正则表达式匹配的实际文本。

    gsub 会从 RT 中删除任何所需的逗号(如果存在)。

    警告:你需要 gnu awk (gawk) 才能工作。根据gawk手册页,似乎仅限POSIX的awk将缺少带有RT变量功能的regexp-RS

    注意:gsub 并不是真正需要的,sub 已经足够好,可能应该在上面使用。

    【讨论】:

      【解决方案5】:

      这可能对你有用(GNU sed):

      sed 'N;s/,\n)/\n)/;P;D' file
      

      在整个文件中保留两行的移动窗口,如果第一行以, 结尾,第二行以) 开头,请删除,

      如果有空白需要保留,请使用:

      sed 'N;s/,\(\s*\n\s*)\)/\1/;P;D' file
      

      【讨论】:

        猜你喜欢
        • 2017-08-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-12-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多