【问题标题】:Extract email addresses from academic curly braces format从学术花括号格式中提取电子邮件地址
【发布时间】:2019-08-23 00:15:28
【问题描述】:

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

{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

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

【问题讨论】:

  • 您能否详细说明您的minimal reproducible example“不起作用”(并添加)?你期待什么,实际发生了什么?如果您遇到异常/错误,请按照如何创建minimal reproducible example 页面发布它发生的行和异常/错误详细信息。请edit您的问题将这些详细信息添加到其中,否则我们可能无法提供帮助。

标签: python parsing email-address


【解决方案1】:

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 的作者。

【讨论】:

  • 听起来pyparsing 实际上是一个库,可以像我建议的那样简化构建词法分析器/标记器。绝对看起来它比尝试将逻辑包装在正则表达式周围的代码更少且更准确,并且看起来比手动实现表达式语法要容易得多。
  • 是的,PEG 解析器绝对是 lex/yacc 传统的终结。如果您愿意,David Beazley 的 PLY 包可以将 lex/yacc 直接构建到您的 Python 代码中。除了 pyparsing 之外,还有许多 PEG 解析库。
  • 工作得很好,在这种情况下确实是比正则表达式更好的方法
【解决方案2】:

注意

我对 JavaScript 比对 Python 更熟悉,而且基本逻辑都是一样的(不同的是语法),所以我在这里用 JavaScript 编写了我的解决方案。随意翻译成 Python。

问题

这个问题比一个简单的单行脚本或正则表达式要复杂一些,但根据具体要求,你可能会得到一些基本的东西。

对于初学者来说,解析电子邮件并不是简单地归结为一个正则表达式。 This website 有几个正则表达式示例可以匹配“许多”电子邮件,但解释了权衡(复杂性与准确性)并继续包含理论上应该匹配的 RFC 5322 标准正则表达式 任何电子邮件,后跟一段说明您不应该使用它的原因。然而,即使 那个 正则表达式也假定采用 IP 地址形式的域名只能由 0 到 255 之间的四个整数组成的元组——它没有t 允许 IPv6

甚至像这样简单的事情:

{a, b}@domain.com

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

"{a, b}"@domain.com

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

一些基本的东西

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

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

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

这应该大致符合以下形式:

{one, two}@domain1.domain2.toplevel

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

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" ]

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

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

为了构建电子邮件的“域”部分,我们需要对@ 之后的所有内容都使用类似的逻辑,因为这有可能重复,就像名称部分有可能重复一样。我们的最终代码(在 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}`);
}

多邮件线路

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

但是,使用正则表达式可以轻松完成的是找到包含后与号逗号的文本块。比如:@[^@{]+?,

在字符串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% 的用例,这个简单的词法分析器就足够了,我们现在可以构建一个“通常有效但不完美”的解决方案,如下所示:

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),你一次读一个字母,但根据你当前的状态来解释它。

【讨论】:

  • 我选择了 python 解决方案,但感谢详细的分步说明
【解决方案3】:

使用 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

【讨论】:

  • 我尝试了您的解决方案,但在一行包含花括号和常规邮件的“压缩电子邮件”的情况下,它不起作用。仍然可能是我遇到的更简单的情况
猜你喜欢
  • 2013-04-18
  • 1970-01-01
  • 2016-04-12
  • 2011-12-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多