Извлечение адресов электронной почты из академического формата фигурных скобок

У меня есть файл, в котором каждая строка содержит строку, представляющую один или несколько адресов электронной почты. Несколько адресов можно сгруппировать внутри фигурных скобок следующим образом:

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

Это означает, что оба адреса [email protected] и [email protected] действительны (этот формат обычно используется в научных статьях).

Кроме того, одна строка может содержать фигурные скобки несколько раз. Пример:

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

приводит к:

[email protected] 
[email protected] 
[email protected]
[email protected]
[email protected]

Любое предложение о том, как я могу проанализировать этот формат, чтобы извлечь все адреса электронной почты? Я пытаюсь использовать регулярные выражения, но в настоящее время я борюсь.


person Gian Luca Scoccia    schedule 01.04.2019    source источник
comment
Можете ли вы уточнить, почему ваш минимально воспроизводимый пример не работает (и добавить его)? Чего вы ожидали и что произошло на самом деле? Если вы получили исключение/ошибку, опубликуйте строку, в которой она произошла, и сведения об исключении/ошибке в соответствии с Как создать минимальный воспроизводимый пример страницы. Пожалуйста, отредактируйте свой вопрос, чтобы добавить в него эти данные, иначе мы не сможем помочь.   -  person Patrick Artner    schedule 01.04.2019


Ответы (3)


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
    [email protected]

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

email_list_parser.runTests(tests)

Отпечатки:

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

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

# just a plain email
[email protected]
['[email protected]']

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

РАСКРЫТИЕ: я автор pyparsing.

person PaulMcG    schedule 01.04.2019
comment
Похоже, что pyparsing по сути является библиотекой для упрощения создания лексера/токенизатора, как я и предлагал. Определенно похоже, что это будет меньше кода и более точно, чем попытка обернуть логику вокруг регулярных выражений, и выглядит намного проще, чем реализация грамматики выражений вручную. - person stevendesu; 02.04.2019
comment
Да, синтаксические анализаторы PEG определенно являются концом традиции lex/yacc. Пакет PLY Дэвида Бизли встраивает lex/yacc прямо в ваш код Python, если хотите. Помимо pyparsing, существует также ряд библиотек для синтаксического анализа PEG. - person PaulMcG; 02.04.2019
comment
Сработало отлично, и действительно, в этом случае это гораздо лучший подход, чем регулярные выражения. - person Gian Luca Scoccia; 02.04.2019

Примечание

Я больше знаком с JavaScript, чем с Python, и основная логика у них одинакова (разница в синтаксисе), поэтому я написал здесь свои решения на JavaScript. Не стесняйтесь переводить на Python.

Проблема

Этот вопрос немного сложнее, чем простой однострочный скрипт или регулярное выражение, но в зависимости от конкретных требований вы можете обойтись чем-то элементарным.

Во-первых, синтаксический анализ электронной почты не сводится к одному регулярному выражению. На этом веб-сайте есть несколько примеров регулярных выражений, которые будут соответствовать "многим" электронным письмам, но объясняет компромиссы (сложность против точности) и продолжает включать стандартное регулярное выражение RFC 5322, которое теоретически должно соответствовать любому электронному письму, за которым следует абзац для почему вы не должны использовать его. Однако даже это регулярное выражение предполагает, что доменное имя, принимающее форму IP-адреса, может состоять только из кортежа из четырех целых чисел в диапазоне от 0 до 255 — это не так. т разрешить IPv6

Даже что-то такое простое, как:

{a, b}@domain.com

Может произойти сбой, поскольку технически, согласно спецификации адреса электронной почты, адрес электронной почты может содержать ЛЮБЫЕ символы ASCII, заключенные в кавычки. Ниже приведен действительный (единственный) адрес электронной почты:

"{a, b}"@domain.com

Чтобы точно разобрать электронное письмо, потребуется, чтобы вы читали символы по одной букве за раз и построили конечный автомат, чтобы отслеживать, находитесь ли вы в двойной кавычке, в фигурной скобке, перед @, после @, синтаксический анализ доменное имя, анализ IP-адреса и т. д. Таким образом, вы можете токенизировать адрес, найти токен фигурной скобки и проанализировать его независимо.

