我如何动态构建Perl正则表达式?

10

我有一个Perl脚本,使用File::Next::files遍历目录层次结构。 它只会将以".avi"、".flv"、".mp3"、".mp4"和".wmv"结尾的文件返回给脚本。 此外,它将跳过以下子目录:".svn"和任何以".frames"结尾的子目录。 这在下面的file_filterdescend_filter子例程中指定。

my $iter = File::Next::files(
        { file_filter => \&file_filter, descend_filter => \&descend_filter },
        $directory );

sub file_filter { 
    # Called from File::Next:files.
    # Only select video files that end with the following extensions.
    /.(avi|flv|mp3|mp4|wmv)$/
}

sub descend_filter { 
    # Called from File::Next:files.
    # Skip subfolders that either end in ".frames" or are named the following:
    $File::Next::dir !~ /.frames$|^.svn$/
}

我想做的是将允许的文件扩展名和禁止的子目录名称放在配置文件中,以便可以随时更新。

我想知道的是如何编写子程序来基于配置文件中的参数构建正则表达式?

/.(avi|flv|mp3|mp4|wmv)$/

$File::Next::dir !~ /.frames$|^.svn$/

无法回答你的问题,但是你正在使用的那个包看起来很棒。我以前用普通的File::Find做同样的事情,但是它非常混乱。我得试试这个。谢谢!+1 - Zenshai
请查看:http://search.cpan.org/dist/File-Next/ - Dr. Faust
根据情况,http://p3rl.org/File::Find::Rule 可能更适合您。 - Kent Fredric
6个回答

29

假设您已经解析了配置文件以获取扩展名列表和忽略的目录,您可以将正则表达式构建为字符串,然后使用qr运算符将其编译成正则表达式:

my @extensions = qw(avi flv mp3 mp4 wmv);  # parsed from file
my $pattern    = '\.(' . join('|', @wanted) . ')$';
my $regex      = qr/$pattern/;

if ($file =~ $regex) {
    # do something
}

编译并非必需;您可以直接使用字符串模式:

if ($file =~ /$pattern/) {
    # do something
}

目录稍微复杂一些,因为你有两种不同情况: 完整名称和后缀。您的配置文件将必须使用不同的键来明确哪个是哪个。例如 "dir_name" 和 "dir_suffix"。对于完整名称,我会建立一个哈希表:

%ignore = ('.svn' => 1);

目录的后缀可以像文件扩展名一样处理:

my $dir_pattern = '(?:' . join('|', map {quotemeta} @dir_suffix), ')$';
my $dir_regex   = qr/$dir_pattern/;
你甚至可以将这些模式构建成匿名子例程,以避免引用全局变量:
my $file_filter    = sub { $_ =~ $regex };
my $descend_filter = sub {
    ! $ignore{$File::Next::dir} &&
    ! $File::Next::dir =~ $dir_regex;
};

my $iter = File::Next::files({
    file_filter    => $file_filter,
    descend_filter => $descend_filter,
}, $directory);

我没有解释的是,我的客户将修改配置文件。 我不能假设他们会知道Perl或足够了解以不引入正则表达式中的语法错误。 所以我真的不想从配置文件中读取正则表达式,我只想要一个文件扩展名和目录名称和/或目录模式列表。 例如:ext = avi ext = flv ext = mp3 dir = .svn dirp= .frames一旦读取了这些信息,我就想动态创建类似于以下内容的东西:.(avi | flv | mp3 | mp4 | wmv)$ - Dr. Faust

3
假设您使用Config::General来管理配置文件,而且它包含以下行:
<MyApp>
    extensions    avi flv mp3 mp4 wmv
    unwanted      frames svn
</MyApp>

你可以这样使用它(更多信息请参见Config :: General):
my $conf = Config::General->new('/path/to/myapp.conf')->getall();
my $extension_string = $conf{'MyApp'}{'extensions'};

my @extensions = split m{ }, $extension_string;

# Some sanity checks maybe...

my $regex_builder = join '|', @extensions;

$regex_builder = '.(' . $regex_builder . ')$';

my $regex = qr/$regex_builder/;

if($file =~ m{$regex}) {
    # Do something.
}


