在文本文件中使用Perl就地排序行
我想要修改一个文本文件,把每一行根据一个指定的关键字进行排序,并且把原来的文件保存为备份。这个关键字是每一行中包含的一个数字字符。
有没有简单的脚本可以做到这一点,最好是直接在原文件上操作?
谢谢!
3 个回答
其实没有简单的脚本可以做到你想要的,因为你提的这个事情其实挺复杂的,而且效率也不高。除非你文件里的每一行长度都完全一样,不然几乎是不可能的(或者说这样做非常傻)。
如果你真的不能在内存中处理这些数据,并且想自己写代码,最好的办法可能就是使用一种叫做基于磁盘的归并排序的方法。关于如何在磁带驱动器上实现这个的例子应该能给你一些启发。
有一些就地排序算法,它们的复杂度是O(n log n),比如堆排序。不过我不太明白为什么你会想用这种算法,而不直接用简单的Unix sort
命令。除非你有特别严格的性能要求或者处理的数据量非常大……但在这种情况下,perl和python可能也不是最合适的工具。
假设你要排序的关键字是每行开头的一串数字,就像下面这个例子。
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[扩展名]
这个选项指定用<>
结构处理的文件要进行就地编辑。它的做法是重命名输入文件,打开原名的输出文件,并将这个输出文件作为
$/
是输入记录的分隔符。把它设置为未定义的值意味着你希望后续的 readline 操作 可以读取到文件末尾。对于非常大的输入,性能会受到影响。
在每次 while
循环中,特殊变量 $_
会保存当前文件的全部内容。为了对这些行进行排序,我们首先需要把它们拆分开来。
不要被循环中的 print
吓到。这是 Schwartzian Transform,这是 Perl 中一种常见的技巧,尽管它最初的评价并不高。要理解发生了什么,可以从后往前读。
- 获取当前文件中所有行的列表。
/m
正则表达式选项使得^
可以匹配每行的开头,而不仅仅是目标字符串的开头。 - 对于每一行,尝试捕获这一行开头的一个或多个数字,如果没有则默认为 -1。
- 按排序关键字的升序对这些行进行排序。
- 最后,按排序后的顺序打印这些行。如果启用了就地编辑,
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 的工作原理,所有的临时变量看起来就像是多余的杂乱。