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