my $uw_regex_builder = '.(' . join ('|', split (m{ }, $conf{'MyApp'}{'unwanted'})) . ')$';
my $unwanted_regex = qr/$uw_regex_builder/;

if(File::Next::dir !~ m{$unwanted_regex}) {
    # Do something. (Note that this does not enforce /^.svn$/. You
    # will need some kind of agreed syntax in your conf-file for that.
}

(这完全没有经过测试。)

顺便问一下,为什么需要我的 $regex = qr/$regex_builder/ 语句?谢谢。 - Dr. Faust
在使用qr//之前,没有必要将整个正则表达式构建为字符串。你可以这样做:my $regex_builder = join '|', @extensions; my $regex = qr/.($regex_builder)$/; - rjray

3

像普通字符串一样构建它,然后在结尾使用插值将其转换为编译后的正则表达式。 也要小心,您没有转义 . 或将其放入字符类中,因此它表示任何字符(而不是字面上的句点)。

#!/usr/bin/perl

use strict;
use warnings;

my (@ext, $dir, $dirp);
while (<DATA>) {
    next unless my ($key, $val) = /^ \s* (ext|dirp|dir) \s* = \s* (\S+)$/x;
    push @ext, $val if $key eq 'ext';
    $dir = $val     if $key eq 'dir';
    $dirp = $val    if $key eq 'dirp';
}

my $re = join "|", @ext;
$re = qr/[.]($re)$/;

print "$re\n";

while (<>) {
    print /$re/ ? "matched" : "didn't match", "\n";
}

__DATA__
ext = avi
ext = flv
ext = mp3
dir = .svn
dirp= .frames

当我运行代码并打印出$re时,我得到了: (?-xism:.$)看起来可以工作。非常感谢。 - Dr. Faust
我认为可能会有多个目录和/或目录后缀需要忽略的值,尽管这并没有明确指定。 - Michael Carman

1

使用File::Find::Rule相当简单,只需事先创建列表即可。

use strict;
use warnings;
use aliased 'File::Find::Rule';


# name can do both styles. 
my @ignoredDirs = (qr/^.svn/,  '*.frames' );
my @wantExt = qw( *.avi *.flv *.mp3 );

my $finder = Rule->or( 
    Rule->new->directory->name(@ignoredDirs)->prune->discard, 
    Rule->new->file->name(@wantExt)
);

$finder->start('./');

while( my $file = $finder->match() ){
    # Matching file.
}

然后就是填充这些数组的情况了。(注意:上面的代码也未经测试,但很可能有效)。我通常会使用YAML来完成这个任务,它可以让生活更轻松。

use strict;
use warnings;
use aliased 'File::Find::Rule';
use YAML::XS;

my $config = YAML::XS::Load(<<'EOF');
---
ignoredir:
- !!perl/regexp (?-xism:^.svn)
- '*.frames'
want:
- '*.avi'
- '*.flv'
- '*.mp3'
EOF

my $finder = Rule->or( 
    Rule->new->directory->name(@{ $config->{ignoredir} })->prune->discard, 
    Rule->new->file->name(@{ $config->{want} })
);

$finder->start('./');

while( my $file = $finder->match() ){
    # Matching file.
}

注意 使用方便的模块 'aliased.pm',将 "File::Find::Rule" 导入为 "Rule"。


1
如果你想构建一个可能很大的正则表达式,又不想为了调试括号而烦恼,那就使用 Perl 模块来为你构建它吧!
use strict;
use Regexp::Assemble;

my $re = Regexp::Assemble->new->add(qw(avi flv mp3 mp4 wmv));

...

if ($file =~ /$re/) {
    # a match!
}

print "$re\n"; # (?:(?:fl|wm)v|mp[34]|avi)

0

虽然File::Find::Rule已经有处理这种情况的方法,但在类似的情况下,您并不真正需要一个正则表达式。 正则表达式在这里并没有带来太多好处,因为您正在寻找每个文件名末尾的固定字符序列。 您想知道该固定序列是否在您感兴趣的序列列表中。 将所有扩展名存储在哈希中,并在该哈希中查找:

my( $extension ) = $filename =~ m/\.([^.]+)$/;
if( exists $hash{$extension} ) { ... }

您无需构建正则表达式,也不需要通过多个可能的正则表达式来检查每个您需要检查的扩展名。


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