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

6

我试图解决一个问题,即想要从文件中显示给定文本,省略特殊字符并将多行输入修改为单格式输出,仅使用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>

所需输出为:
Holland, T. "Aeronautics Engineer" 200 06/09/1969 Flight from DC to VA.

如您所见,我需要单行输出; 名字应为第一个字母,而专业应该在输出中放在引号中;日期格式应从-更改为/
以下是我的代码:
#!/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

请建议我接下来该做什么以及如何格式化给定文件的输出。 我对正则表达式和Perl都不熟悉,所以需要帮助。

1
我投票关闭此问题,因为提问者希望我们编写一个XML解析器,而SO不是一个代码编写服务。 - ikegami
我会很感激在正则表达式或Perl语言方面的提示;由于我只是参加基础的Bash/Regex/Perl课程,所以我不知道什么是XML解析器。 - ash_k123
@ikegami 我已经对我目前所拥有的进行了更改,只需要知道如何去掉这些<abc>和</abc>字符,并将输出显示在一行中。 - ash_k123
1
“我不知道什么是XML解析器”,这里有一些松散的笔记,受到评论的启发。XML是用于您展示的文件的“标记语言”。它是一套规则和“标记”(等等),旨在帮助组织文档中的某些数据,以便各种工具可以更轻松地使用信息。从这样的(“XML”)文件中提取信息片段的程序被称为“解析”文件。因此,在某种意义上,您在这里要求一个“XML解析器”。希望这可以帮助您。 - zdim
请参阅“解析”(https://en.wikipedia.org/wiki/Parsing) - zdim
3个回答

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";
}

1
  1. 你没有提到只使用Perl和正则表达式。
  2. 我的答案只使用了Perl。
  3. 你没有提到是否编写基于正则表达式的解析器或不使用模块。
  4. 我们不会为你编写XML解析器(使用正则表达式或其他方式)。那是一个巨大的工作。
- ikegami
如果你“对XML一无所知”,那么你怎么能期望解析XML呢? - ikegami
“关于‘我的导师告诉我使用regex101.com来解决这个问题’,这没有任何意义。这不是仅通过正则表达式就能完成的任务。” - ikegami
1
你没有仔细听。 我从未说过要给XML。我是说你XML。 你试图从XML(<start>...</start>)中提取文本(例如,“航空工程师”)。 - ikegami
1
我无法再为您提供更多帮助。我的答案已经添加了详细信息。 - ikegami
显示剩余12条评论

6
这是您的作业问题,正如您在评论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.-]+ >  # start tag
    .*                             #   the interesting text, greedily
    < \/ ... >                     # end tag
/x;

但是,我不希望*太贪婪。我不想它匹配结束标记,因此我可以在.*后面添加非贪婪修饰符?

m/ 
    < [a-zA-Z_] [a-zA-Z_0-9.-]+ >  # start tag
    .*?                            #   the interesting text, non-greedily
    < \/ ... >                     # end tag
/x;

现在我需要填写结束标记的名称部分。它必须与开始标记的名称相同。通过将开始名称括在(...)中,我捕获匹配的字符串的那一部分。这进入捕获缓冲器$1。然后我可以在模式中重复使用该完全匹配项,称为“反向引用”(我猜这是你问题的关键点)。反向引用以\开头,并使用您要使用的捕获缓冲器的编号。因此,\1使用$1中匹配的确切文本;不是相同的模式,而是实际匹配的文本:
m/ 
    <                              # start tag
      ([a-zA-Z_] [a-zA-Z_0-9.-]+)  #  $1
    >  
    .*?                            #   the interesting text, non-greedily
    < \/ \1 >                      # end tag
/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/
        <                              # 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!

这让我接近了。我错过了元素之间的一些空白(请注意,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)解析器,所以它不关心行数。

#!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";
find使用CSS选择器来确定它要处理的内容。选择器flight *表示flight里的所有子节点(所以,任何子标签都无论它的名称是什么)。mapfind产生的树中的每个部分调用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"

现在你需要为你想要处理的其他所有东西编写子例程,以此来完成上述任务。


1
关于“现在,有一个规则我忽略了”,你忽略并没有提到的还有大约10个,比如实体、空格折叠、注释等等。此外,任务要求处理属性(通过说明它们的值不包含<>来表示),但你甚至都没有尝试。因此,你不仅没有处理XML,也没有做任务要求的事情。 - ikegami
请注意,使用Mojo::DOM处理XML时会有些棘手,因为它期望解码文本,这需要解析XML才能完成...因此,虽然OP的XML在文件中找到了,但您根本不知道如何处理这种情况。另外,Mojo::DOM->new应该是Mojo::DOM->new->xml(1) - ikegami
我并不打算处理XML,也不会帮他们完成任务。有足够的资源让他们自己完成任务。 - brian d foy

1

为了给你一些提示:

你的代码是“好的”,但是

my $text = <>;

你的while循环中有错误。你已经在$row中拥有了这行,所以只需使用$row即可。

而且你的$row也包含了一个换行符,所以在打印之前可能需要将其删除。

chomp($row);

所以,总结一下:

chomp($row);
$row =~ s/<[^>]*>//g;
print $row . " ";

也许你正在寻找while循环中的代码。如果想要额外加分,开始考虑如何删除开头/结尾的不必要空格。


谢谢提示,我已经弄清楚如何去除空格了! - ash_k123

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