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

时间:2019-04-01 18:52:41

标签: python parsing email-address

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

{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

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

3 个答案:

答案 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
''')
相关问题