使用 Perl 驯服文本

使用正则表达式加速基于文本的编码任务。
30 位读者喜欢这篇文章。
Person using a laptop

尽管 Perl 的受欢迎程度已被 Python、Lua 和 Go 等语言所削弱,但它在 Unix 和 Linux 上 30 年来一直是主要的实用语言之一。 如今,它仍然是许多开源系统中重要且强大的组成部分。 如果您不常使用 Perl,那么您可能会惊讶于它在许多任务中的帮助。 如果您在日常工作中处理大量文本,则尤其如此。

如果您需要一种语言来快速轻松地搜索和处理大量文本,那么 Perl 很难被击败。 事实上,这正是 Larry Walls 最初构建该语言的目的。

如果您是 Perl 的新手,您可以阅读这篇 Perl 快速入门,以了解基本知识。

使用正则表达式搜索文本

首先,这是一个简单的正则表达式(有时缩写为“regex”)脚本示例。

假设您有一个名为 names.txt 的文件,其中包含姓名列表

Steve Smith
Jane Murphy
Bobby Jones
Elizabeth Arnold
Michelle Swanson

您想提取所有名为 Elizabeth 的人。 将您要查找的正则表达式(这里是“Elizabeth”)放在正斜杠之间,Perl 将查看特殊 DATA 标记后面的每一行,并且仅打印匹配的行。

use warnings;
use strict;

open my $fh, '<:encoding(UTF-8)', "$names.txt" or
  die "Could not read file\n";

while(<$fh>){
  print if /Elizabeth/;
}

关于此代码的快速说明:正则表达式需要放在行尾。 因此 if /Elizabeth/ print; 将不起作用。 对于新的 Perl 程序员来说,这是一个常见的错误。

使用环视更改选定的单词

有时您可能不想对字符串的每个实例都执行某些操作,而是根据字符串之前或之后的内容进行选择。 例如,也许您想将字符串“Robert”更改为“Bob”,但前提是“Robert”后面跟着“Dylan”。 否则,您不想更改名称。

对于 Perl 来说,这很容易。 您可以使用一行代码直接从终端应用此条件

perl -i.bkp -pe 's/Robert (?=Dylan)/Bob /g' names.txt

对于 Perl 新手来说,这一行代码乍一看可能有点吓人,但它实际上非常简单优雅。

-i 标志使程序的输出写回文件,而不是显示在终端屏幕上。 您可以为 -i 提供扩展名,以将输入文件保存到具有给定扩展名的文件中。 换句话说,我正在创建原始文件的备份,扩展名为 .bkp。 (请确保 -i 和扩展名 .bkp 之间没有空格。)

之后,我使用 -pe 选项。 -e 选项允许我从命令行运行 Perl。 -p 选项使我的代码循环遍历文件的每一行并打印输出。 毕竟,我希望新文件包含原始文件中的每个姓名,而不仅仅是 Dylan 先生的姓名。

接下来是短语 s/Robert (?=Dylan)/Bob /g

在这里,我将第一个和第二个斜杠之间的内容替换为第二个和第三个斜杠之间的内容(由 s 指示)。 在这种情况下,我想在特定情况下将“Robert”替换为“Bob”。 我想对文件中每个实例都执行此操作,而不仅仅是它找到的第一个实例,所以我使用 g 标志表示全局

那么那个看起来很奇怪的 (?=Dylan) 呢? 这就是正则表达式世界中所谓的正向环视。 它是非捕获的,因此它不会被任何东西(在本例中为 Bob)替换; 相反,该表达式缩小了实际更改的结果范围。

我正在寻找字符串“Robert”,当且仅当它后面跟着(那是正向环视)字符串“Dylan”时。

否则,忽略它。 例如,如果我的姓名列表中有姓名“Robert Smith”,我想保持原样,而不是将其更改为“Bob Smith”。

以下是 Perl 用户可用的环视

  • 正向环视:?=pattern
  • 负向环视:?!pattern
  • 正向后顾:?<=pattern
  • 负向后顾:?<!pattern

请务必将后顾放在您要搜索的字符串后面。 要将“Sam”更改为“Samantha”,但前提是“Miss”在其前面,您应该这样写

s/(?<=Miss) Sam/Samantha/g'

捕获单词之前或之后的内容

如果您想获取单词之前或之后的所有内容,但您不知道会有多少个单词,该怎么办? Perl 使执行此操作变得快速而简单。

此示例以最近(虚构的)棒球比赛列表开头,其中获胜球队首先列出,后跟单词“over”,后跟未获胜球队和最终得分。

San Francisco Giants over Miami Marlins 3:0
Chicago Cubs over Houston Astros 6:1
New York Mets over San Francisco Giants 4:3

Perl 有一些特殊的内置变量

  • $&(美元符号)包含最后捕获的字符串
  • $`(反引号)包含行上捕获的字符串之前的内容
  • $'(单引号)包含行上捕获的字符串之后的内容

要获取获胜球队的列表,我需要捕获单词“over”,然后输出它之前的所有内容。

use strict;
use warnings;

while (<DATA>){
        /over/;
        print "$`\n";

}

使用 seek 函数在文件中移动

到目前为止,我提到的所有程序都从顶部开始,逐行继续,直到到达末尾,此时程序结束。 这通常是您所需要的全部,但有时您希望在程序中跳转以按特定顺序执行特定任务。

在这种情况下,Perl 的 seek 函数就是您正在寻找的。

seek 函数接受三个参数:文件句柄、字节偏移量和文件位置。

文件位置可以是以下三个值之一

  • 0 = 文件开头
  • 1 = 文件中的当前位置
  • 2 = 文件末尾

