使用Perl解析文本文件的最有效方法是什么?

6
尽管这很基础,但我找不到类似的问题,如果您知道SO上现有的问题/解决方案,请链接到其中一个。
我有一个大约2MB,大约有16,000行的.txt文件。每个记录长度为160个字符,阻止因子为10。这是一种旧的数据结构,几乎看起来像一个制表符分隔的文件,但分隔是通过单个字符/空格进行的。
首先,我使用glob在目录中获取.txt文件 - 该目录中从不同时存在多个文件,因此这个尝试本身可能效率低下。
my $txt_file = glob "/some/cheese/dir/*.txt";

然后,我使用以下代码打开该文件:
open (F, $txt_file) || die ("Could not open $txt_file");

根据该文件的数据字典,我正在使用Perl的substr()函数在while循环中解析每行中的每个“字段”。
while ($line = <F>)
{
$nom_stat   = substr($line,0,1);
$lname      = substr($line,1,15);
$fname      = substr($line,16,15);
$mname      = substr($line,31,1);
$address    = substr($line,32,30);
$city       = substr($line,62,20);
$st         = substr($line,82,2);
$zip        = substr($line,84,5);
$lnum       = substr($line,93,9);
$cl_rank    = substr($line,108,4);
$ceeb       = substr($line,112,6);
$county     = substr($line,118,2);
$sex        = substr($line,120,1);
$grant_type = substr($line,121,1);
$int_major  = substr($line,122,3);
$acad_idx   = substr($line,125,3);
$gpa        = substr($line,128,5);
$hs_cl_size = substr($line,135,4);
}

这种方法需要处理每一行的时间较长,我想知道是否有更有效的方法来获取文件中每一行的每个字段。是否有人可以建议更高效/首选的方法?

1
请参考 https://dev59.com/MXNA5IYBdhLWcg3wH6EW 获取一些相关基准测试数据。 - mob
1
请参见 http://stackoverflow.com/q/5083436#comment-5695536,以获取 mob 的重复列表。 - daxim
4个回答

8
看起来你在处理固定宽度的字段,是吗?如果是的话,你需要使用 unpack 函数。你提供字段的模板,它将从这些字段中提取信息。有一个可用的 教程,模板信息可以在 pack 的文档中找到,它是 unpack 的逻辑反义词。作为基本示例:
my @values = unpack("A1 A15 A15 ...", $line);

其中 'A' 表示任何文本字符(据我理解),数字表示其数量。对于某些人使用的 unpack 而言,这是一种相当有技巧的方法,但我认为这对于基本使用已经足够了。


@daxim,谢谢,我希望我使用得正确,因为我在编写它的模板方面没有太多经验。 - Joel Berger
谢谢Joel...尽管Mob建议的线程显示substr更好,但它可能仅适用于某些情况。这个解包对我来说是新的,看起来很合理,因为它们确实是固定长度的字段。我会试一下...谢谢。 - CheeseConQueso
substr并不比unpack更快。你有读完整个基准测试帖子吗? - socket puppet
现在我已经做了...我正在运行解包方法...我们会看看它的表现。我不知道为什么这个脚本要花费超过2个小时才能完成...最后的脏验证SQL不需要超过几秒钟。 - CheeseConQueso
1
@CheeseConQueso:我在我的答案中包含了Benchmark以及如何使用它的示例,这样可以更有教育意义。要了解为什么你的程序运行时间很长,BenchmarkDevel::DProf是你Perl工具库中不可或缺的工具。 - Ian C.

4

使用/o选项编译和缓存的单个正则表达式是最快的方法。我使用Benchmark模块以三种方式运行了您的代码,得到了以下结果:

         Rate unpack substr regexp
 unpack 2.59/s     --   -59%   -67%
 substr 6.23/s   141%     --   -21%
 regexp 7.90/s   206%    27%     --

输入是一个具有20k行的文件,每行上都有相同的160个字符(16个重复的字符0123456789)。因此,这与您处理的数据大小相同。

Benchmark::cmpthese()方法按照从最慢最快的顺序输出子例程调用。第一列告诉我们每秒可以运行子程序的次数。正则表达式方法是最快的。不像我之前所述的那样使用解包。对此我感到很抱歉。

基准测试代码如下。打印语句作为健全性检查存在。这是使用Perl 5.10.0构建的darwin-thread-multi-2level版本。

#!/usr/bin/env perl
use Benchmark qw(:all);
use strict;

sub use_substr() {
    print "use_substr(): New itteration\n";
    open(F, "<data.txt") or die $!;
    while (my $line = <F>) {
        my($nom_stat, 
           $lname,   
           $fname,      
           $mname,    
           $address,     
           $city,    
           $st,       
           $zip,         
           $lnum,        
           $cl_rank,
           $ceeb,    
           $county,
           $sex,     
           $grant_type,
           $int_major, 
           $acad_idx,  
           $gpa,   
           $hs_cl_size) = (substr($line,0,1),
                           substr($line,1,15),
                           substr($line,16,15),
                           substr($line,31,1),
                           substr($line,32,30),
                           substr($line,62,20),
                           substr($line,82,2),
                           substr($line,84,5),
                           substr($line,93,9),
                           substr($line,108,4),
                           substr($line,112,6),
                           substr($line,118,2),
                           substr($line,120,1),
                           substr($line,121,1),
                           substr($line,122,3),
                           substr($line,125,3),
                           substr($line,128,5),
                           substr($line,135,4));
       #print "use_substr(): \$lname = $lname\n";
       #print "use_substr(): \$gpa   = $gpa\n";
    }    
    close(F);
    return 1;
}

