如何从任意文本中提取电子邮件标题和邮件 ID?

How can I extract email headers and message IDs from arbitrary text?

提问人:Todd 提问时间:11/16/2023 最后编辑:brian d foyTodd 更新时间:11/22/2023 访问量:103

问:

以下测试程序说明了我在尝试区分 MessageID 和电子邮件地址时遇到的一个问题,尤其是当我事先不知道我正在解析电子邮件标题时。

#!/opt/perl/bin/perl
  # use Regexp::Debugger;
  use warnings;
  no warnings qw(experimental::vlb);

  my $re = qr{
          (
              (?:
                  # one or more of these
                  [\=a-z0-9!\#$%&'*+/?^_`{|}~-]+
                  # zero or more of these
                  (?:\.[\=a-z0-9!\#$%&'*+/?^_`{|}~-]+)*
              )
              @
              (?:
                  (?!\d+\.\d+)
                  (?=.{4,255})
                  (?:
                      (?:[a-zA-Z0-9-]{1,63}(?<!-)\.)+
                      [a-zA-Z0-9-]{2,63}
                  )
              )
          )
  }xims;
  my $text = <<'EOF';

  Arbitrary text followed by a snippet of an email header:

  To: "T B" <[email protected]>, "Foobar" <[email protected]>
  Message-ID: <[email protected]>

  More text.

  EOF

  while ( $text =~ m/$re/g ) {
      print "$1\n";
  }

输出:

[email protected]
[email protected]
[email protected]

我想要的输出是

[email protected]
[email protected]

我尝试在后面添加一个外观,但后来我没有得到匹配项。(?<=To:\ )

较大的程序对输入文本应用几百个正则表达式。每个正则表达式都是一种特定的类型,例如 foo => qr/[Ff]oo/,如果它匹配,则该文本会用标识它匹配的正则表达式的标记“包装”。例如<span class=“foo”>foo</span>。

正则表达式 Perl

评论

0赞 Carlo Arenas 11/16/2023
您需要在表达式之前使用一个后视,以便有一个“约束”,指示何时可以匹配,但这不是匹配文本的一部分。(?<=(From|To))
4赞 Shawn 11/16/2023
不要试图一步到位。首先找到 to 和 from 行,然后提取地址。
0赞 Barmar 11/16/2023
您是否必须在单个正则表达式中执行此操作?首先找到您关心的所有标题,然后从中提取电子邮件地址不是更容易吗?
0赞 zdim 11/16/2023
"这里只能使用正则表达式“——这到底是什么意思?这不是Perl程序的一部分吗?如果不是,请删除标签。perl
1赞 Steffen Ullrich 11/16/2023
“......如果该行开始......“ - 对于邮件来说,必须是同一行的假设是错误的。标题字段可以跨越邮件中的多行,即诸如之类的内容完全没问题,甚至可能需要将行长度限制为 1000。解析邮件标题非常棘手,很容易出错。最好分步进行,而不是在单个正则表达式中尝试。To: [email protected],\r\n<space> [email protected]\r\n

答:

2赞 zdim 11/17/2023 #1

通过问题澄清(以及更改为不只请求正则表达式),这里有一个看法。

首先提取所有标题,每个标题都带有后面的文本,直到下一个标题(请参阅下面的进一步内容,以仅匹配标题)。然后,我们可以从这些捕获的项目中获取地址,从我们想要的标头中获取地址。必须首先获取所有标头,否则不需要的标头会被我们匹配的标头啜饮。

use warnings;
use strict;
use feature 'say';

my $text = do { local $/; <DATA> };  # slurp all text into a scalar    
#say $text; say '-'x60;

# These better be all headers with email addresses in the text
my $hdr_re = qr/To|From|Message-ID/;
my @headers_plus = $text =~ /( $hdr_re: .*? )(?=(?:$hdr_re|$) )/sxg;

#say "\nHeaders with the following text (until next header):\n";
#say "$_\n---\n" for @headers_plus;

foreach my $hdr_plus (@headers_plus) {
    next if not $hdr_plus =~ /^\s*(To|From)/;
    my $header_type = $1;

    my @addresses = $hdr_plus =~ /<([^>]+)>/g;

    say "Addresses for |$header_type| header:";
    say for @addresses;
    say '';
}

__DATA__
Arbitrary text followed by a snippet of an email header:

To: "T B" <[email protected]>, "Foobar" <[email protected]>
Message-ID: <[email protected]>
From: "X Y" <[email protected]>,
"Other" <[email protected]>

To: "Yo" <[email protected]>

More text.

我在问题的文本中添加了一些标题,一个多行。为了匹配下一个标题的开头,我使用正 lookahead (?=...) 作为下一个标题的关键字(或字符串末尾,如果它是文本中的最后一个标题)。

这是相当初级的,但重点是它很简单,希望它适用于问题中所示的简化情况。但请记住,解析标头是很棘手的。

这种方法非常简单,正则表达式非常宽松 -- 在捕获每个 or 或 all 之后,直到下一个 or 或 .因此,它会对大部分文本进行分区,如果其他电子邮件地址存在于标题之外的文本中,我们可能会“捕获”它们。因此,也许你更愿意限制在&Co之后捕获的内容。To:From:Message-ID:To:From:Message-ID:To:

为此,让我们大致遵循 RFC 2822 第 3.6 节和第 3.4 节,并考虑字段可以具有逗号分隔的地址,并且对于地址,采用“name”<address>形式(并使 name 成为可选),如问题所示。该列表可能只有一个地址,然后不会以逗号结尾,例如 for 和 。然后ToFrom:Message-ID

my @headers = $text =~ / ( 
    $hdr_re: \s*
    (?: (?: "[^"]+")? \s* <[^>]+>, \s* )* 
        (?: "[^"]+")? \s* <[^>]+> 
) /xg;

这在我的测试中适用于扩展问题的样本。但是,以这种方式限制正则表达式,这仍然很不完整,当然可能会导致丢失一些现实生活中的地址和/或无法匹配。因此,请在真实样品上仔细测试。


还可以立即过滤掉不需要的标头

my @headers_plus = 
    grep { /^\s*To|From/ }
    $text =~ /( $hdr_re: .*? )(?=(?:$hdr_re|$))/sxg;

然后你也可以在那里抛出一个并获取地址,但我认为没有理由像那样塞满它。map


文本中的正则表达式允许标题从行中的任意位置开始。但是,如果它们总是从一行的开头开始,那么这可能是一个很好的限制。然后我们就会有

my @headers_plus = $text =~ /^\s*( $hdr_re: .*? )(?=(?:$hdr_re|\Z))/msxg;

现在我们需要“多行”修饰符 (),以便匹配文本中的新行。然后整个字符串的末尾是(因为现在匹配文本中每个“行”的末尾)。/m^\Z$

与与实际标题匹配的模式相同(没有以下文本,稍后在帖子中显示),只是我们不需要修饰符,因为该模式中不需要修饰符。/s.

评论

0赞 zdim 11/21/2023
@Todd 添加了一个正则表达式,用于从文本中挑选实际的标题,比原始正则表达式选取的“header-plus”更受限制(字段名称后面的所有内容,直到下一个字段名称)。这当然更好 - 但是如果它与文本中的所有现实标题相匹配,那么现在这成为一个有效的问题。(当然,原来的“header-plus”应该匹配,因为它实际上只是对文本进行了分区。如果它们像你展示的那样简单和一致,这应该有效。
0赞 Reilas 11/17/2023 #2

Message-ID 字段应仅包含一个地址。

RFC 2822 – Internet 消息格式 – 3.6.4。标识字段

...“Message-ID:”字段包含单个唯一消息标识符。...

请尝试以下捕获模式

(?<!^Message-ID:\s)<(.+?)>

或者,匹配模式

(?<!^Message-ID:\s<)(?<=<).+?(?=>)
0赞 mpersico 11/21/2023 #3

首先,我会编写一个脚本,任何匹配的内容提取到一个单独的文件中

/^\w+:/

即:任何看起来像标题的东西。然后我会尝试找到一个邮件消息解析模块并使用它。