Что-то элементарное

Регулярные выражения не обеспечивают стопроцентной точности и поддержки всех электронных писем, *особенно*, если вы хотите поддерживать более одного электронного письма в одной строке. Но мы начнем с них и попробуем построить оттуда.

Вы, вероятно, пробовали регулярное выражение, например:

/\{(([^,]+),?)+\}\@(\w+\.)+[A-Za-z]+/
  • Совпадение с одной фигурной скобкой...
  • Followed by one or more instances of:
    • One or more non-comma characters...
    • После нуля или одной запятой
  • За ним следует одна закрывающая фигурная скобка...
  • За ним следует один @
  • Followed by one or more instances of:
    • One or more "word" characters...
    • За ним следует один .
  • За которым следует один или несколько буквенных символов

Это должно соответствовать чему-то примерно в форме:

{one, two}@domain1.domain2.toplevel

Это обрабатывает проверку, затем следует извлечение всех допустимых адресов электронной почты. Обратите внимание, что у нас есть два набора скобок в части имени адреса электронной почты, которые являются вложенными: (([^,]+),?). Это вызывает у нас проблему. Многие механизмы регулярных выражений не знают, как возвращать совпадения в этом случае. Посмотрите, что происходит, когда я запускаю это в 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" ]

Теперь мы можем обрезать их и получить наши имена:

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}`);
}

Несколько линий электронной почты

Здесь все становится сложнее, и нам нужно принять немного меньшую точность, если мы не хотим строить полный лексер / токенизатор. Поскольку наши электронные письма содержат запятые (в поле имени), мы не можем точно разделить запятые, если только эти запятые не заключены в фигурные скобки. С моим знанием регулярных выражений я не знаю, легко ли это сделать. Это может быть возможно с операторами просмотра вперед или назад, но кто-то другой должен будет рассказать мне об этом.

Однако с помощью регулярных выражений можно легко найти блок текста, содержащий запятую после амперсанда. Что-то вроде: @[^@{]+?,

В строке [email protected], [email protected] это будет соответствовать всей фразе @b.com,, но важно то, что это дает нам место для разделения строки. Хитрость заключается в том, чтобы узнать, как разделить вашу строку здесь. Что-то вроде этого будет работать большую часть времени:

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

У этого есть потенциальная ошибка, если у вас есть два электронных письма в списке с одним и тем же доменом:

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

Но опять же, не создавая лексер/токенизатор, мы соглашаемся с тем, что наше решение будет работать только в большинстве случаев, а не во всех.

Однако, поскольку задача разделения одной строки на несколько электронных писем проще, чем погружение в электронное письмо, извлечение имени и разбор имени: мы можем написать действительно глупый лексер только для этой части:

var inBrackets = false
var emails = "{a, b}@c.com, [email protected]"
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: [ "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]" ]

Если вы хотите попробовать реализовать полное решение для лексера/токенизатора, вы можете посмотреть на простой/тупой лексер, который я создал в качестве отправной точки. Общая идея заключается в том, что у вас есть машина состояний (в моем случае у меня было только два состояния: inBrackets и !inBrackets), и вы читаете по одной букве за раз, но интерпретируете ее по-разному в зависимости от вашего текущего состояния.

person stevendesu    schedule 01.04.2019
comment
Я выбрал решение для Python, но спасибо за подробное пошаговое объяснение. - person Gian Luca Scoccia; 02.04.2019

быстрое решение с использованием 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)

вывод в список результатов:

[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]

тест с текстовым файлом:

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)

вывод в список результатов:

[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
person Frenchy    schedule 02.04.2019
comment
Я попробовал ваше решение, и оно не сработало в случаях, когда в одной строке содержатся как сжатые электронные письма в фигурных скобках, так и обычные. Все еще может быть в более простых случаях, чем тот, с которым я столкнулся - person Gian Luca Scoccia; 02.04.2019