如何删除正则表达式中多行中的特殊字符?

ash*_*123 6 regex perl

我正在尝试解决一个问题,即希望从文件中显示给定文本,省略特殊字符并将多行输入修改为仅使用 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 的新手,所以需要帮助。

ike*_*ami 6

预告

根据发布此答案后发表的评论,这是一项作业,其中老师鼓励 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)


bri*_*foy 6

这是您的作业问题,正如您在对池上回答的评论中所指出的:

创建 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)

现在,您将使用子例程完成要处理的所有其他事情。