免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
1234下一页
最近访问板块 发新帖
查看: 12047 | 回复: 39
打印 上一主题 下一主题

[原创]perl在编辑巨大文件时的应用 [复制链接]

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-07-25 18:02 |只看该作者 |倒序浏览
在实际中我们经常需要处理一些非常大的文件,这时会有两个问题我们比较关注:一个是处理程序的性能,希望越快越好;另一个就是空间的占用问题,不希望产生中间的文件,尤其是在有些空间非常紧张的磁盘上。

关于性能,通常用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。
  1. # seq 10000 >file1
  2. # sed -i '1,10d'p file1|lsof -c sed
  3. COMMAND   PID USER   FD   TYPE DEVICE     SIZE    NODE NAME
  4. sed     14855 root  cwd    DIR  253,0     4096 5357570 /home/user1
  5. sed     14855 root  rtd    DIR  253,0     4096       2 /
  6. sed     14855 root  txt    REG  253,0    52904 6012986 /bin/sed
  7. sed     14855 root  mem    REG  253,0 48508544 2559248 /usr/lib/locale/locale-archive
  8. sed     14855 root  mem    REG  253,0    21546 2589098 /usr/lib64/gconv/gconv-modules.cache
  9. sed     14855 root  mem    REG  253,0   182160 2589145 /usr/lib64/gconv/GB18030.so
  10. sed     14855 root  mem    REG  253,0   105080 3997927 /lib64/ld-2.3.4.so
  11. sed     14855 root  mem    REG  253,0  1489097 3997928 /lib64/tls/libc-2.3.4.so
  12. sed     14855 root    0u   CHR  136,3                5 /dev/pts/3
  13. sed     14855 root    1w  FIFO    0,7           130511 pipe
  14. sed     14855 root    2u   CHR  136,3                5 /dev/pts/3
  15. sed     14855 root    3r   REG  253,0    48894 5367617 /home/user1/file1
  16. sed     14855 root    4u   REG  253,0    28263 5367609 /home/user1/sed0cb2We
复制代码


先说明一下,这里用lsof工具监视sed打开的文件,你也许需要su成为root才行。另外sed处理的文件不能太短,让lsof可以抓到。
请看最后两行,倒数第二行是sed处理的目标文件,最后一行是...
哈哈,抓到了!sed偷偷地打开了一个文件。
让我们再看清楚一点:
  1. # seq 1000000 >file1
  2. # sed -i '1,10d' file1|{ lsof -a +r 1 -c sed -d3,4;}
  3. COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
  4. sed     16030 root    3r   REG  253,0 6888894 5367609 /home/user1/file1
  5. sed     16030 root    4u   REG  253,0   31778 5367617 /home/user1/sedmXZuni
  6. =======
  7. COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
  8. sed     16030 root    3r   REG  253,0 6888894 5367609 /home/user1/file1
  9. sed     16030 root    4u   REG  253,0 1613492 5367617 /home/user1/sedmXZuni
  10. =======
  11. COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
  12. sed     16030 root    3r   REG  253,0 6888894 5367609 /home/user1/file1
  13. sed     16030 root    4u   REG  253,0 3285078 5367617 /home/user1/sedmXZuni
  14. =======
  15. COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
  16. sed     16030 root    3r   REG  253,0 6888894 5367609 /home/user1/file1
  17. sed     16030 root    4u   REG  253,0 4959317 5367617 /home/user1/sedmXZuni
  18. =======
  19. COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
  20. sed     16030 root    3r   REG  253,0 6888894 5367609 /home/user1/file1
  21. sed     16030 root    4u   REG  253,0 6631246 5367617 /home/user1/sedmXZuni
  22. =======
复制代码

这次处理的文件加大了,lsof每1秒钟采样一次。可以看到临时文件越来越大,最后接近原文件的大小。

如此我们可以推论:sed -i的处理过程是先将输出写入一个临时文件,然后自动将临时文件改名为原文件,--就像前面我们手工做的那样。
这样的话,用sed -i只是方便了一点,并没有空间占用上的优势。

