在文本文件中使用Perl就地排序行

1 投票
3 回答
709 浏览
提问于 2025-04-16 16:16

我想要修改一个文本文件,把每一行根据一个指定的关键字进行排序,并且把原来的文件保存为备份。这个关键字是每一行中包含的一个数字字符。

有没有简单的脚本可以做到这一点,最好是直接在原文件上操作?

谢谢!

3 个回答

0

其实没有简单的脚本可以做到你想要的,因为你提的这个事情其实挺复杂的,而且效率也不高。除非你文件里的每一行长度都完全一样,不然几乎是不可能的(或者说这样做非常傻)。

如果你真的不能在内存中处理这些数据,并且想自己写代码,最好的办法可能就是使用一种叫做基于磁盘的归并排序的方法。关于如何在磁带驱动器上实现这个的例子应该能给你一些启发。

1

有一些就地排序算法,它们的复杂度是O(n log n),比如堆排序。不过我不太明白为什么你会想用这种算法,而不直接用简单的Unix sort命令。除非你有特别严格的性能要求或者处理的数据量非常大……但在这种情况下,perl和python可能也不是最合适的工具。

1

假设你要排序的关键字是每行开头的一串数字,就像下面这个例子。

5 Fine
2 Good
1 Every
4 Does
3 Boy

要对命令行中指定的一个或多个文件进行排序,你可以使用下面的代码。

#! /usr/bin/env perl

use strict;
use warnings;

die "Usage: $0 file ..\n" unless @ARGV;

$^I = ".bak";
undef $/;

while (<>) {
  print map $_->[0],
        sort { $a->[1] <=> $b->[1] }
        map { [ $_, /^(\d+)/ ? $1 : -1 ] }
        /^(.*\n?)/mg;
}

@ARGV 是从命令行传入的参数。如果你不传任何参数运行这个程序,它会在标准错误输出一个使用指南。

$^I 保存了在进行就地编辑时,给文件名添加的扩展名。你也可以通过 Perl 的 -i 选项来启用这个功能,详细信息可以查看 perlrun 文档

-i[扩展名]
这个选项指定用 <> 结构处理的文件要进行就地编辑。它的做法是重命名输入文件,打开原名的输出文件,并将这个输出文件作为 print 语句的默认输出。

$/ 是输入记录的分隔符。把它设置为未定义的值意味着你希望后续的 readline 操作 可以读取到文件末尾。对于非常大的输入,性能会受到影响。

在每次 while 循环中,特殊变量 $_ 会保存当前文件的全部内容。为了对这些行进行排序,我们首先需要把它们拆分开来。

不要被循环中的 print 吓到。这是 Schwartzian Transform,这是 Perl 中一种常见的技巧,尽管它最初的评价并不高。要理解发生了什么,可以从后往前读。

  1. 获取当前文件中所有行的列表。/m 正则表达式选项使得 ^ 可以匹配每行的开头,而不仅仅是目标字符串的开头。
  2. 对于每一行,尝试捕获这一行开头的一个或多个数字,如果没有则默认为 -1。
  3. 按排序关键字的升序对这些行进行排序。
  4. 最后,按排序后的顺序打印这些行。如果启用了就地编辑,print 会输出到当前正在排序的文件。

如果用更程序化的风格来写这个循环,你可以这样写:

while (<>) {
  my @lines = /^(.*\n?)/mg;
  my @augmented = map { [ $_, /^(\d+)/ ? $1 : -1 ] } @lines;
  my @sorted = sort { $a->[1] <=> $b->[1] } @augmented;
  print map $_->[0], @sorted;
}

一旦你理解了 Schwartzian Transform 的工作原理,所有的临时变量看起来就像是多余的杂乱。

撰写回答