perl在编辑巨大文件时的应用

来源:百度文库 编辑:神马文学网 时间:2024/04/29 10:55:13
日期:[2006-07-26] 人气:[51]评论:[0]条 字体:[大中小] 得分:[0.00]
在实际中我们经常需要处理一些非常大的文件,这时会有两个问题我们比较关注:一个是处理程序的性能,希望越快越好;另一个就是空间的占用问题,不希望产生中间的文件,尤其是在有些空间非常紧张的磁盘上。
关于性能,通常用C编写的程序性能较高,这是显然的,在此我们暂不讨论。实际上一个编写良好的shell脚本往往能用可以接受的性能简单地完成任务。对于不太复杂的处理脚本甚至可以写在一行上,调试起来非常方便。相比之下,相应的C程序就有点“杀鸡用牛刀”的感觉。我并不想挑起另一场Language War,我绝对承认:C的执行效率更高;这里只是想说明:对于一些简单的问题,shell的“开发效率”要更高一些。
另外一个焦点就是空间占用问题,如果能不产生临时或者中间文件的话就比较理想。
大多数shell工具程序都具有相同的特点,可以从标准输入读取输入,并把结果写到标准输出,这一特点非常好,使我们用管道连接多个工具完成一个相对复杂的任务成为可能。但这样做势必要生成中间文件,例如:
grep pattern file > tmpfile
mv tmpfile file
在小文件时这样做没多大关系,开销不会很大。但如果文件非常大的话有时就难以忍受了。
好在最新版本的GNU sed已经支持-i选项,直接编辑原文件(只是在用户表面看来如此:-(,下面会讨论),只要这样就行了:
sed -i ‘/pattern/!d‘ file
情况似乎很完美了,没有中间文件,没有额外的空间开销,问题解决!但是稍等,真的如此吗?一个不可就药的怀疑论者总会对此疑虑重重。^_^OK,我们来测试一下:
删除一个文件的前10行,测试环境:P4 Xeon X2, 内存1G,10000转SCSI磁盘,CentOS 4.2 x86_64。GNU sed 4.1.2, Perl 5.8.5。
CODE:
[Copy to clipboard]
# seq 10000 >file1
# sed -i ‘1,10d‘p file1|lsof -c sed
COMMAND   PID USER   FD   TYPE DEVICE     SIZE    NODE NAME
sed     14855 root  cwd    DIR  253,0     4096 5357570 /home/user1
sed     14855 root  rtd    DIR  253,0     4096       2 /
sed     14855 root  txt    REG  253,0    52904 6012986 /bin/sed
sed     14855 root  mem    REG  253,0 48508544 2559248 /usr/lib/locale/locale-archive
sed     14855 root  mem    REG  253,0    21546 2589098 /usr/lib64/gconv/gconv-modules.cache
sed     14855 root  mem    REG  253,0   182160 2589145 /usr/lib64/gconv/GB18030.so
sed     14855 root  mem    REG  253,0   105080 3997927 /lib64/ld-2.3.4.so
sed     14855 root  mem    REG  253,0  1489097 3997928 /lib64/tls/libc-2.3.4.so
sed     14855 root    0u   CHR  136,3                5 /dev/pts/3
sed     14855 root    1w  FIFO    0,7           130511 pipe
sed     14855 root    2u   CHR  136,3                5 /dev/pts/3
sed     14855 root    3r   REG  253,0    48894 5367617 /home/user1/file1
sed     14855 root    4u   REG  253,0    28263 5367609 /home/user1/sed0cb2We
先说明一下,这里用lsof工具监视sed打开的文件,你也许需要su成为root才行。另外sed处理的文件不能太短,让lsof可以抓到。
请看最后两行,倒数第二行是sed处理的目标文件,最后一行是...
哈哈,抓到了!sed偷偷地打开了一个文件。
让我们再看清楚一点:
CODE:
[Copy to clipboard]
# seq 1000000 >file1
# sed -i ‘1,10d‘ file1|{ lsof -a +r 1 -c sed -d3,4;}
COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
sed     16030 root    3r   REG  253,0 6888894 5367609 /home/user1/file1
sed     16030 root    4u   REG  253,0   31778 5367617 /home/user1/sedmXZuni
=======
COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
sed     16030 root    3r   REG  253,0 6888894 5367609 /home/user1/file1
sed     16030 root    4u   REG  253,0 1613492 5367617 /home/user1/sedmXZuni
=======
COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
sed     16030 root    3r   REG  253,0 6888894 5367609 /home/user1/file1
sed     16030 root    4u   REG  253,0 3285078 5367617 /home/user1/sedmXZuni
=======
COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
sed     16030 root    3r   REG  253,0 6888894 5367609 /home/user1/file1
sed     16030 root    4u   REG  253,0 4959317 5367617 /home/user1/sedmXZuni
=======
COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
sed     16030 root    3r   REG  253,0 6888894 5367609 /home/user1/file1
sed     16030 root    4u   REG  253,0 6631246 5367617 /home/user1/sedmXZuni
=======
这次处理的文件加大了,lsof每1秒钟采样一次。可以看到临时文件越来越大,最后接近原文件的大小。
如此我们可以推论:sed -i的处理过程是先将输出写入一个临时文件,然后自动将临时文件改名为原文件,--就像前面我们手工做的那样。
这样的话,用sed -i只是方便了一点,并没有空间占用上的优势。
那么是不是就非用C不可呢?别急,flw在本版曾给出过一个perl脚本(请参看:如何不需要更多的空间,去掉文件的首位注释行?),为了和上面的 例子对应,改写如下:
CODE:
[Copy to clipboard]
$ cat t.pl
#!/usr/bin/perl
$fn = shift;
open R, "<$fn";
open W, "+<$fn";
while(){
print W if $. > 10
}
truncate( W, tell(W) );
这段代码用两个句柄打开要处理的文件,处理的结果写回原文件,最后截断文件的长度以适应处理后的结果。很明显这里没有用到任何中间文件。
经测试,脚本工作得很好,而且性能比sed要高出一个量级。
CODE:
[Copy to clipboard]
$ seq 100000 >file1
$ time ./t.pl file1
real    0m0.075s
user    0m0.073s
sys     0m0.002s
$ seq 100000 >file1
$ time sed -i ‘1, 10d‘ file1
real    0m0.417s
user    0m0.134s
sys     0m0.283s
可以相信,性能的差距主要是临时文件的IO造成的,如果去掉-i选项,sed的性能会好很多,与perl在一个量级上:
CODE:
[Copy to clipboard]
$ time sed ‘1, 10d‘ file1 >/dev/null
real    0m0.072s
user    0m0.071s
sys     0m0.001s
相应的perl代码的性能也相近:
CODE:
[Copy to clipboard]
$ time perl -ne ‘print if $. > 10‘ file1 >/dev/null
real    0m0.071s
user    0m0.070s
sys     0m0.001s
由此可见flw的代码效率相当高,额外的磁盘IO很少。
至此我们终于有一种方法解决了大文件的空间占用问题。似乎可以大功告成,收兵回营了。但是再等一下,flw的代码性能虽好,但还是稍微麻烦了一点。有没有性能又好编写又简单的方法呢?
上面我们已经讨论过sed的-i选项,我们知道perl也有-i选项,实际上GNU sed的-i选项应该是从perl借鉴过去的。既然sed -i性能很差,perl -i性能究竟如何呢?关键在于,它是否会使用中间文件--因为那会引入很多磁盘IO。我们来测试看看:
CODE:
[Copy to clipboard]
$seq 100000 >file1
$ time perl -i -ne ‘print if $. > 10‘ file1
real    0m0.076s
user    0m0.070s
sys     0m0.006s
不错!性能和flw的代码相差不大。这似乎说明没有中间文件,我们来验证一下:
CODE:
[Copy to clipboard]
# perl -i -ne ‘print if $. > 10‘ file1|lsof -a -c perl -d0-9
COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
perl    16279 root    0u   CHR  136,3               5 /dev/pts/3
perl    16279 root    1w  FIFO    0,7          136352 pipe
perl    16279 root    2u   CHR  136,3               5 /dev/pts/3
perl    16279 root    3r   REG  253,0 6888602 5367617 /home/user1/file1 (deleted)
perl    16279 root    4w   REG  253,0  225280 5367609 /home/user1/file1
如我们所料:perl打开了file1两次,没有中间文件。对比一下flw代码的情况:
CODE:
[Copy to clipboard]
# ./t.pl file1|lsof -a -c t.pl -d0-9
COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
t.pl    16294 root    0u   CHR  136,3               5 /dev/pts/3
t.pl    16294 root    1w  FIFO    0,7          136475 pipe
t.pl    16294 root    2u   CHR  136,3               5 /dev/pts/3
t.pl    16294 root    3r   REG  253,0 6888562 5367609 /home/user1/file1
t.pl    16294 root    4u   REG  253,0 6888562 5367609 /home/user1/file1
让我们再加大文件试试:
CODE:
[Copy to clipboard]
$ seq 10000000 >file1
$ time ./t.pl file1
real    0m7.810s
user    0m7.524s
sys     0m0.284s
$ seq 10000000 >file1
$ time perl -i -ne ‘print if $. > 10‘ file1
real    0m7.825s
user    0m7.189s
sys     0m0.635s
如上,flw的代码sys时间较少,反映其IO耗时较少,但user时间稍长点,可能是在显式的while循环上吃亏的缘故。总的来看两者差距微小。
至此,我们已经得到了perl处理大文件的两种方法,两者性能相差无几,但perl -i更加简单,可以写出漂亮的单行脚本--one liner,推荐大家优先使用。