那么是不是就非用C不可呢?别急,flw在本版曾给出过一个perl脚本(请参看:如何不需要更多的空间,去掉文件的首位注释行?),为了和上面的 例子对应,改写如下:
  1. $ cat t.pl
  2. #!/usr/bin/perl
  3. $fn = shift;
  4. open R, "<$fn";
  5. open W, "+<$fn";
  6. while(<R>){
  7.     print W if $. > 10
  8. }
  9. truncate( W, tell(W) );
复制代码


这段代码用两个句柄打开要处理的文件,处理的结果写回原文件,最后截断文件的长度以适应处理后的结果。很明显这里没有用到任何中间文件。
经测试,脚本工作得很好,而且性能比sed要高出一个量级。
  1. $ seq 100000 >file1
  2. $ time ./t.pl file1

  3. real    0m0.075s
  4. user    0m0.073s
  5. sys     0m0.002s

  6. $ seq 100000 >file1
  7. $ time sed -i '1, 10d' file1

  8. real    0m0.417s
  9. user    0m0.134s
  10. sys     0m0.283s
复制代码

可以相信,性能的差距主要是临时文件的IO造成的,如果去掉-i选项,sed的性能会好很多,与perl在一个量级上:
  1. $ time sed '1, 10d' file1 >/dev/null

  2. real    0m0.072s
  3. user    0m0.071s
  4. sys     0m0.001s
复制代码

相应的perl代码的性能也相近:
  1. $ time perl -ne 'print if $. > 10' file1 >/dev/null

  2. real    0m0.071s
  3. user    0m0.070s
  4. sys     0m0.001s
复制代码


由此可见flw的代码效率相当高,额外的磁盘IO很少。


至此我们终于有一种方法解决了大文件的空间占用问题。似乎可以大功告成,收兵回营了。但是再等一下,flw的代码性能虽好,但还是稍微麻烦了一点。有没有性能又好编写又简单的方法呢?

[woodie注:]下面加了灰色的部分是这个帖子最早的一段错误的内容。由于自己的粗心得出了错误的结论,多谢waker兄指出来!之所以一直没有修改下面的内容,是因为想保留帖子的原貌,不想文过饰非。错误的东西摆在那里,大家都看得到,也可以警醒我,一定要严谨,失之毫厘,往往就会差之千里!后来waker兄提到,怕没有耐心的网友没看到后面的帖子,不知道下面的结论是错误的。我想他讲的很有道理!所以现在把下面错误的部分改为灰色,请大家注意,那是错的!
perl -i的处理方式是先将要编辑的文件改名,然后把处理的结果写入以原来名字命名的新文件,所以也会产生新文件,无法避免额外的空间需求。

下面再转贴woodie在后面写的两个perl小玩意儿,让没耐心的网友也有机会看到。^_^


要想利用perl的truncat来截断文件,但又不想每次编写perl代码,可以用perl写一个简单的wrapper,类似perl写的rename工具那样调用。例如下面的例子,pdel,用来在大文件中删除一些行:
  1. #!/usr/bin/perl
  2. die "Usage: $0 perl-condition file\n" if $#ARGV != 1;
  3. $cmd = "print W unless $ARGV[0]";
  4. $file = $ARGV[1];
  5. open R, "<$file" or die "Cannot open file $file to read\n";
  6. open W, "+<$file" or die "Cannot open file $file to write\n";
  7. eval "while(<R>){ $cmd;}";
  8. truncate( W, tell(W) );
复制代码

调用方法:
pdel 删除条件 文件名
如删除前10行:
pdel '$. <= 10' file
删除#打头的行:
pdel '/^#/' file
删除空行:
pdel '/^$/' file

另一个例子,用来删行的一部分,有的朋友可能用的着。
  1. #!/usr/bin/perl
  2. die "Usage: $0 pattern-to-del file\n" if $#ARGV != 1;
  3. $cmd = "s/$ARGV[0]//g;print W";
  4. $file = $ARGV[1];
  5. open R, "<$file" or die "Cannot open file $file to read\n";
  6. open W, "+<$file" or die "Cannot open file $file to write\n";
  7. eval "while(<R>){ $cmd;}";
  8. 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。我们来测试看看:
  1. $seq 100000 >file1
  2. $ time perl -i -ne 'print if $. > 10' file1

  3. real    0m0.076s
  4. user    0m0.070s
  5. sys     0m0.006s
复制代码