sub use_regexp() {
    print "use_regexp(): New itteration\n";
    my $pattern = '^(.{1})(.{15})(.{15})(.{1})(.{30})(.{20})(.{2})(.{5})(.{9})(.{4})(.{6})(.{2})(.{1})(.{1})(.{3})(.{3})(.{5})(.{4})';
    open(F, "<data.txt") or die $!;
    while (my $line = <F>) {
        if ( $line =~ m/$pattern/o ) {
            my($nom_stat, 
               $lname,   
               $fname,      
               $mname,    
               $address,     
               $city,    
               $st,       
               $zip,         
               $lnum,        
               $cl_rank,
               $ceeb,    
               $county,
               $sex,     
               $grant_type,
               $int_major, 
               $acad_idx,  
               $gpa,   
               $hs_cl_size) = ( $1,
                                $2,
                                $3,
                                $4,
                                $5,
                                $6,
                                $7,
                                $8,
                                $9,
                                $10,
                                $11,
                                $12,
                                $13,
                                $14,
                                $15,
                                $16,
                                $17,
                                $18);
            #print "use_regexp(): \$lname = $lname\n";
            #print "use_regexp(): \$gpa   = $gpa\n";
        }
    }    
    close(F);
    return 1;
}

sub use_unpack() {
    print "use_unpack(): New itteration\n";
    open(F, "<data.txt") or die $!;
    while (my $line = <F>) {
        my($nom_stat, 
           $lname,   
           $fname,      
           $mname,    
           $address,     
           $city,    
           $st,       
           $zip,         
           $lnum,        
           $cl_rank,
           $ceeb,    
           $county,
           $sex,     
           $grant_type,
           $int_major, 
           $acad_idx,  
           $gpa,   
           $hs_cl_size) = unpack(
               "(A1)(A15)(A15)(A1)(A30)(A20)(A2)(A5)(A9)(A4)(A6)(A2)(A1)(A1)(A3)(A3)(A5)(A4)(A*)", $line
               );
        #print "use_unpack(): \$lname = $lname\n";
        #print "use_unpack(): \$gpa   = $gpa\n";
    }
    close(F);
    return 1;
}

# Benchmark it
my $itt = 50;
cmpthese($itt, {
        'substr' => sub { use_substr(); },
        'regexp' => sub { use_regexp(); },
        'unpack' => sub { use_unpack(); },
    }
);
exit(0)

谢谢Ian...这是一个好的、全面的解释...我会尝试它们,但可能会选择unpack,因为你已经做了所有的艰苦工作。 - CheeseConQueso
我尝试了你的解包方法,但它使每次循环迭代变得明显更慢。我不确定原因是什么。 - CheeseConQueso
没事了 - 我已经找到了...虽然并没有让它变得更快,但肯定有所帮助。 - CheeseConQueso
哇,我真是太蠢了。我才刚注意到cmpthese()函数中的第一列有一个“/s”,而不是“s”。这很重要。我已经更新了我的答案。 - Ian C.
正则表达式没有正确解析这些行...我回到工作时会提供一些样本输入。 - CheeseConQueso

0
在编程中,将每一行进行分割,如下所示:

my @values = split(/\s/,$line);

然后与您的值一起工作。


进一步说,它只有在数据以空格分隔的情况下才能正常工作。由于 OP 从位置零使用一个字符,然后从位置1使用15个字符,再从位置16使用15个字符,因此除非我的数学有误,否则似乎不是这种情况。 - Joel Berger
我曾考虑过使用split,但认为它仅用于固定长度的断点或字符作为其鉴别器。我认为字段之间的断点变化太大,如果不包含在其他逻辑测试中,split是无法处理的。 - CheeseConQueso

0
你可以这样做:
while ($line = <F>){
   if ($line =~ /(.{1}) (.{15}) ........ /){
     $nom_stat = $1;
     $lname = $2;
     ...
   }
}

我认为这比你的substr建议更快,但我不确定它是否是最快的解决方案,但我认为它很有可能是。


这对我来说看起来很神秘 - 我不习惯那种语法。这个尝试在做什么? - CheeseConQueso
这是一个正则表达式,点(dot)可以代表任意一个字符,花括号里的数字表示出现的次数。换句话说:1个字符,空格,15个字符 [等等]。不过我仍然不建议用这种方式,最好使用 unpack() 函数。 - RET
哇,我没想到它会像Ian C展示的那样慢。我真的很惊讶,我以为它至少比substr快... - markijbema

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接