第二个参数,字节偏移量,是您要移动到的位置与文件位置之间的字节数。

正数将光标位置向右移动,而负数将光标向左移动。 因为开头前面没有任何内容,所以只有当文件位置为 1 或 2 时,才能使用负字节偏移量。

这是一个使这一切清晰的示例

假设您有一个包含姓名和生日的大列表。 您想要创建一个新列表,其中八月份生日的人列在顶部,然后是其他人。

要完成此操作,您需要遍历整个列表,找到所有八月份生日的人。 然后,一旦到达列表底部,您必须返回顶部并获取所有非八月份生日的人。

这是原始文件的一部分

Bob Smith 03/12/1967
Carl Carlson 01/22/1998
Susan Meyers 01/28/1980
Derek Jackson 08/02/2009
Sara Miller 02/11/2002
Marcus Philips 08/28/1999
Jeremy Stills 11/30/2001

这是一个完成此任务的 Perl 脚本

use strict;
use warnings;

open my $fh, '<:encoding(UTF-8)', "originalfile.txt" or
    or die "Error opening file: $!d\n";

while($line = <$fh>){
  if ($line =~ m#\t\t08/#){
    print "$line\n";
  }

seek ($fh, 0, 0);

while (<$fh>){
  if ($line !~ m#\t\t08/#){
    print "$line";
  }

close $fh;

if ($line =~ m#\t\t08/#) { 使用 m 标志进行正则表达式搜索,允许您在搜索中使用任意分隔符。

正如您之前可能已经注意到的,默认值是正斜杠 (/)。 但是由于日期中使用了正斜杠,这可能会使搜索出错。 幸运的是,Perl 允许您通过在 m 标志后放置您的选择来使用不同的分隔符。 在此示例中,我使用了常见的替代哈希 (#),但您可以使用其他字符(例如,方括号、& 符号、大写 X 等),只要它不干扰或混淆您的查询即可。

在本例中,您要搜索两个制表符,在本例中写为 \t\t。 它也可以写成 \t{2}

制表符后面必须跟一个 0,然后是 8(八月是第八个月),然后是正斜杠。 请注意,您不能只搜索 08,因为这也将匹配任何月份的第八天出生的人以及 2008 年出生的人。

在 Perl 找到并打印所有八月份生日之后,我使用 seek 函数返回到文件开头。 第二次遍历文件时,正则表达式搜索从匹配 (=~) 更改为不匹配 (!~),以获取在其他 11 个月中出生的人。

向他人解释正则表达式

正则表达式在 Perl 和许多 其他语言中,是一件很棒的知识和工具。

它们可以将原本冗长而令人困惑的编程过程变成一个只有几个字符的简单表达式。 但它们确实有时会给人留下有点神秘的印象。

编写一个冗长而复杂的正则表达式可能会让程序员感到自豪,但对于不必要的复杂代码来说,没有容身之地。 优秀程序员的一个标志是其他程序员可以轻松理解他们在做什么。

在编写任何比相对基本的正则表达式更复杂的内容时,通常最好使用 x 选项注释您的正则表达式。 此选项使 Perl 忽略正则表达式中的任何注释和空格,因此您可以向未来的自己和其他人解释您试图做什么。

注意:要问自己的问题不是“即使没有注释,我是否可以弄清楚正则表达式在做什么”,而是“我是否应该要求其他人弄清楚”。 不要让其他人试图弄清楚您在做什么。

比较下面的两个代码示例。 它们都做同样的事情,但第二个版本更容易理解。

假设您有以下示例数据要搜索

01/21/1998
sample text
Sept/21/97
Here is another line
Mr. Smith
01-12-2009
7/23/1998
Fake text
Feb./5/09

并且您的 Perl 脚本中有以下正则表达式

m%(?<![-|/|\d])((\d\d?)|[A-Z][a-z]*\.?)(?=[-|/])(/|-)\d\d?(/|-)\d{2,4}%

您能看一眼并理解它在做什么吗? 可能不能。 您或许可以弄清楚,但这需要几分钟时间。

另一方面,您可以像这样编写相同的正则表达式

use strict;
use warnings;

while (<DATA>){
  print if m%   # capture dates written in multiple formats
  (?<![-/\d])   # is not preceded by a hyphen, slash, or digit
  ((\d\d?)|[A-Z][a-z]*\.?)(?=[-/])  # month 1 or 2 digits, or word with optional hyphen
                                    # followed by a hyphen or slash
  (/|-)\d\d?    # 1 or 2 digit day
  (/|-)\d{2,4}  # 2 or 4 digit year
  %x;
}

此版本清楚地表明我们正在搜索日期。

月份显示为一位或两位数字,写成单词或缩写,带或不带句点,后跟斜杠或连字符。 日期写成一位或两位数字,后跟斜杠或连字符,后跟年份,写成四位数年份或两位数年份。

在正则表达式中使用 x 修饰符使 Perl 忽略空格和注释,从而可以更友好的方式解释正则表达式。 请注意,在本示例中,和以前一样,我也使用了 m 修饰符将正则表达式分隔符从默认的 / 更改为 %,因为此正则表达式包含正斜杠。

结论

我希望本文能让您体验到 Perl 语言如何加速您的一些基于文本的编码问题并使您的工作更轻松。 Perl 是一种成熟且丰富的语言; 本文的介绍仅仅触及了它功能的皮毛。 如果您有兴趣提高作为程序员的生产力,那么 Perl 值得一看。

接下来阅读什么
标签
User profile image.
Hunter 是一位开源和数据爱好者,也是让每个人更容易访问数据的倡导者。 他是 OpenCurator.com 的创始人,该网站致力于让公共数据易于查找和理解。

评论已关闭。

© . All rights reserved.