不错!性能和flw的代码相差不大。这似乎说明没有中间文件,我们来验证一下:
  1. # perl -i -ne 'print if $. > 10' file1|lsof -a -c perl -d0-9
  2. COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
  3. perl    16279 root    0u   CHR  136,3               5 /dev/pts/3
  4. perl    16279 root    1w  FIFO    0,7          136352 pipe
  5. perl    16279 root    2u   CHR  136,3               5 /dev/pts/3
  6. perl    16279 root    3r   REG  253,0 6888602 5367617 /home/user1/file1 (deleted)
  7. perl    16279 root    4w   REG  253,0  225280 5367609 /home/user1/file1
复制代码

如我们所料:perl打开了file1两次,没有中间文件。对比一下flw代码的情况:
  1. # ./t.pl file1|lsof -a -c t.pl -d0-9
  2. COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
  3. t.pl    16294 root    0u   CHR  136,3               5 /dev/pts/3
  4. t.pl    16294 root    1w  FIFO    0,7          136475 pipe
  5. t.pl    16294 root    2u   CHR  136,3               5 /dev/pts/3
  6. t.pl    16294 root    3r   REG  253,0 6888562 5367609 /home/user1/file1
  7. t.pl    16294 root    4u   REG  253,0 6888562 5367609 /home/user1/file1
复制代码

让我们再加大文件试试:
  1. $ seq 10000000 >file1
  2. $ time ./t.pl file1

  3. real    0m7.810s
  4. user    0m7.524s
  5. sys     0m0.284s

  6. $ seq 10000000 >file1
  7. $ time perl -i -ne 'print if $. > 10' file1

  8. real    0m7.825s
  9. user    0m7.189s
  10. sys     0m0.635s
复制代码

如上,flw的代码sys时间较少,反映其IO耗时较少,但user时间稍长点,可能是在显式的while循环上吃亏的缘故。总的来看两者差距微小。

至此,我们已经得到了perl处理大文件的两种方法,两者性能相差无几,但perl -i更加简单,可以写出漂亮的单行脚本--one liner,推荐大家优先使用。
[/code]

[ 本帖最后由 woodie 于 2006-10-14 21:12 编辑 ]

评分

参与人数 1可用积分 +3 收起 理由
waker + 3

查看全部评分

论坛徽章:
0
2 [报告]
发表于 2006-07-25 18:14 |只看该作者
还没这么深的研究过,不过明显的性能差别还是可以看出来的,一般情况下一个文件的每行只处理一遍效率就可以了。

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
3 [报告]
发表于 2006-07-25 20:13 |只看该作者
学perl去~~~
PS:用awk会如何呢?

论坛徽章:
7
荣誉版主
日期:2011-11-23 16:44:17子鼠
日期:2014-07-24 15:38:07狮子座
日期:2014-07-24 11:00:54巨蟹座
日期:2014-07-21 19:03:10双子座
日期:2014-05-22 12:00:09卯兔
日期:2014-05-08 19:43:17卯兔
日期:2014-08-22 13:39:09
4 [报告]
发表于 2006-07-25 23:27 |只看该作者
勉强来个命令组合的
  1. off=$(($(head -10 file|wc -c)+0))
  2. n=$(dd if=file of=file ibs=1 skip=$off conv=notrunc 2>&1)
  3. :|dd of=file obs=${n%%+*} seek=1
复制代码

btw:火兄慢走

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
5 [报告]
发表于 2006-07-25 23:33 |只看该作者
原帖由 r2007 于 2006-7-25 23:27 发表
勉强来个命令组合的
  1. off=$(($(head -10 file|wc -c)+0))
  2. n=$(dd if=file of=file ibs=1 skip=$off conv=notrunc 2>&1)
  3. :|dd of=file obs=${n%%+*} seek=1
复制代码

btw:火兄慢走[img]http://bbs ...

呵呵~~~

