【问题标题】:Trouble with regex in PerlPerl中正则表达式的问题
【发布时间】:2013-04-10 18:36:50
【问题描述】:

我正在尝试编写一个脚本来解析试算表。文件中每一行的布局始终相同,但我在让正则表达式正确匹配时遇到问题。该行的前 10 个字符始终是帐号。这是一个例子:

0000000099 S000 Doe, John  00 1,243.22  01/01/1901 

我正在尝试将这些中的每一个捕获到单独的变量的列中,但我的表达式不起作用。

这是我目前所拥有的。

#!/usr/bin/perl -w
use strict;

my $filename = "S:\\TELLERS\\GalaxyDown\\tbal";
my $answer   = undef;
open(FIN, $filename) || die "File not found";

do {
    print "Enter an account number: ";
    chomp(my $acctNum = <STDIN>);

    if ($acctNum =~ /\d{1,10}/) {
        $acctNum = pad_zeros($acctNum);

        #print "$acctNum\n";    #test to make sure the padding extends the account
                                #number to 10 digits - comment out after verification

        while (<FIN>) {

            #print "$_\n";

            if (m/(^[0-9]{10}/) {
                print "Passed\n";
            }
            else {
                print "Failed\n";
            }
        }

    }
    else {
        print "Invalid account number. Please try again.\n";
    }
    print "Would you like to view another account balance? (yes/no): ";
    chomp($answer = lc <STDIN>);

} while ($answer ne "no");

sub pad_zeros {
    my $optimal_length = 10;
    my $num            = shift;
    $num =~ s/^(\d+)$/("0"x($optimal_length-length$1)).$1/e;
    return $num;
}

任何帮助将不胜感激。

【问题讨论】:

  • 鉴于它是常量,使用 perl 的 split 函数并在空格上进行拆分是否同样容易?我认为这会更直接。
  • 可能是剪切粘贴,但我相信这里有一个错字: if (m/(^[0-9]{10}/){ (@987654324 之前的额外 ( @
  • 哪个正则表达式失败了?失败时的输入是什么?
  • 不幸的是,拆分不起作用,因为名称字段中可能有空格。这是我最初的想法。正则表达式无法匹配除额外的 ( 可能是原因。

标签: regex perl text-parsing string-parsing


【解决方案1】:

您的pad_zeros 函数实际上是sprintf '%0*d', $optimal_length, $num 的简写形式。

您的while(&lt;FIN&gt;) 循环读取 tbal 文件中的所有行,并为该文件中的每一行打印该行是否以十位数字开头,但仅针对输入的第一个帐号(readline 运算符 &lt;&gt; 有效一个迭代器,并且在您阅读所有行后耗尽)。解决方法是打开if 分支内的文件句柄。

还有其他一些可以改进的地方:

  • 您不需要使用undef 初始化标量变量:这已经是它们的默认值了。
  • 要打开文件句柄,您应该 (1) 为该文件句柄使用普通变量,以及 (2) 使用 open 的三参数形式:

    open my $fin, "<", $filename or die "Can't open $filename: $!";
    

    其中$! 包含open 失败的原因。指定显式模式 &lt; 会使一些极端情况更加安全。

  • 反斜杠路径很难看,但 Windows 可以很好地处理普通斜杠 → S:/TELLERS/...

要将一行拆分为多个字段,您必须考虑确切的格式:每个字段是否由公共分隔符分隔,例如空格?在这种情况下,

my @fields = split " ", $line;

会成功的。将" " 更改为正则表达式,确定不同分隔符(制表符、逗号等)的分隔符。

但是,您的格式看起来并不那么简单,因为姓氏后面的逗号可能不是姓氏字段数据的一部分 (?)

类似的正则表达式

my $regex = qr{\A
  \s* ([0-9]{10})
  \s+ (S[0-9]{3})
  \s+ ([^,]+),            # the surname
  \s+ ([^0-9]+(?<!\s))    # other names
  \s+ ([0-9]{2})
  \s+ ([0-9,]+\.[0-9]{2})
  \s+ ([0-9]{2})
   /  ([0-9]{2})
   /  ([0-9]{4})
   \s*\z
}x;
my @fields = $line =~ $regex;

可能会更好,但这取决于您所拥有的确切格式。

匹配名字很困难,因为有些人可能有多个名字。考虑条目Gogh, Vincent vanTucker, Charles III. 我决定匹配“任何不以空格字符结尾的非数字字符串”。

【讨论】:

  • 我将不得不稍微玩一下你的正则表达式,但如果 substr 方法不适合我,它可能会奏效。由于名称字段,拆分不起作用,我无法控制报告中的分隔符。
【解决方案2】:

我没有得到任何积分。 Amon 已经搞定了,并为您提供了您需要知道的一切,包括一些很棒的建议。

您说您的帐户行如下所示:

0000000099 S000 Doe, John  00 1,243.22  01/01/1901 

问题是空格可以用作名称的一部分。 Mary Jane Von Corona 有四个空格。但是,它是一个名字,Mary Jane,和一个姓氏Von Corona。我怎么知道名字在哪里拆分?

最好的方法是使用固定长度的字段,或者使用文件中没有的分隔符。

0000000099|S000|Doe|John|00|1,243.22|01/01/1901

在这里,我使用| 作为字段分隔符。我可以这样做:

my ( $account,   $something,   $something2,
     $last,      $first,       $something3,
     $balance,   $date)                       = split /\|/, $line;

这是在| 上一次性拆分整条线路。

如果字段具有固定宽度,我可以使用substr 函数将这一行中的各个字段分开:

my $account = substr( $line, 0, 10 );   #First 10 characters is always the account number

我还建议使用autodie。这样,您不必测试各种事情,例如您的文件是否已成功打开。当发生这样的事情时,Perl 会自动死掉(并且通常会显示一个很好的错误消息)。

【讨论】:

  • Mary Jane Von Corona三个 个空格和四个字段 :)
【解决方案3】:

您的代码没有明显错误。您没有说出 “不工作” 的意思,但我注意到您正在多次阅读文件以搜索输入。到达文件末尾后,您需要 seek 重新开始或重新打开文件。

这里有一些建议

  • 不要使用-w 命令行限定符。 use warnings 更好

  • 使用单引号分隔包含反斜杠的字符串。那么它们就不需要转义了,除非它们有多个在一起或者它们出现在字符串的末尾

  • 如果您使用 snake_case 而不是 CamelCase 作为本地标识符,您会让许多经验丰富的 Perl 程序员更快乐

  • 目前的最佳实践是使用词法文件句柄和open 的三参数形式。您应该将$! 放入您的die 字符串中,这样您就可以了解为什么打开失败了

  • 您在输入中检查/\d{1,10}/,它测试字符串是否在任何地方包含一串数字。你的意思可能是/^\d{1,10}$/

  • sub pad_zeroes 最好写成sprintf '%0*d', $optimal_length, $_[0]

这是一个建议的重写。我修改了代码,检查输入文本指定的账号是否被读取,想必是你的本意。

注意,在文件中顺序搜索每个输入的新帐号效率非常低,并且仅适用于小型数据文件或一次性程序。我建议您将Tie::File 与一个哈希一起使用,该哈希指示要读取绑定数组的哪个元素来访问给定的帐号。

注意您的文件似乎使用了固定宽度的字段,即字段始终在行中的相同字符位置开始和结束。如果是这样,那么与其使用正则表达式来处理数据,不如使用substrunpack。更好的是,Parse::FixedLength 模块允许您简单地指定每个字段的长度,其余的工作将为您完成。

#!/usr/bin/perl

use strict;
use warnings;

my $filename = 'S:\TELLERS\GalaxyDown\tbal';
my $answer;

do {
    print "Enter an account number: ";
    chomp(my $acct_num = <STDIN>);

    if ($acct_num =~ /^\d{1,10}$/) {

        $acct_num = pad_zeroes($acct_num);

        #print "$acct_num\n";    #test to make sure the padding extends the account
                                 #number to 10 digits - comment out after verification

        open(my $fin, '<', $filename) || die "File not found: $!";
        while (<$fin>) {
            if (/^$acct_num/) {
              print "Passed\n";
            }
        }
    }
    else {
        print "Invalid account number. Please try again.\n";
    }
    print "Would you like to view another account balance? (yes/no): ";
    chomp($answer = lc <STDIN>);

} until $answer eq 'no';


sub pad_zeroes {
    my $optimal_length = 10;
    sprintf '%0*d', $optimal_length, $_[0];
}

【讨论】:

  • substr 或类似的函数可能是我最好的选择。我使用它们是因为我在发布时不确定列宽。
【解决方案4】:

如果你想检查整行,你可以使用这样的东西:

  while(<FIN>){

        if( @a = (m/^\s*(\d{1,10})\s+(S\d+)\s+(\w+)\s*,\s*(\w+)\s+(\d\d)\s+(\S+)\s+(\d\d?\/\d\d?\/(?:\d\d)\d\d)\s*/) ) {
            $a[0] = sprintf "%010d", $a[0];
            print "Account number:  $a[0]";
            print "Account series:  $a[1]";
            print "Account owner:   $a[3] $a[2]";
            print "Account type:    $a[4]";
            print "Account balance: $a[5]";
            print "Account date:    $a[6]";
        } else {
            print "Failed\n";
        }

任何与所需格式的偏差都会打印“失败” 您可以根据自己的需要进行调整。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多