- 论坛徽章:
- 1
|
在实际中我们经常需要处理一些非常大的文件,这时会有两个问题我们比较关注:一个是处理程序的性能,希望越快越好;另一个就是空间的占用问题,不希望产生中间的文件,尤其是在有些空间非常紧张的磁盘上。
关于性能,通常用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。
- # 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偷偷地打开了一个文件。
让我们再看清楚一点:
- # 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脚本(请参看:如何不需要更多的空间,去掉文件的首位注释行?),为了和上面的 例子对应,改写如下:
- $ cat t.pl
- #!/usr/bin/perl
- $fn = shift;
- open R, "<$fn";
- open W, "+<$fn";
- while(<R>){
- print W if $. > 10
- }
- truncate( W, tell(W) );
复制代码
这段代码用两个句柄打开要处理的文件,处理的结果写回原文件,最后截断文件的长度以适应处理后的结果。很明显这里没有用到任何中间文件。
经测试,脚本工作得很好,而且性能比sed要高出一个量级。
- $ 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在一个量级上:
- $ time sed '1, 10d' file1 >/dev/null
- real 0m0.072s
- user 0m0.071s
- sys 0m0.001s
复制代码
相应的perl代码的性能也相近:
- $ time perl -ne 'print if $. > 10' file1 >/dev/null
- real 0m0.071s
- user 0m0.070s
- sys 0m0.001s
复制代码
由此可见flw的代码效率相当高,额外的磁盘IO很少。
至此我们终于有一种方法解决了大文件的空间占用问题。似乎可以大功告成,收兵回营了。但是再等一下,flw的代码性能虽好,但还是稍微麻烦了一点。有没有性能又好编写又简单的方法呢?
[woodie注:]下面加了灰色的部分是这个帖子最早的一段错误的内容。由于自己的粗心得出了错误的结论,多谢waker兄指出来!之所以一直没有修改下面的内容,是因为想保留帖子的原貌,不想文过饰非。错误的东西摆在那里,大家都看得到,也可以警醒我,一定要严谨,失之毫厘,往往就会差之千里!后来waker兄提到,怕没有耐心的网友没看到后面的帖子,不知道下面的结论是错误的。我想他讲的很有道理!所以现在把下面错误的部分改为灰色,请大家注意,那是错的!
perl -i的处理方式是先将要编辑的文件改名,然后把处理的结果写入以原来名字命名的新文件,所以也会产生新文件,无法避免额外的空间需求。
下面再转贴woodie在后面写的两个perl小玩意儿,让没耐心的网友也有机会看到。^_^
要想利用perl的truncat来截断文件,但又不想每次编写perl代码,可以用perl写一个简单的wrapper,类似perl写的rename工具那样调用。例如下面的例子,pdel,用来在大文件中删除一些行:
- #!/usr/bin/perl
- die "Usage: $0 perl-condition file\n" if $#ARGV != 1;
- $cmd = "print W unless $ARGV[0]";
- $file = $ARGV[1];
- open R, "<$file" or die "Cannot open file $file to read\n";
- open W, "+<$file" or die "Cannot open file $file to write\n";
- eval "while(<R>){ $cmd;}";
- truncate( W, tell(W) );
复制代码
调用方法:
pdel 删除条件 文件名
如删除前10行:
pdel '$. <= 10' file
删除#打头的行:
pdel '/^#/' file
删除空行:
pdel '/^$/' file
另一个例子,用来删行的一部分,有的朋友可能用的着。
- #!/usr/bin/perl
- die "Usage: $0 pattern-to-del file\n" if $#ARGV != 1;
- $cmd = "s/$ARGV[0]//g;print W";
- $file = $ARGV[1];
- open R, "<$file" or die "Cannot open file $file to read\n";
- open W, "+<$file" or die "Cannot open file $file to write\n";
- eval "while(<R>){ $cmd;}";
- truncate( W, tell(W) );
复制代码
将上面文件存为pdw,
chmod +x pdw
用法:./pdw 要删除部分的正则表达式 处理的文件名
注意因为用了g修饰符,所有匹配正则的东西都会删除。
举例:
删除所有的单词hello:
./pdw '\bhello\b' ur-file
删除所有的除号/:
./pdw '\/' ur-file
上面我们已经讨论过sed的-i选项,我们知道perl也有-i选项,实际上GNU sed的-i选项应该是从perl借鉴过去的。既然sed -i性能很差,perl -i性能究竟如何呢?关键在于,它是否会使用中间文件--因为那会引入很多磁盘IO。我们来测试看看:
- $seq 100000 >file1
- $ time perl -i -ne 'print if $. > 10' file1
- real 0m0.076s
- user 0m0.070s
- sys 0m0.006s
复制代码
不错!性能和flw的代码相差不大。这似乎说明没有中间文件,我们来验证一下:
- # 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代码的情况:
- # ./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
复制代码
让我们再加大文件试试:
- $ 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,推荐大家优先使用。[/code]
[ 本帖最后由 woodie 于 2006-10-14 21:12 编辑 ] |
评分
-
查看全部评分
|