Извлечение адресов электронной почты из формата академических фигурных скобок - PullRequest
0 голосов
/ 01 апреля 2019

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

{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

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

Ответы [ 3 ]

2 голосов
/ 02 апреля 2019

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)

Синтаксический анализатор поставляется с 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 голос
/ 02 апреля 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)

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

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
1 голос
/ 01 апреля 2019

Примечание

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

Выпуск

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

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

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

{a, b}@domain.com

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

"{a, b}"@domain.com

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

Нечто рудиментарное

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

Вы, вероятно, пытались использовать регулярное выражение вроде:

/\{(([^,]+),?)+\}\@(\w+\.)+[A-Za-z]+/
  • Соответствует одной фигурной скобке ...
  • Вслед за одним или несколькими случаями:
    • Один или несколько не запятых символов ...
    • С последующим нулем или одной запятой
  • За ним следует одна закрывающая фигурная скобка ...
  • сопровождается одним @
  • Вслед за одним или несколькими случаями:
    • Один или несколько символов "слова" ...
    • За ним следует один .
  • Вслед за одним или несколькими буквенными символами

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

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

Линии для нескольких электронных писем

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

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

В строке 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), и вы читаете по одному письму за раз, но интерпретируете его по-разному в зависимости от текущего состояния.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...