我有一个文件,其中每一行包含一个代表一个或多个电子邮件地址的字符串。 可以在花括号内将多个地址分组,如下所示:
{name.surname, name2.surnam2}@something.edu
这意味着地址name.surname@something.edu
和name2.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
关于如何解析此格式以提取所有电子邮件地址的任何建议?我正在尝试使用正则表达式,但目前正在挣扎。
答案 0 :(得分:2)
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的作者。
答案 1 :(得分:1)
我比Python更熟悉JavaScript,并且无论如何(语法不同),基本逻辑都是相同的,因此我在这里用JavaScript编写了解决方案。随时翻译成Python。
这个问题比简单的单行脚本或正则表达式要复杂得多,但是根据特定要求,您也许可以摆脱一些基本知识。
对于初学者来说,解析电子邮件并非简单地归结为单个正则表达式。 This website有几个与“许多”电子邮件匹配的正则表达式示例,但解释了折衷(复杂性与准确性),并继续包括理论上应与匹配的RFC 5322标准正则表达式 any 电子邮件,后跟一段为什么不使用它的原因。但是,即使 正则表达式也假定采用IP地址形式的域名只能由0至255范围内的四个整数组成的元组-它不会不支持IPv6
甚至很简单:
{a, b}@domain.com
可能会跳闸,因为从技术上讲,根据电子邮件地址规范,电子邮件地址可以包含 ANY 引号引起来的ASCII字符。以下是有效的(单个)电子邮件地址:
"{a, b}"@domain.com
要准确地解析电子邮件,需要您一次读取一个字母并构建一个有限状态机,以跟踪您是否在{{1}之前的双引号中,大括号中},在@
之后,解析域名,解析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):
@
这是开始变得棘手的地方,如果我们不想在lexer / tokenizer上构建完整的表,我们需要接受稍低的准确性。由于我们的电子邮件包含逗号(在名称字段中),因此我们无法准确地分割逗号-除非这些逗号不在花括号内。以我对正则表达式的了解,我不知道这是否容易实现。先行或后行运算符可能是可行的,但其他人将不得不填写。
但是,使用正则表达式可以轻松地完成查找包含后与号逗号的文本块。类似于: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)
但是,对于99%的用例,这个简单的词法分析器就足够了,我们现在可以构建一个“通常可行但不完美”的解决方案,如下所示:
","@domain.com
如果您想尝试实现完整的lexer / tokenizer解决方案,则可以看一下我作为起点构建的简单/笨拙的lexer。一般的想法是,您有一个状态机(在我的情况下,我只有两个状态: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
),您一次读一个字母,但根据当前状态以不同的方式解释它。
答案 2 :(得分:1)
使用re 的快速解决方案:
perl -pe "s/(DEFAULT) (?!(NULL|CHARSET|''))([a-zA-Z0-9_]+)/\1 '\3'/g" file
在列表结果中输出:
#1 CREATE TABLE `table` (`column` int(10) unsigned DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
#2 ALTER TABLE `table` MODIFY COLUMN `column2` enum('ONE','TWO') NOT NULL DEFAULT 'ONE' AFTER `column1`;
#3 ALTER TABLE `table` MODIFY COLUMN `column` varchar(64) NOT NULL DEFAULT '' FIRST;
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
''')