Perl中的正则表达式组:如何从一个字符串中捕获匹配未知数量/多个/可变出现次数的正则表达式组中的元素并存入数组?

52
在Perl中,我如何使用一个正则表达式分组来捕获多个匹配项,并将其放入多个数组元素中?
例如,对于一个字符串:
var1=100 var2=90 var5=hello var3="a, b, c" var7=test var3=hello

使用代码处理此内容:

$string = "var1=100 var2=90 var5=hello var3=\"a, b, c\" var7=test var3=hello";

my @array = $string =~ <regular expression here>

for ( my $i = 0; $i < scalar( @array ); $i++ )
{
  print $i.": ".$array[$i]."\n";
}

我希望看到的输出结果是:

0: var1=100
1: var2=90
2: var5=hello
3: var3="a, b, c"
4: var7=test
5: var3=hello

我需要使用正则表达式匹配内容,这些内容都是赋值语句模式,例如:

my @array = $string =~ m/(\w+=[\w\"\,\s]+)*/;

在上述正则表达式中,“*”表示匹配该组的一个或多个出现。

(我不使用split(),因为某些匹配项本身包含空格(即var3...),因此无法得到所需的结果。)

使用上述正则表达式,我只得到:

0: var1=100 var2

我需要在正则表达式中实现这个功能吗?还是需要添加代码?

已经查看了现有的答案,搜索"perl regex multiple group",但线索不够:


6
太长不看,但是因为你认真做作业我给你点赞。 - DVK
顺便说一句,我认为你的问题不是多个组,而是匹配引号。这可以在Perl正则表达式中处理,但需要非常小心。 - DVK
6
http://ideone.com/Qvm2u - Alan Moore
@Alan:对我也有效!你把它发布为评论而不是答案,非常谦虚。如果你把它发布为答案,我可能会接受它作为答案!非常感谢你。你的答案可能是最简单的(即最整洁的解决方案),即不需要支持代码,例如循环结构。 - therobyouknow
1
在填写了您代码中的空白部分后,我仍然不确定您的问题是关于其中哪一部分的。由于有点匆忙,我只是发布了链接并离开了。您是想理解所有匹配项如何在数组中累加的方式吗? - Alan Moore
显示剩余4条评论
9个回答

48
my $string = "var1=100 var2=90 var5=hello var3=\"a, b, c\" var7=test var3=hello";

while($string =~ /(?:^|\s+)(\S+)\s*=\s*("[^"]*"|\S*)/g) {
        print "<$1> => <$2>\n";
}

输出:

<var1> => <100>
<var2> => <90>
<var5> => <hello>
<var3> => <"a, b, c">
<var7> => <test>
<var3> => <hello>

解释:

首先看最后一个部分:末尾的g标志意味着您可以将正则表达式应用于字符串多次。第二次匹配将从上一次匹配在字符串中结束的位置继续进行匹配。

现在看正则表达式:(?:^|\s+) 匹配字符串的开头或一个或多个空格组。这是必需的,这样下次应用正则表达式时,我们将跳过键值对之间的空格。 ?: 的含义是括号内的内容不会被捕获为组(我们只需要键和值,而不是空格)。\S+ 匹配变量名。然后跳过任意数量的空格和等号。最后,("[^"]*"|\S*)/ 匹配两个引号之间任意数量的字符或任意数量的非空格字符作为值。注意,引号匹配非常脆弱,并且无法正确处理转义引号,例如"\"quoted\""将导致"\"

编辑:

如果你真的想获取整个赋值语句,而不是单个键/值,那么这是提取它们的一行代码:

my @list = $string =~ /(?:^|\s+)((?:\S+)\s*=\s*(?:"[^"]*"|\S*))/g;

1
OP说只需要一个正则表达式组,但这个表达式捕获了2个正则表达式组... - dawg
好的,我的错。你可以通过在正则表达式的键/值部分周围添加更多括号来修复这个问题。 - jkramer
我的 $string = "var1=100 var2=90 var5=hello var3="a, b, c" var7=test var3=hello";while($string =~ /((?:^|\s+)(\S+\s*=\s*"[^"]"|\S))/g) { print "<$1>\n"; } 或者,http://ideone.com/otgyc -- 这会在整个表达式周围添加一个额外的括号集: - therobyouknow
1
更新了帖子,加入了一行代码,可以提取完整的var=value赋值。 - jkramer
为什么是 while 而不是 for 或者 foreach?从其他编程语言来看,这看起来很奇怪。 - BeniBela
显示剩余7条评论

11

使用正则表达式时,我喜欢称之为“钉住和拉伸”的技巧:先将你知道会出现的特征固定下来(钉住),然后抓取两者之间的内容(拉伸)。

在这种情况下,你知道一个单一的赋值符号是匹配的。

\b\w+=.+
并且在$string中有多个这样的重复。请记住,\b表示单词边界:
引用块: 单词边界(\b)是两个字符之间的位置,其一侧具有\w,另一侧具有\W(以任何顺序),计算从字符串开头和结尾开始的虚拟字符作为匹配\W
赋值语句中的值可能有点棘手,但您还知道每个值都将以空格结束——尽管不一定是遇到的第一个空格!之后是另一个赋值或字符串结尾。
为了避免重复断言模式,请使用qr//编译它一次,并在模式中重用它,以及前瞻断言(?=...),以扩展匹配范围,捕获整个值,同时防止其溢出到下一个变量名中。
在列表上下文中使用m//g对模式进行匹配会导致以下行为:
引用块: /g修饰符指定全局模式匹配,即在字符串中尽可能多地进行匹配。它的行为取决于上下文。在列表上下文中,它返回由正则表达式中任何捕获括号匹配的子字符串列表。如果没有括号,则返回所有匹配的字符串列表,就好像整个模式周围有括号。
模式$assignment使用非贪婪的.+?,一旦前瞻看到另一个赋值或行尾,就会截断该值。请记住,匹配返回所有捕获子模式的子字符串,因此前瞻断言的选择使用非捕获的(?:...)。相反,qr//包含隐式捕获的括号。
#! /usr/bin/perl

use warnings;
use strict;

my $string = <<'EOF';
var1=100 var2=90 var5=hello var3="a, b, c" var7=test var3=hello
EOF

my $assignment = qr/\b\w+ = .+?/x;
my @array = $string =~ /$assignment (?= \s+ (?: $ | $assignment))/gx;

for ( my $i = 0; $i < scalar( @array ); $i++ )
{
  print $i.": ".$array[$i]."\n";
}

输出:

0: var1=100
1: var2=90
2: var5=hello
3: var3="a, b, c"
4: var7=test
5: var3=hello

1
感谢您的贡献。我尝试了您的解决方案,它对我也有效 - 谢谢!+1. 还要感谢您建议的系统化正则表达式构建方法/技巧:“钉和拉:先锚定您知道会出现的特征(钉),然后抓取中间的内容(拉)。”我会在有更多时间时深入阅读您的答案并提供反馈。 - therobyouknow
@Rob 我很高兴它有帮助。享受吧! - Greg Bacon
+1 这是你解决这个问题的非常好的解释。 - dawg

8
我并不是在说这就是你应该做的事情,而是你所尝试的是编写一个语法规则。虽然对于语法规则来说,你的例子非常简单,但Damian Conway的模块Regexp::Grammars非常擅长处理这种情况。如果你需要扩展这个规则,你会发现这会让你的生活变得更加轻松。我在这里经常使用它 - 它有点像perl6。
use Regexp::Grammars;
use Data::Dumper;
use strict;
use warnings;

my $parser = qr{
    <[pair]>+
    <rule: pair>     <key>=(?:"<list>"|<value=literal>)
    <token: key>     var\d+
    <rule: list>     <[MATCH=literal]> ** (,)
    <token: literal> \S+

}xms;

q[var1=100 var2=90 var5=hello var3="a, b, c" var7=test var3=hello] =~ $parser;
die Dumper {%/};

输出:

$VAR1 = {
          '' => 'var1=100 var2=90 var5=hello var3="a, b, c" var7=test var3=hello',
          'pair' => [
                      {
                        '' => 'var1=100',
                        'value' => '100',
                        'key' => 'var1'
                      },
                      {
                        '' => 'var2=90',
                        'value' => '90',
                        'key' => 'var2'
                      },
                      {
                        '' => 'var5=hello',
                        'value' => 'hello',
                        'key' => 'var5'
                      },
                      {
                        '' => 'var3="a, b, c"',
                        'key' => 'var3',
                        'list' => [
                                    'a',
                                    'b',
                                    'c'
                                  ]
                      },
                      {
                        '' => 'var7=test',
                        'value' => 'test',
                        'key' => 'var7'
                      },
                      {
                        '' => 'var3=hello',
                        'value' => 'hello',
                        'key' => 'var3'
                      }
                    ]

2
+1 因为我喜欢语法概念的想法(在计算机科学中有一定的研究),尽管我还没有尝试过这个答案。我喜欢语法概念,因为这种方法可以应用于解决更复杂的问题,特别是在从遗留的过时语言中解析代码/数据,以进行迁移到新语言或数据驱动的系统/数据库 - 这实际上是我的原始问题的原因(尽管当时我没有提到它)。 - therobyouknow
1
我欢迎你来查看这个模块。正则表达式经常与语法混淆 - 如果你要用正则表达式编写语法(这不是一个坏主意),那么这个模块确实非常准确。请查看我在psql shell中解析COPY命令的应用程序 - Evan Carroll

5
也许有点过头了,但这是我研究 http://p3rl.org/Parse::RecDescent 的借口。怎么样,做一个解析器?
#!/usr/bin/perl

use strict;
use warnings;

use Parse::RecDescent;

use Regexp::Common;

my $grammar = <<'_EOGRAMMAR_'
INTEGER: /[-+]?\d+/
STRING: /\S+/
QSTRING: /$Regexp::Common::RE{quoted}/

VARIABLE: /var\d+/
VALUE: ( QSTRING | STRING | INTEGER )

assignment: VARIABLE "=" VALUE /[\s]*/ { print "$item{VARIABLE} => $item{VALUE}\n"; }

startrule: assignment(s)
_EOGRAMMAR_
;

$Parse::RecDescent::skip = '';
my $parser = Parse::RecDescent->new($grammar);

my $code = q{var1=100 var2=90 var5=hello var3="a, b, c" var7=test var8=" haha \" heh " var3=hello};
$parser->startrule($code);

产生:
var1 => 100
var2 => 90
var5 => hello
var3 => "a, b, c"
var7 => test
var8 => " haha \" heh "
var3 => hello

PS. 注意双倍的var3,如果你想让后一个赋值覆盖第一个,可以使用哈希表来存储这些值,然后稍后再使用。

PPS. 我最初的想法是根据“=”分离字符串,但如果一个字符串包含“=”,那么这种方法将失败,而且正则表达式几乎总是用于解析的不好工具,所以我尝试了一下并发现其可行。

编辑:支持转义引号在引用字符串内部。


谢谢您的回答。不过,我需要在我的特定系统上安装Parse模块才能尝试它。因此,我更倾向于一个不依赖此模块的解决方案。 - therobyouknow

3

最近我需要解析x509证书的“Subject”行。它们的格式与您提供的类似:

echo 'Subject: C=HU, L=Budapest, O=Microsec Ltd., CN=Microsec e-Szigno Root CA 2009/emailAddress=info@e-szigno.hu' | \
  perl -wne 'my @a = m/(\w+\=.+?)(?=(?:, \w+\=|$))/g; print "$_\n" foreach @a;'

C=HU
L=Budapest
O=Microsec Ltd.
CN=Microsec e-Szigno Root CA 2009/emailAddress=info@e-szigno.hu

正则表达式的简要说明:

(\w+\=.+?) - 以非贪婪模式捕获紧跟着'='的单词及其后面的所有字符
(?=(?:, \w+\=|$)) - 需要满足后面要么是另一个 ,KEY=val ,要么是行末。

该正则表达式中的有趣部分是:

  • .+? - 非贪婪模式
  • (?:pattern) - 非捕获模式
  • (?=pattern) 零宽度正向先行断言

2
#!/usr/bin/perl

use strict; use warnings;

use Text::ParseWords;
use YAML;

my $string =
    "var1=100 var2=90 var5=hello var3=\"a, b, c\" var7=test var3=hello";

my @parts = shellwords $string;
print Dump \@parts;

@parts = map { { split /=/ } } @parts;

print Dump \@parts;

1
我认为最好使用Text::ParseWords而不是Text::Shellwords来完成这个任务。Text::ParseWords具有类似的功能,但它是Perl核心的一部分。 - dawg
1
@drewk 感谢提醒。我总是混淆这两个。我会更新示例,使用Text::ParseWords - Sinan Ünür

对我来说运行良好。请查看下面的评论输出。这取决于一个模块 - 我很幸运我的机器上有它,但对于一些Perl模块,在每个分发/平台上都不能保证有它。以下是输出: - var1=100 - var2=90 - var5=hello - 'var3=a, b, c' - var7=test - var3=hello

  • var1: 100
  • var2: 90
  • var5: hello
  • var3: 'a, b, c'
  • var7: test
  • var3: hello
- therobyouknow
1
@Rob:我认为Text::ParseWords从5.00版本开始就是核心分发的一部分了。 shellwords功能非常有用,在5.00之前,许多人即使面临安全风险也使用shell eval来实现它。 自从5.00以后就不再需要这样做了。 - dawg
1
@Rob:请问哪一个更易于维护:复杂的模式,自定义解析器还是核心模块依赖? - Sinan Ünür
显示剩余4条评论

2
您要求提供一个正则表达式解决方案或其他代码。这里提供了一个(主要)非正则表达式解决方案,仅使用核心模块。唯一的正则表达式是\s+,用于确定分隔符;在这种情况下为一个或多个空格。
use strict; use warnings;
use Text::ParseWords;
my $string="var1=100 var2=90 var5=hello var3=\"a, b, c\" var7=test var3=hello";  

my @array = quotewords('\s+', 0, $string);

for ( my $i = 0; $i < scalar( @array ); $i++ )
{
    print $i.": ".$array[$i]."\n";
}

您可以在此处执行代码HERE
输出结果为:
0: var1=100
1: var2=90
2: var5=hello
3: var3=a, b, c
4: var7=test
5: var3=hello

如果你真的想要一个正则表达式的解决方案,Alan Moore的评论链接到他在IDEone上的代码是绝佳的!

2
这个将为您提供双引号中常见的转义,例如 var3="a, \"b, c"。
@a = /(\w+=(?:\w+|"(?:[^\\"]*(?:\\.[^\\"]*)*)*"))/g;

实际应用中:

echo 'var1=100 var2=90 var42="foo\"bar\\" var5=hello var3="a, b, c" var7=test var3=hello' |
perl -nle '@a = /(\w+=(?:\w+|"(?:[^\\"]*(?:\\.[^\\"]*)*)*"))/g; $,=","; print @a'
var1=100,var2=90,var42="foo\"bar\\",var5=hello,var3="a, b, c",var7=test,var3=hello

0

使用正则表达式可能可以实现这一点,但它非常脆弱。

my $string = "var1=100 var2=90 var5=hello var3=\"a, b, c\" var7=test var3=hello";

my $regexp = qr/( (?:\w+=[\w\,]+) | (?:\w+=\"[^\"]*\") )/x;
my @matches = $string =~ /$regexp/g;

当我运行它时,可能需要添加一些缺失的内容或更正一些内容,因为我收到了错误消息:http://ideone.com/4bR1b,而且在我的机器上也是如此。 - therobyouknow
在./regex_solution.pl第8行附近发现裸字,应该是操作符。语法错误,在./regex_solution.pl第8行附近,qr/((?:\w+=[\w,]+)|(?:\w+="[^"]*"))/xg。由于编译错误,执行./regex_solution.pl中止。 - therobyouknow

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