我正在尝试解决一个问题,即希望从文件中显示给定文本,省略特殊字符并将多行输入修改为仅使用 Perl/Regex 语言(没有其他语言,如 XML,等等。)。这是我的flight.txt文件中的给定文本:
<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>
Run Code Online (Sandbox Code Playgroud)
所需的输出是:
Holland, T. "Aeronautics Engineer" 200 06/09/1969 Flight from DC to VA.
Run Code Online (Sandbox Code Playgroud)
如您所见,我需要一行输出;并且名字应该是第一个首字母,而输出也主要应该在“”中,而输出和日期格式应该从 更改-为/。
到目前为止,这是我在代码中的内容:
#!/bin/perl
use strict;
use warnings;
my $filename = "flights.txt"
open(my $input, '<:encoding(UTF-8)', $filename)
or die "Could not open file '$filename' $!";
while (my $row = <$input>){
my $text = <>;
$text =~ s/<[^>]*>//g;
print $text;
}
close $input
Run Code Online (Sandbox Code Playgroud)
请建议我下一步做什么以及如何格式化给定文件的输出。我是 Regex 和 Perl 的新手,所以需要帮助。
预告
根据发布此答案后发表的评论,这是一项作业,其中老师鼓励 OP 对 XML 做出许多错误的假设。他们正在教他们做人们永远不应该做的事情。如果老师要定义一种格式,那就没问题了;它不会是 XML,而只是受 XML 启发的东西。但他们没有那样做。他们明确声明它是 XML。我无法进一步帮助 OP,因为
接下来是问题的答案(而不是 OP 作业的解决方案)。
回答
您正在尝试解析 XML。您可以使用现有的 XML 解析器,而无需花费大量精力编写自己的解析器。我个人使用XML::LibXML。
use XML::LibXML qw( );
my $doc = XML::LibXML->new->parse_file("flight.txt");
for my $flight_node ($doc->findnodes("/start/flight")) {
my $pilot = $flight_node->findvalue("pilot");
my $major = $flight_node->findvalue("major");
my $price = $flight_node->findvalue("price");
my $date = $flight_node->findvalue("date");
my $details = $flight_node->findvalue("details");
say "$pilot \"$major\" $price $date $details";
}
Run Code Online (Sandbox Code Playgroud)
这是您的作业问题,正如您在对池上回答的评论中所指出的:
创建 Perl 脚本“code.pl”,打印包含“flights.txt”中的开始和结束 XML 标题标记的行。有效标签是飞行员、专业、公司、价格、日期和详细信息,无论大小写。标签也可以在其中包含任意内容。您可以假设“<”或“>”字符不会出现在属性的值部分内
让我们忘记您的输入是 XML,因为池上已经解释了所有原因。整个事情是一个人为的例子,让你练习一些特定的正则表达式功能。我将经历一个解决这个问题的过程,但稍后也会揭示我认为教师的期望。
首先,您一次只需要考虑一行,因此您不必关心开头和结尾在不同行上的节点,例如<start>and </start>、 or<flight>和</flight>。您想查找以下行:
<node>...</node>
Run Code Online (Sandbox Code Playgroud)
模式是您在行首附近匹配了一些字符串,并且该匹配必须显示在该行的后面。我认为您的预期任务是练习反向引用。编写好的练习很难,人们会依赖于熟悉的东西,比如 XML。我的学习 Perl 练习在这方面考虑得更周到。
您的基本程序需要与第一次尝试类似。读取输入行,跳过与您的模式不匹配的行,然后输出其余的行。每当您...在此答案中看到时,这只是我需要填写的内容,而不是 Perl 语法(忽略yada 运算符,它不能出现在正则表达式中):
use strict;
use warnings;
while( <> ) {
next unless m/ ... /;
print;
}
Run Code Online (Sandbox Code Playgroud)
我将主要忽略该程序结构并专注于匹配运算符m//. 在我逐步完成时更新模式。
那么,诀窍在于模式中的内容。您知道您必须匹配一些看起来像 XML 开放标记的东西(再次忽略这是 XML,因为它不是一个很好的输入示例)。这以中间的一些东西开始<和结束>。此模式使用/x标志使空白变得无关紧要。我可以展开模式,这样我就可以更容易地理解它:
m/ < ... > /x;
Run Code Online (Sandbox Code Playgroud)
那么尖括号里面可以放什么?在我假装不是 XML 的 inputL 中,角度内的内容遵循这些规则,如果这是 XML,您可以在 XML 标准中阅读这些规则:
xml无论如何都不能开始让我们暂时忽略最后一个,因为我认为这不是您需要做的简单练习的一部分。而且规则实际上稍微复杂一些。
区分大小写很容易。我们不会/i在匹配运算符上使用标志,所以我们免费获得它。
以字母或下划线开头。这很容易。因为我假装这不是 XML,所以我不会支持当前 XML 允许的所有 Unicode 脚本。我将把它限制为 ASCII,并使用一个字符类来表示我允许的所有字母>:
m/ < [a-zA-Z_] ... > /x;
Run Code Online (Sandbox Code Playgroud)
之后,我可以有字母和下划线,但现在也有连字符、数字和句点。顺便说一句,许多这样的东西都有一组字符作为“标识符”的开头(ID_Start)和一个更广泛的字符集(ID_Continue)。Perl 的变量名也有类似的规则。
我使用第二个字符类进行延续。这里有一个小问题,因为您需要一个文字连字符,但这也会在字符类中形成一个范围。也就是说,它形成一个范围,除非它在最后。该.字符类是文字.:
m/ < [a-zA-Z_] [a-zA-Z_0-9.-]+ > /x;
Run Code Online (Sandbox Code Playgroud)
有了这种模式,你得到的比你想要的要多得多。输出是具有开始标记的每一行。请注意,它不匹配,<flight number="12345">因为此模式不处理属性,这很好,因为我假装这不是 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>
Run Code Online (Sandbox Code Playgroud)
结束标签与开始标签同名。在我们的输入中,每行有一个开始标记和一个结束标记,因为我一次只看一行,所以我可以忽略 XML 解析器必须关心的许多事情。现在我将我的模式分散在几行上,因为/x允许我这样做,\x还允许我添加注释,以便我记住模式的每个部分的作用。该/到底标签也是匹配运算符,让我逃脱为\/:
m/
< [a-zA-Z_] [a-zA-Z_0-9.-]+ > # start tag
... # the interesting text
< \/ ... > # end tag
/x;
Run Code Online (Sandbox Code Playgroud)
我需要填写...部分。“有趣的文字”部分很简单。我会匹配任何东西。在.*贪婪地匹配零或多个非换行符:
m/
< [a-zA-Z_] [a-zA-Z_0-9.-]+ > # start tag
.* # the interesting text, greedily
< \/ ... > # end tag
/x;
Run Code Online (Sandbox Code Playgroud)
但是,我真的不想*贪婪。我不希望它与结束标记匹配,因此我可以将非贪婪修饰符添加?到.*:
m/
< [a-zA-Z_] [a-zA-Z_0-9.-]+ > # start tag
.*? # the interesting text, non-greedily
< \/ ... > # end tag
/x;
Run Code Online (Sandbox Code Playgroud)
现在我需要填写结束标签的名称部分。它必须与起始名称相同。通过将 中的起始名称括起来(...),我捕获了匹配的字符串部分。那进入捕获缓冲区$1。然后,我可以使用“反向引用”(我猜是您的问题的重点)在模式中重新使用该完全匹配。反向引用以 a 开头,\并使用您要使用的捕获缓冲区的编号。因此,\1使用匹配的确切文本$1;不相同的模式,但实际的文本匹配:
m/
< # start tag
([a-zA-Z_] [a-zA-Z_0-9.-]+) # $1
>
.*? # the interesting text, non-greedily
< \/ \1 > # end tag
/x;
Run Code Online (Sandbox Code Playgroud)
现在输出排除,<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>
Run Code Online (Sandbox Code Playgroud)
如果您修改数据以更改</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;
Run Code Online (Sandbox Code Playgroud)
但是现在您想打印有趣的测试,而不是整行,所以我将打印$2捕获缓冲区而不是整行。请记住,这些缓冲区仅在成功匹配后才有效,但我跳过了不匹配的行,所以我很好:
use strict;
use warnings;
while( <DATA> ) {
next unless m/
< # start tag
([a-zA-Z_] [a-zA-Z_0-9.-]+) # $1
>
(.*?) # $2, the interesting text, non-greedily
< \/ \1 > # end tag
/x;
print $2;
}
print "\n"; # end all the output!
Run Code Online (Sandbox Code Playgroud)
这让我很接近。我在元素之间缺少一些空格(并注意之前有一个前导空格Holland):
Holland, TomAeronautics EngineerBoeing20006-09-1969Flight from DC to VA.
Run Code Online (Sandbox Code Playgroud)
我可以在每个打印的末尾添加一个空格:
print $2, ' ';
Run Code Online (Sandbox Code Playgroud)
现在你有你的输出:
Holland, Tom Aeronautics Engineer Boeing 200 06-09-1969 Flight from DC to VA.
Run Code Online (Sandbox Code Playgroud)
我猜你会看到答案要简单得多。如果您忽略有关名称的所有规则,而只处理问题的准确输入,则可能会逃脱:
m/ <(.*?)> (.*?) < \/ \1 > /x
Run Code Online (Sandbox Code Playgroud)
作为一个简单的练习反向引用,这很好。但是,您最终会在像这样处理真正的 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, ' ';
}
Run Code Online (Sandbox Code Playgroud)
这样,while循环的机制就不同于细节。模式的复杂性不会影响我理解循环的能力。
现在,这样做之后,我会变得更复杂。到目前为止,我使用了编号捕获和反向引用,但如果我添加更多捕获,我可能会搞砸。如果在开始标记之前还有另一个捕获,则开始标记捕获不再是$1,这意味着\1现在指的是错误的东西。代替数字,我可以用(?<LABEL>...)Perl 从 Python 中窃取的特性给他们自己的标签。对该标签的反向引用是\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;
Run Code Online (Sandbox Code Playgroud)
我什至可以标记“有趣的文本”部分:
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;
Run Code Online (Sandbox Code Playgroud)
程序的其余部分仍然有效,因为这些标签是编号捕获变量的别名。但是,我不想依赖它(因此,标签)。散列%+具有标记捕获中的值,并且标签是键。有趣的文字在$+<text>:
while( <DATA> ) {
next unless m/$pattern/;
print $+{'text'}, ' ';
}
Run Code Online (Sandbox Code Playgroud)
现在,有一条我忽略了的规则。xml任何情况下都不能以标签名称开头。这与我将在此处忽略的 XML 功能相关。我将更改我的输入以包含一个xmlmeal节点:
<start>
<flight number="12345">
<pilot> Holland, Tom</pilot>
<xmlmeal> chicken</xmlmeal>
</flight>
</start>
Run Code Online (Sandbox Code Playgroud)
我匹配那个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;
Run Code Online (Sandbox Code Playgroud)
没关系,它不会在输出中显示“chicken”。但是,如果输入标签名称是XMLmeal呢?我只排除了小写版本。我需要排除更多:
<start>
<flight number="12345">
<pilot> Holland, Tom</pilot>
<XMLmeal>chicken</XMLmeal>
<xmldrink>diet coke</xmldrink>
<Xmlsnack>almonds</Xmlsnack>
</flight>
</start>
Run Code Online (Sandbox Code Playgroud)
我可以变得更狂热。我不使用/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;
Run Code Online (Sandbox Code Playgroud)
但是,在分组括号内,(?i)仅在该组结束之前有效。我可以限制我的模式的哪一部分忽略大小写。(?: ... )没有捕获的组(所以不打扰什么$1或$2捕获):
(?: (?i) (?!xml) )
Run Code Online (Sandbox Code Playgroud)
现在我的模式排除了我添加的那三个标签:
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;
Run Code Online (Sandbox Code Playgroud)
到目前为止,我所介绍的内容都没有处理标签中的属性,无论如何您都想忽略这些属性。您应该能够自己将这些添加到正则表达式中。但是,我将转向其他方式来处理 XML 之类的东西。
这是一个理解 XML 并可以提取内容的 Mojolicious 程序。由于它是一个真正的文档对象模型 (DOM) 解析器,因此它不关心行。
#!perl
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";
Run Code Online (Sandbox Code Playgroud)
在find使用CSS选择器来决定它想做的过程。选择器flight *是飞行中的所有子节点(因此,任何子标签,无论其名称如何)。该map调用text在该树的每个部分的方法find产生,并且each每一个输出结果。这很简单,因为有人已经完成了所有艰苦的工作。
但是,Mojo::DOM并不适合所有情况。它想立即知道整个树,对于非常大的文档,这对内存来说是一种负担。有可以处理的“流”解析器。
您在原始问题中提出的问题与您在评论中发布的作业不同。您想根据文本来自哪个标签来转换文本。这是一个不同类型的问题,因为
XML::Twig可用于以不同方式处理不同的节点类型。它的另一个优点是它不需要一次在内存中保存整个 XML 树。
这是一个示例,该示例对飞行员和主要部分使用了两个不同的处理程序。当 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);
Run Code Online (Sandbox Code Playgroud)
这输出:
Holland, T. "Aeronautics Engineer"
Run Code Online (Sandbox Code Playgroud)
现在,您将使用子例程完成要处理的所有其他事情。