【问题标题】:Why does my Perl regular expression only find the last occurrence?为什么我的 Perl 正则表达式只能找到最后一次出现?
【发布时间】:2010-03-20 13:55:21
【问题描述】:

我在 Perl 脚本中有以下输入,我希望在每个 <table>...</table> 结构中首次出现 NAME="..." 字符串。

整个文件被读入单个字符串,正则表达式作用于该输入。

但是,正则表达式总是返回最后一次出现的 NAME="..." 字符串。谁能解释发生了什么以及如何解决这个问题?

Input file: 
ADSDF
<TABLE>
NAME="ORDERSAA"
line1
line2
NAME="ORDERSA"
line3
NAME="ORDERSAB"
</TABLE>
<TABLE>
line1
line2
NAME="ORDERSB"
line3
</TABLE>
<TABLE>
line1
line2
NAME="ORDERSC"
line3
</TABLE>
<TABLE>
line1
line2
NAME="ORDERSD"
line3
line3
line3
</TABLE>
<TABLE>
line1
line2
NAME="QUOTES2"
line3
NAME="QUOTES3"
NAME="QUOTES4"
line3
NAME="QUOTES5"
line3
</TABLE>
<TABLE>
line1
line2
NAME="QUOTES6"
NAME="QUOTES7"
NAME="QUOTES8"
NAME="QUOTES9"
line3
line3
</TABLE>
<TABLE>
NAME="MyName IsKhan"
</TABLE>

Perl 代码从这里开始:

use warnings;
use strict;

my $nameRegExp = '(<table>((NAME="(.+)")|(.*|\n))*</table>)';

sub extractNames($$){
 my ($ifh, $ofh) = @_;
 my $fullFile;
 read ($ifh, $fullFile, 1024);#Hardcoded to read just 1024 bytes.
 while( $fullFile =~ m#$nameRegExp#gi){
  print "found: ".$4."\n";
 }
}

sub main(){
 if( ($#ARGV + 1 )!= 1){
  die("Usage: extractNames infile\n");
 }
 my $infileName = $ARGV[0];
 my $outfileName = $ARGV[1];
 open my $inFile, "<$infileName" or die("Could not open log file $infileName");
 my $outFile;
 #open my $outFile, ">$outfileName" or die("Could not open log file $outfileName");
 extractNames( $inFile, $outFile );
 close( $inFile );
 #close( $outFile );
}

#call 
main();

【问题讨论】:

  • 请在您的代码中添加一些真正的缩进。

标签: regex perl


【解决方案1】:

试试这个:

'(?><TABLE>\n+(?:(?!</TABLE>|NAME=).*\n+)*)NAME="([^"]+)"'

(?:.*\n+)* 消耗任何不需要的行,而嵌入的前瞻 - (?!&lt;/TABLE&gt;|NAME=) - 防止它超出第一个 NAME 字段或 TABLE 记录的末尾。为了防止没有 NAME 字段的记录,我将大部分表达式包装在一个原子组中 -- (?&gt;...) -- 以防止无意义的回溯。

请注意,现在只有一个捕获组。仅当您确实需要捕获某些内容时才使用它们是一种很好的做法;否则,请使用非捕获品种:(?:...)


编辑:至于你的正则表达式为什么不起作用,简短的回答是贪婪。匹配开始标签后,这部分接管:

((NAME="(.+)")|(.*|\n))*

最外层括号中的部分可以匹配任何内容:标签、NAME= 行、换行符——甚至是空行。将其包裹在一个由贪婪的* 控制的组中,现在它匹配所有内容。没有任何东西可以让它在第一个 NAME 字段,甚至在记录的末尾停止匹配。

所以它实际上是在“查找”每一个出现的NAME="..." 字符串,但它是在一次匹配尝试中完成的,它会一次消耗整个输入。随着封闭* 的每次迭代,捕获组被覆盖;完成后,最终的 NAME 值 -- MyName IsKhan -- 恰好留在第 4 组中。

我使用负前瞻来检查贪心,但您也可以通过使用非贪心量词更直接地做到这一点。以下是我的正则表达式用不情愿的 * 代替否定前瞻的样子:

'<TABLE>\n+(?:.*\n+)*?NAME="([^"]+)"'

不过,简单地切换到非贪婪量词对您的正则表达式没有帮助;您还必须进行一些结构更改。

【讨论】:

  • 谢谢!那行得通。我不知道独立的子表达式 - 学到了一些新东西。
  • 你能解释一下为什么原来的正则表达式不起作用吗?特别是,为什么它总是找到最后一次出现?
【解决方案2】:

首先,使用正则表达式解析 XML 是个坏主意。 其次,您需要将您的正则表达式更改为以下内容:

my $nameRegExp = '(<table>((NAME="(.+)?")|(.*?|\n))*?</table>)';

这样正则表达式变得不贪心,应该返回第一次出现。

【讨论】:

  • 我已经尝试过不贪心的选项;这没用。而且,这不是 XML 文件。它只是碰巧有一个具有表格结构的结构 - 因此我选择使用正则表达式。
【解决方案3】:

尝试让你的正则表达式不贪婪:

my $nameRegExp = '(<table>((NAME="(.+?)")|(.*?|\n))*</table>)';

即使是上面的正则表达式也会列出文件中的所有 NAME 行。它将仅列出每个 &lt;TABLE&gt;...&lt;/TABLE&gt; 块中的一个 NAME 行(最后一个)。

列出您可以执行的所有 NAME 行:

my $nameRegExp = 'NAME="(.+?)"';

print $1;

【讨论】:

  • 我也尝试过非贪婪选项。我想知道为什么它没有找到第一个出现而总是找到最后一个。对此有什么想法吗?
【解决方案4】:
$/ = '</TABLE>';
while (<>) {
    chomp;
    @F = split "\n";
    $g = 0;
    for ($o = 0; $o <= $#F; $o++) {
        if ($F[$o] =~ /^NAME=/) {
            $F[$o] =~ s/^NAME=//g;
            $v = $F[$o];
            $g = 1;
            last;
        }
    }    
    if ($g) {  print $v."\n"; }
}

输出

$ perl myscript.pl file
"ORDERSAA"
"ORDERSB"
"ORDERSC"
"ORDERSD"
"QUOTES2"
"QUOTES6"
"MyName IsKhan"

它的全部要点:使用&lt;/TABLE&gt; 作为记录分隔符和换行符作为字段分隔符。浏览每个字段并找到NAME=。如果找到,替换并获取= 符号后面的字符串。

【讨论】:

  • 我不明白这个脚本在做什么,但看起来不错。
  • 我曾考虑过这种替代方案;但很确定它可以由单个正则表达式处理,因为输入具有某种结构。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-01-12
  • 2021-07-18
  • 1970-01-01
  • 1970-01-01
  • 2016-11-24
  • 1970-01-01
相关资源
最近更新 更多