这是您的作业问题,正如您在评论ikegami的答案中指出的那样:
创建Perl脚本“code.pl”,从“flights.txt”打印包含开头和结尾XML标记的行。无论大小写,有效标记为pilot、major、company、price、date和details。标记内也可能有任意内容。可以假设“<”或“>”字符不会出现在属性值部分中。
让我们忘记输入是XML,因为ikegami已经解释了所有的原因。整个事情都是一个虚构的例子,目的是让你练习某些特定的正则表达式功能。我将通过解决这个问题的过程,并透露一下我认为教练员期望什么来解决这个问题。
首先,您只需要考虑一行,所以您不关心开头和结尾在不同行的节点,例如和,或和。您要找到以下行:
<node>...</node>
模式是在行的开头附近匹配某些字符串,并且该匹配项必须在行的后面出现。我认为您打算练习反向引用。编写好的练习很难,人们会退而求其次,选择熟悉的东西,例如XML。我的《
Learning Perl Exercises》对此更加深思熟虑。
您的基本程序需要首先尝试这样做。读取输入行,跳过不符合模式的行并输出其余内容。每当您在此答案中看到
...
时,那只是我需要填充的内容,不是Perl语法(忽略
yada运算符,它不能出现在正则表达式中)。
use strict;
use warnings;
while( <> ) {
next unless m/ ... /;
print;
}
我将大多数忽略程序结构,重点关注匹配运算符m//
。在这个过程中,更新模式。
关键是要确定模式中需要匹配的内容。你需要匹配像XML开放标签一样的东西(再次忽略它是XML,因为它不是一个好的输入示例)。它以<
开始,并以>
结束,中间带有一些内容。此模式使用/x
标志使空格无关紧要。我可以展开模式以便更容易理解:
m/ < ... > /x;
那么尖括号内可以放什么?在输入L中,我假装它不是XML,括号里的内容遵循这些规则,如果这是XML,你可以在XML标准中了解这些规则:
- 区分大小写
- 以字母或下划线开头
- 可以包含字母、数字、连字符、下划线和句点
- 不能以任何情况下的
xml
开头
让我们暂时忽略最后一个规则,因为我认为它不是你需要完成的简单练习的一部分。实际上,规则还稍微有些复杂
区分大小写很容易。我们不会在匹配操作符上使用/i
标志,所以我们可以免费得到它。
以字母或下划线开头。这很容易。由于我假装这不是XML,我不会支持当前XML允许的所有Unicode脚本。我将限制它只用ASCII,并使用字符类来表示我会允许在>
后面出现的所有字母:
m/ < [a-zA-Z_] ... > /x;
接着,我可以使用字母和下划线,但现在也可以使用连字符、数字和句点。顺带一提,许多标识符都有一组用于“标识符”开头的字符(ID_Start
),以及一组更广泛的用于其余部分的字符(ID_Continue
)。Perl 也有类似变量名的规则。
我使用第二个字符类来进行续写。这里有一个小问题,因为你想要一个连字符,但它同时也形成了字符类中的一组范围。也就是说,只有在字符类的末尾时,它才不会形成范围。而字符类中的 .
是字面意思的点号:
m/ < [a-zA-Z_] [a-zA-Z_0-9.-]+ > /x;
使用这种模式,你将获得更多的内容。输出结果是每一行都有一个起始标记。请注意,它不匹配,因为这个模式不能处理属性,但这没关系,因为我假装这不是XML。
<start>
<pilot> Holland, Tom</pilot>
<major>Aeronautics Engineer</major>
<company>Boeing</company>
<price>200</price>
<date>06-09-1969</date>
<details>Flight from DC to VA.</details>
结束标签与开始标签名称相同。在我们的输入中,每行有一个开始标签和一个结束标签,由于我一次只查看一行,因此可以忽略许多XML解析器要关心的问题。现在我将我的模式分散到几行上,因为/x
允许我这样做,而\x
也允许我添加注释,以便我记住模式的每个部分所做的事情。结束标记中的斜杠 /
同样是匹配操作符的分隔符,因此我将其转义为 \/
:
m/
< [a-zA-Z_] [a-zA-Z_0-9.-]+ > # start tag
... # the interesting text
< \/ ... > # end tag
/x
我需要填写...
部分。 "interesting text" 部分很容易,我将匹配任何内容,而 .*
则会贪婪地匹配零个或多个非换行字符:
m/
< [a-zA-Z_] [a-zA-Z_0-9.-]+ >
.*
< \/ ... >
/x;
但是,我不希望*
太贪婪。我不想它匹配结束标记,因此我可以在.*
后面添加非贪婪修饰符?
:
m/
< [a-zA-Z_] [a-zA-Z_0-9.-]+ >
.*?
< \/ ... >
/x;
现在我需要填写结束标记的名称部分。它必须与开始标记的名称相同。通过将开始名称括在
(...)
中,我捕获匹配的字符串的那一部分。这进入捕获缓冲器
$1
。然后我可以在模式中重复使用该完全匹配项,称为“反向引用”(我猜这是你问题的关键点)。反向引用以
\
开头,并使用您要使用的捕获缓冲器的编号。因此,
\1
使用
$1
中匹配的确切文本;不是相同的模式,而是实际匹配的文本:
m/
<
([a-zA-Z_] [a-zA-Z_0-9.-]+)
>
.*?
< \/ \1 >
/x;
现在的输出中不包括
<start>
标签,因为它没有结束标签。
<pilot> Holland, Tom</pilot>
<major>Aeronautics Engineer</major>
<company>Boeing</company>
<price>200</price>
<date>06-09-1969</date>
<details>Flight from DC to VA.</details>
如果你修改了数据并将
</date>
更改为
</data>
,那么该行就不会匹配,因为起始标签和结束标签是不同的。
但是,你真正想要的是中间的文本,所以你需要捕获它。你可以添加另一个捕获缓冲区。作为第二个括号内,这是缓冲区
$2
,不会干扰
$1
或
\1
:
m/
< # start tag
([a-zA-Z_] [a-zA-Z_0-9.-]+) # $1
>
( .*? ) # $2, the interesting text, non-greedily
< \/ \1 > # end tag
/x
现在你想要打印有趣的测试内容,而不是整行文本,所以我将打印 $2
捕获缓冲区而不是整行文本。请记住,这些缓冲区仅在成功匹配后才有效,但我已跳过了不匹配的行,所以一切都好:
use strict;
use warnings;
while( <DATA> ) {
next unless m/
<
([a-zA-Z_] [a-zA-Z_0-9.-]+)
>
(.*?)
< \/ \1 >
/x;
print $2;
}
print "\n";
这让我接近了。我错过了元素之间的一些空白(请注意,
Holland
前面有一个前导空格):
Holland, TomAeronautics EngineerBoeing20006-09-1969Flight from DC to VA.
我可以在每个打印语句结尾添加一个空格:
print $2, ' ';
现在您已经获得了输出:
Holland, Tom Aeronautics Engineer Boeing 200 06-09-1969 Flight from DC to VA.
可能的答案是什么
我猜测你将看到的答案要简单得多。如果忽略所有关于名称的规则,只处理问题中给出的精确输入,那么你可以使用以下方法:
m/ <(.*?)> (.*?) < \/ \1 > /x
作为一个练习回溯引用的练习,这样做是可以的。但是,你最终会因为这样处理真正的XML而出现问题。请注意,$1可以捕获flight number="1234"中的所有内容,因为它没有排除空格或其他不允许的字符。
让我们深入一点
我展示的模式非常复杂,特别是对于正在学习的人来说。我可以预编译这个模式并将其保存在标量中,然后在匹配运算符中使用该标量:
use strict;
use warnings;
my $pattern = qr/
< # start tag
([a-zA-Z_] [a-zA-Z_0-9.-]+) # $1
>
( .*? ) # the interesting text, non-greedily
< \/ \1 > # end tag
/x;
while( <DATA> ) {
next unless m/$pattern/;
print $2, ' ';
}
这样,while
循环的机制就与具体细节分开了。即使模式复杂,也不会影响我理解循环的能力。
现在,我将变得更加复杂。到目前为止,我使用了编号捕获和反向引用,但如果我添加了更多捕获,可能会搞砸。如果开始标签之前有另一个捕获,那么开始标签捕获就不再是$1
了,这意味着\1
现在指的是错误的东西。我可以使用Perl从Python中窃取的(?<LABEL>...)
功能给它们自己的标签。对该标签的反向引用是\k<LABEL>
:
my $pattern = qr/
< # start tag
(?<tag> # labeled capture
[a-zA-Z_] [a-zA-Z_0-9.-]+
)
>
( .*? ) # the interesting text, non-greedily
< \/ \k<tag> > # end tag
/x;
我可以甚至将“有趣的文字”部分标记出来:
my $pattern = qr/
< # start tag
(?<tag>
[a-zA-Z_] [a-zA-Z_0-9.-]+
)
>
(?<text> .*? ) # the interesting text, non-greedily
< \/ \k<tag> > # end tag
/x;
程序的其余部分仍然有效,因为这些标签是指向编号捕获变量的别名。但是,我不想依赖它(因此使用标签)。哈希表
%+
具有标记捕获组的值,而标签则是键。有趣的文本在
$+<text>
中。
while( <DATA> ) {
next unless m/$pattern/;
print $+{'text'}, ' ';
}
我忽略的规则
现在,有一个规则我忽略了。任何情况下标签名称不能以xml
开头。这与XML功能有关,在此不做解释。 我将更改我的输入以包含xmlmeal
节点:
<start>
<flight number="12345">
<pilot> Holland, Tom</pilot>
<xmlmeal> chicken</xmlmeal>
</flight>
</start>
我匹配了那个xmlmeal
节点,因为我没有做任何事情遵循规则。我可以添加一个负向先行断言(?!...)
来排除它。作为一种断言(\b
和\A
是其他的断言),先行断言不会消耗文本;它只是匹配一个条件。我使用(?!xml)
表示“无论我现在在哪里,xml
都不能接下来”:
my $pattern = qr/
< # start tag
(?<tag>
(?!xml)
[a-zA-Z_] [a-zA-Z_0-9.-]+
)
>
(?<text> .*? ) # the interesting text, non-greedily
< \/ \k<tag> > # end tag
/x;
这很好,输出中不会显示 "chicken"。但是,如果输入标签名为XMLmeal
怎么办?我只排除了小写版本。我需要排除更多:
<start>
<flight number="12345">
<pilot> Holland, Tom</pilot>
<XMLmeal>chicken</XMLmeal>
<xmldrink>diet coke</xmldrink>
<Xmlsnack>almonds</Xmlsnack>
</flight>
</start>
我可以更高级一些。我没有使用/i
标记进行大小写不敏感,因为起始和结束标签需要完全匹配。但是,我可以在模式的一部分开启大小写不敏感,方法是使用(?i)
,并且该标记之后的所有内容都忽略大小写:
my $pattern = qr/
< # start tag
(?<tag>
(?i) # ignore case starting here
(?!xml)
[a-zA-Z_] [a-zA-Z_0-9.-]+
)
>
(?<text> .*? ) # the interesting text, non-greedily
< \/ \k<tag> > # end tag
/x;
然而,在分组括号内,(?i)
仅在该组结束之前有效。我可以限制模式中哪一部分忽略大小写。 (?: ... )
组不捕获(因此不会干扰 $1
或 $2
捕获的内容):
(?: (?i) (?!xml) )
现在我的模式排除了我添加的那三个标签:
my $pattern = qr/
< # start tag
(?<tag>
(?: (?i) (?!xml) ) # not XmL in any case
[a-zA-Z_] [a-zA-Z_0-9.-]+
)
>
(?<text> .*? ) # the interesting text, non-greedily
< \/ \k<tag> > # end tag
/x;
一些技巧
到目前为止,我所介绍的内容都没有处理标记中的属性,而你也不需要关注它们。你应该可以自己将其添加到正则表达式中。但是,我会转向其他处理类 XML 事物的方法。
这是一个 Mojolicious 程序,它可以理解 XML 并提取信息。由于它是真正的文档对象模型(DOM)解析器,所以它不关心行数。
use Mojo::DOM;
my $not_xml = <<~'HERE';
<start>
<flight number="12345">
<pilot> Holland, Tom</pilot>
<major>Aeronautics Engineer</major>
<company>Boeing</company>
<price>200</price>
<date>06-09-1969</date>
<details>Flight from DC to VA.</details>
</flight>
</start>
HERE
Mojo::DOM->new( $not_xml )->xml(1)
->find( 'flight *' )
->map( 'text' )
->each( sub { print "$_ " } );
print "\n";
find
使用CSS选择器来确定它要处理的内容。选择器
flight *
表示flight里的所有子节点(所以,任何子标签都无论它的名称是什么)。
map
对
find
产生的树中的每个部分调用
text
方法,并且
each
输出每个结果。这很简单,因为有人已经完成了所有的艰苦工作。
但是,
Mojo::DOM并不是每种情况都适用。它希望一次性知道整棵树,在处理非常大的文档时,这会给内存带来负担。有“流式”解析器可以处理这个问题。
Twiggy
你在原始问题中提出的问题与你在评论中发布的作业不同。你想根据标签来转换文本。这是一个完全不同类型的问题,因为
XML::Twig适用于针对不同类型的节点进行不同的处理。它的额外优势是它不需要一次性将整个XML树存储在内存中。
下面是一个示例,使用两个不同的处理程序处理 pilot 和 major 部分。当Twig运行到这些节点时,它会调用您在
twig_handlers
中引用的适当子例程。我不会在这里解释特定的Perl功能:
use XML::Twig;
my $twig = XML::Twig->new(
twig_handlers => {
pilot => \&pilot,
major => \&major,
},
);
sub pilot {
my( $twig, $e ) = @_;
my $text = $e->text;
$text =~ s/,\s.\K.*/./;
print $text, ' ';
$twig->purge;
}
sub major {
my( $twig, $e ) = @_;
print '"' . $e->text . '"' . ' ';
$twig->purge;
}
my $xml = <<~'HERE';
<start>
<flight number="12345">
<pilot> Holland, Tom</pilot>
<major>Aeronautics Engineer</major>
<company>Boeing</company>
<price>200</price>
<date>06-09-1969</date>
<details>Flight from DC to VA.</details>
</flight>
</start>
HERE
$twig->parse($xml);
这将输出:
Holland, T. "Aeronautics Engineer"
现在你需要为你想要处理的其他所有东西编写子例程,以此来完成上述任务。