论坛徽章:
7
荣誉版主
日期:2011-11-23 16:44:17子鼠
日期:2014-07-24 15:38:07狮子座
日期:2014-07-24 11:00:54巨蟹座
日期:2014-07-21 19:03:10双子座
日期:2014-05-22 12:00:09卯兔
日期:2014-05-08 19:43:17卯兔
日期:2014-08-22 13:39:09
6 [报告]
发表于 2006-07-26 00:10 |只看该作者
ibs=1是逐字节传送,影响效率,又改进了一下,seq 1000000 >file 把home分区填满了,没试
seq 100000时不输于perl -i的方法。

  1. off=$(($(find file -printf "%s")-$(head -10 file|wc -c)))
  2. sed '1,10d' file|dd of=file conv=notrunc bs=32k
  3. :|dd of=file obs=$off seek=1
复制代码

评分

参与人数 1可用积分 +1 收起 理由
waker + 1

查看全部评分

论坛徽章:
8
摩羯座
日期:2014-11-26 18:59:452015亚冠之浦和红钻
日期:2015-06-23 19:10:532015亚冠之西悉尼流浪者
日期:2015-08-21 08:40:5815-16赛季CBA联赛之山东
日期:2016-01-31 18:25:0515-16赛季CBA联赛之四川
日期:2016-02-16 16:08:30程序设计版块每日发帖之星
日期:2016-06-29 06:20:002017金鸡报晓
日期:2017-01-10 15:19:5615-16赛季CBA联赛之佛山
日期:2017-02-27 20:41:19
7 [报告]
发表于 2006-07-26 08:35 |只看该作者
to woodie兄:
  1. # perl -i -ne 'print if $. > 10' file1|lsof -a -c perl -d0-9
  2. COMMAND   PID USER   FD   TYPE DEVICE    SIZE    NODE NAME
  3. perl    16279 root    0u   CHR  136,3               5 /dev/pts/3
  4. perl    16279 root    1w  FIFO    0,7          136352 pipe
  5. perl    16279 root    2u   CHR  136,3               5 /dev/pts/3
  6. perl    16279 root    3r   REG  253,0 6888602 5367617 /home/user1/file1 (deleted)
  7. perl    16279 root    4w   REG  253,0  225280 5367609 /home/user1/file1
复制代码

不知道有否注意到两个file1的inode不同么?那么这两个file1是同一个文件么?
flw的方法是基于已经人工的预见了写指针永远都在读指针的前面,虽然Larry Wall的作品一项智能到另人吃不消,但窃以为还不会作这种判断吧?

论坛徽章:
8
摩羯座
日期:2014-11-26 18:59:452015亚冠之浦和红钻
日期:2015-06-23 19:10:532015亚冠之西悉尼流浪者
日期:2015-08-21 08:40:5815-16赛季CBA联赛之山东
日期:2016-01-31 18:25:0515-16赛季CBA联赛之四川
日期:2016-02-16 16:08:30程序设计版块每日发帖之星
日期:2016-06-29 06:20:002017金鸡报晓
日期:2017-01-10 15:19:5615-16赛季CBA联赛之佛山
日期:2017-02-27 20:41:19
8 [报告]
发表于 2006-07-26 08:37 |只看该作者
to 火哥:
awk的方法是指

awk 'NR>10{print >"file"}' file么?

如果file的size大于awk缓冲区的大小,结果是不可以预料的吧?

论坛徽章:
8
摩羯座
日期:2014-11-26 18:59:452015亚冠之浦和红钻
日期:2015-06-23 19:10:532015亚冠之西悉尼流浪者
日期:2015-08-21 08:40:5815-16赛季CBA联赛之山东
日期:2016-01-31 18:25:0515-16赛季CBA联赛之四川
日期:2016-02-16 16:08:30程序设计版块每日发帖之星
日期:2016-06-29 06:20:002017金鸡报晓
日期:2017-01-10 15:19:5615-16赛季CBA联赛之佛山
日期:2017-02-27 20:41:19
9 [报告]
发表于 2006-07-26 08:39 |只看该作者
to 007哥
dd of=file obs=$off seek=1这种截断方法太牛叉了

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
10 [报告]
发表于 2006-07-26 09:04 |只看该作者
原帖由 r2007 于 2006-7-25 23:27 发表
勉强来个命令组合的
  1. off=$(($(head -10 file|wc -c)+0))
  2. n=$(dd if=file of=file ibs=1 skip=$off conv=notrunc 2>&1)
  3. :|dd of=file obs=${n%%+*} seek=1
复制代码

btw:火兄慢走[img]http://bbs ...

7兄dd用得妙!学习...
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP