从学术大括号格式中提取电子邮件地址

Extract email addresses from academic curly braces format

我有一个文件,其中每一行都包含一个代表一个或多个电子邮件地址的字符串。 多个地址可以在大括号内分组,如下所示:

{name.surname, name2.surnam2}@something.edu

这意味着地址name.surname@something.eduname2.surname2@something.edu都有效(这种格式通常用于科学论文)。

此外,单行也可以多次包含大括号。示例:

{a.b, c.d, e.f}@uni.somewhere, {x.y, z.k}@edu.com

结果:

a.b@uni.somewhere 
c.d@uni.somewhere 
e.f@uni.somewhere
x.y@edu.com
z.k@edu.com

关于如何解析此格式以提取所有电子邮件地址的任何建议?我正在尝试使用正则表达式,但我目前正在苦苦挣扎。

备注

我对JavaScript比Python更熟悉,基本逻辑都是一样的(不同的是语法),所以我把我的解决方案写在这里[=154] =].欢迎翻译成 Python.

问题

这个问题比简单的单行脚本或正则表达式要复杂一些,但根据具体要求,您可能能够解决一些基本问题。

对于初学者来说,解析电子邮件并不能简单地归结为单个正则表达式。 This website 有几个匹配 "many" 电子邮件的正则表达式示例,但解释了权衡(复杂性与准确性)并继续包括理论上应该匹配的 RFC 5322 标准正则表达式 any 电子邮件,后跟一段说明您不应该使用它的原因。然而,即使 that 正则表达式也假设采用 IP 地址形式的域名只能由 0 到 4 个整数的元组组成255 -- 它不允许 IPv6

甚至像这样简单的东西:

{a, b}@domain.com

可能会出错,因为从技术上讲,根据电子邮件地址规范,电子邮件地址可以包含 ANY ASCII 字符并用引号括起来。以下是有效的(单个)电子邮件地址:

"{a, b}"@domain.com

要准确解析一封电子邮件,您需要一次读取一个字母的字符,并构建一个有限状态机来跟踪您是否在双引号内、大括号内、[= 之前24=],在@之后,解析域名,解析IP等。这样你就可以标记地址,找到你的大括号标记,并独立解析它。

基本的东西

正则表达式不是获得 100% 准确度和支持所有电子邮件的方法,*尤其是* 如果您想支持多个电子邮件一条线。但我们将从它们开始,并尝试从那里开始构建。

您可能尝试过如下正则表达式:

/\{(([^,]+),?)+\}\@(\w+\.)+[A-Za-z]+/
  • 匹配单个大括号...
  • 后跟一个或多个实例:
    • 一个或多个非逗号字符...
    • 后跟零个或一个逗号
  • 后跟一个右花括号...
  • 后跟一个@
  • 后跟一个或多个实例:
    • 一个或多个 "word" 个字符...
    • 后跟一个.
  • 后跟一个或多个字母字符

这应该大致匹配以下形式:

{one, two}@domain1.domain2.toplevel

这里处理验证,接下来是提取的问题 所有有效的电子邮件。请注意,我们在电子邮件地址的名称部分嵌套了两组括号:(([^,]+),?)。这给我们带来了一个问题。在这种情况下,许多正则表达式引擎不知道如何 return 匹配。考虑一下当我 运行 在 JavaScript 中使用我的 Chrome 开发者控制台时会发生什么:

var regex = /\{(([^,]+),?)+\}\@(\w+\.)+[A-Za-z]+/
var matches = "{one, two}@domain.com".match(regex)
Array(4) [ "{one, two}@domain.com", " two", " two", "domain." ]

那是不对的。它找到了 two 两次,但没有找到 one 一次!要解决此问题,我们需要消除嵌套并分两步完成。

var regexOne = /\{([^}]+)\}\@(\w+\.)+[A-Za-z]+/
"{one, two}@domain.com".match(regexOne)
Array(3) [ "{one, two}@domain.com", "one, two", "domain." ]

现在我们可以使用匹配并单独解析它了:

// Note: It's important that this be a global regex (the /g modifier) since we expect the pattern to match multiple times
var regexTwo = /([^,]+,?)/g
var nameMatches = matches[1].match(regexTwo)
Array(2) [ "one,", " two" ]

现在我们可以 trim 这些并得到我们的名字:

nameMatches.map(name => name.replace(/, /g, "")
nameMatches
Array(2) [ "one", "two" ]

为了构建电子邮件的 "domain" 部分,我们需要对 @ 之后的所有内容使用类似的逻辑,因为这可能会像名称部分一样重复重复的可能性。我们的最终代码(在 JavaScript 中)可能看起来像这样(您必须自己转换为 Python):

function getEmails(input)
{
    var emailRegex = /([^@]+)\@(.+)/;
    var emailParts = input.match(emailRegex);

    var name = emailParts[1];
    var domain = emailParts[2];

    var nameList;

    if (/\{.+\}/.test(name))
    {
        // The name takes the form "{...}"
        var nameRegex = /([^,]+,?)/g;
        var nameParts = name.match(nameRegex);
        nameList = nameParts.map(name => name.replace(/\{|\}|,| /g, ""));
    }
    else
    {
        // The name is not surrounded by curly braces
        nameList = [name];
    }

    return nameList.map(name => `${name}@${domain}`);
}

多条电子邮件线路

这是事情开始变得棘手的地方,如果我们不想构建完整的词法分析器/分词器,我们需要接受稍低的准确性。因为我们的电子邮件包含逗号(在姓名字段内),所以我们无法准确地按逗号分隔——除非这些逗号不在大括号内。以我对正则表达式的了解,我不知道这是否可以轻松完成。前瞻或后视运算符可能是可行的,但其他人必须填写我的内容。

然而,使用正则表达式可以轻松完成的任务是查找包含 post-& 逗号的文本块。类似于:@[^@{]+?,

在字符串 a@b.com, c@d.com 中,这将匹配整个短语 @b.com, - 但重要的是它为我们提供了一个拆分字符串的位置。棘手的一点是找出如何在此处拆分您的字符串。大部分时间都可以使用类似的方法:

var emails = "a@b.com, c@d.com"
var matches = emails.match(/@[^@{]+?,/g)
var split = emails.split(matches[0])
console.log(split) // Array(2) [ "a", " c@d.com" ]
split[0] = split[0] + matches[0] // Add back in what we split on

如果您在列表中有两封具有相同域的电子邮件,这可能存在错误:

var emails = "a@b.com, c@b.com, d@e.com"
var matches = emails.match(@[^@{]+?,/g)
var split = emails.split(matches[0])
console.log(split) // Array(3) [ "a", " c", " d@e.com" ]
split[0] = split[0] + matches[0]
console.log(split) // Array(3) [ "a@b.com", " c", " d@e.com" ]

但同样,在不构建词法分析器/分词器的情况下,我们接受我们的解决方案仅适用于 大多数 情况,而不是所有情况.

然而,由于将一行拆分为多封电子邮件的任务比深入研究电子邮件、提取名称和解析名称更容易:我们也许可以为此编写一个非常愚蠢的词法分析器部分:

var inBrackets = false
var emails = "{a, b}@c.com, d@e.com"
var split = []
var lastSplit = 0
for (var i = 0; i < emails.length; i++)
{
    if (inBrackets && emails[i] === "}")
        inBrackets = false;
    if (!inBrackets && emails[i] === "{")
        inBrackets = true;
    if (!inBrackets && emails[i] === ",")
    {
        split.push(emails.substring(lastSplit, i))
        lastSplit = i + 1 // Skip the comma
    }
}
split.push(emails.substring(lastSplit))
console.log(split)

再说一次,这不会是完美的解决方案,因为可能存在如下电子邮件地址:

","@domain.com

但是,对于 99% 的用例,这个简单的词法分析器就足够了,我们现在可以构建一个 "usually works but not perfect" 解决方案,如下所示:

function getEmails(input)
{
    var emailRegex = /([^@]+)\@(.+)/;
    var emailParts = input.match(emailRegex);

    var name = emailParts[1];
    var domain = emailParts[2];

    var nameList;

    if (/\{.+\}/.test(name))
    {
        // The name takes the form "{...}"
        var nameRegex = /([^,]+,?)/g;
        var nameParts = name.match(nameRegex);
        nameList = nameParts.map(name => name.replace(/\{|\}|,| /g, ""));
    }
    else
    {
        // The name is not surrounded by curly braces
        nameList = [name];
    }

    return nameList.map(name => `${name}@${domain}`);
}

function splitLine(line)
{
    var inBrackets = false;
    var split = [];
    var lastSplit = 0;
    for (var i = 0; i < line.length; i++)
    {
        if (inBrackets && line[i] === "}")
            inBrackets = false;
        if (!inBrackets && line[i] === "{")
            inBrackets = true;
        if (!inBrackets && line[i] === ",")
        {
            split.push(line.substring(lastSplit, i));
            lastSplit = i + 1;
        }
    }
    split.push(line.substring(lastSplit));
    return split;
}

var line = "{a.b, c.d, e.f}@uni.somewhere, {x.y, z.k}@edu.com";
var emails = splitLine(line);
var finalList = [];
for (var i = 0; i < emails.length; i++)
{
    finalList = finalList.concat(getEmails(emails[i]));
}
console.log(finalList);
// Outputs: [ "a.b@uni.somewhere", "c.d@uni.somewhere", "e.f@uni.somewhere", "x.y@edu.com", "z.k@edu.com" ]

如果您想尝试实施完整的词法分析器/分词器解决方案,您可以查看我构建的简单/哑词法分析器作为起点。一般的想法是你有一个状态机(在我的例子中我只有两个状态:inBrackets!inBrackets)并且你一次读一个字母但是根据你当前的状态不同地解释它。

Pyparsing 是一个 PEG 解析器,它为您提供了一个嵌入式 DSL 来构建可以读取像这样的表达式的解析器,生成的代码比正则表达式更具可读性(和可维护性),并且足够灵活以添加事后思考(等等,电子邮件的某些部分可以用引号引起来?)。

pyparsing 使用“+”和“|”运算符从较小的位构建您的解析器。它还支持命名字段(类似于正则表达式命名组)和解析时回调。在下面查看这一切如何组合在一起:

import pyparsing as pp

LBRACE, RBRACE = map(pp.Suppress, "{}")
email_part = pp.quotedString | pp.Word(pp.printables, excludeChars=',{}@')

# define a compressed email, and assign names to the separate parts
# for easier processing - luckily the default delimitedList delimiter is ','
compressed_email = (LBRACE 
                    + pp.Group(pp.delimitedList(email_part))('names')
                    + RBRACE
                    + '@' 
                    + email_part('trailing'))

# add a parse-time callback to expand the compressed emails into a list
# of constructed emails - note how the names are used
def expand_compressed_email(t):
    return ["{}@{}".format(name, t.trailing) for name in t.names]
compressed_email.addParseAction(expand_compressed_email)

# some lists will just contain plain old uncompressed emails too
# Combine will merge the separate tokens into a single string
plain_email = pp.Combine(email_part + '@' + email_part)

# the complete list parser looks for a comma-delimited list of compressed 
# or plain emails
email_list_parser = pp.delimitedList(compressed_email | plain_email)

pyparsing 解析器附带一个 runTests 方法来针对各种测试字符串测试您的解析器:

tests = """\
    # original test string
    {a.b, c.d, e.f}@uni.somewhere, {x.y, z.k}@edu.com

    # a tricky email containing a quoted string
    {x.y, z.k}@edu.com, "{a, b}"@domain.com

    # just a plain email
    plain_old_bob@uni.elsewhere

    # mixed list of plain and compressed emails
    {a.b, c.d, e.f}@uni.somewhere, {x.y, z.k}@edu.com, plain_old_bob@uni.elsewhere
"""

email_list_parser.runTests(tests)

打印:

# original test string
{a.b, c.d, e.f}@uni.somewhere, {x.y, z.k}@edu.com
['a.b@uni.somewhere', 'c.d@uni.somewhere', 'e.f@uni.somewhere', 'x.y@edu.com', 'z.k@edu.com']

# a tricky email containing a quoted string
{x.y, z.k}@edu.com, "{a, b}"@domain.com
['x.y@edu.com', 'z.k@edu.com', '"{a, b}"@domain.com']

# just a plain email
plain_old_bob@uni.elsewhere
['plain_old_bob@uni.elsewhere']

# mixed list of plain and compressed emails
{a.b, c.d, e.f}@uni.somewhere, {x.y, z.k}@edu.com, plain_old_bob@uni.elsewhere
['a.b@uni.somewhere', 'c.d@uni.somewhere', 'e.f@uni.somewhere', 'x.y@edu.com', 'z.k@edu.com', 'plain_old_bob@uni.elsewhere']

披露:我是 pyparsing 的作者。

使用 re 的快速解决方案:

用一个文本行测试:

import re

line = '{a.b, c.d, e.f}@uni.somewhere, {x.y, z.k}@edu.com, {z.z, z.a}@edu.com'

com = re.findall(r'(@[^,\n]+),?', line)  #trap @xx.yyy
adrs = re.findall(r'{([^}]+)}', line)  #trap all inside { }
result=[]
for i  in range(len(adrs)):
    s = re.sub(r',\s*', com[i] + ',', adrs[i]) + com[i]
    result=result+s.split(',')

for r in result:
    print(r)

列表结果输出:

a.b@uni.somewhere
c.d@uni.somewhere
e.f@uni.somewhere
x.y@edu.com
z.k@edu.com
z.z@edu.com
z.a@edu.com

使用文本文件进行测试:

import io
data = io.StringIO(u'''\
{a.b, c.d, e.f}@uni.somewhere, {x.y, z.k}@edu.com, {z.z, z.a}@edu.com
{a.b, c.d, e.f}@uni.anywhere
{x.y, z.k}@adi.com, {z.z, z.a}@du.com
''')

result=[]
import re
for line in data:
    com = re.findall(r'(@[^,\n]+),?', line)
    adrs = re.findall(r'{([^}]+)}', line)
    for i in range(len(adrs)):
        s = re.sub(r',\s*', com[i] + ',', adrs[i]) + com[i]
        result = result + s.split(',')

for r in result:
    print(r)

列表结果输出:

a.b@uni.somewhere
c.d@uni.somewhere
e.f@uni.somewhere
x.y@edu.com
z.k@edu.com
z.z@edu.com
z.a@edu.com
a.b@uni.anywhere
c.d@uni.anywhere
e.f@uni.anywhere
x.y@adi.com
z.k@adi.com
z.z@du.com
z.a@du.com