Гибкие стратегии анализа текста
проблема
Я пытаюсь найти гибкий способ анализа содержимого электронной почты. Ниже приведен пример фиктивного текста письма, с которым я работаю. Я также хотел бы избежать регулярных выражений, если это вообще возможно. Тем не менее, в этот момент моего процесса решения проблем я начинаю думать, что это неизбежно. Обратите внимание, что это всего лишь небольшая часть полного электронного письма. Мне нужно разобрать каждое поле (например, номер билета, сотовый телефон) в соответствующие им типы данных. Наконец, некоторые поля не обязательно присутствуют в электронном письме (в моем текущем решении показано ниже, почему это проблема).
Header Code:EMERGENCY
Ticket No: 123456789 Seq. No: 2
Update of:
Original Call Date: 01/02/2011 Time: 11:17:03 AM OP: 1102
Second Call Date: 01/02/2011 Time: 12:11:00 AM OP:
Company: COMPANY NAME
Contact: CONTACT NAME Contact Phone: (111)111-1111
Secondary Contact: SECONDARY CONTACT
Alternate Contact: Altern. Phone:
Best Time to Call: AFTER 4:30P Fax No: (111)111-1111
Cell Phone: Pager No:
Caller Address: 330 FOO
FOO AVENUE 123
Текущее решение
Для этого простого примера я успешно смог разобрать большинство полей с помощью функции ниже.
private T BetweenOperation<T>(string emailBody, string start, string end)
{
var culture = StringComparison.InvariantCulture;
int startIndex =
emailBody.IndexOf(start, culture) + start.Length;
int endIndex =
emailBody.IndexOf(end, culture);
int length = endIndex - startIndex;
if (length < 0) return default(T);
return (T)Convert.ChangeType(
emailBody.Substring(startIndex, length).Trim(),
typeof(T));
}
По сути, моя идея состояла в том, чтобы я мог разобрать содержимое между двумя полями. Например, я мог бы сделать код заголовка, выполнив
// returns "EMERGENCY"
BetweenOperation<string>("email content", "Header Code:", "Ticket No:")
Этот подход, однако, имеет много недостатков. Один большой недостаток в том, что end
поле не всегда присутствует. Как вы можете видеть, есть некоторые похожие ключи с идентичными ключевыми словами, которые не совсем правильно анализируются, такие как "Контакт" и "Вторичный контакт". Это заставляет синтаксический анализатор получать слишком много информации. Кроме того, если мое конечное поле отсутствует, я получу непредсказуемый результат. Наконец, я могу разобрать целые строки, чтобы затем передать его BetweenOperation<T>
используя это.
private string LineOperation(string startWithCriteria)
{
string[] emailLines = EmailBody.Split(new[] { '\n' });
return
emailLines.Where(emailLine => emailLine.StartsWith(startWithCriteria))
.FirstOrDefault();
}
Мы бы использовали LineOperation
в некоторых случаях, когда имя поля не является уникальным (например, время) и передать результат в BetweenOperation<T>
,
Вопрос
Как можно разобрать содержимое показанное выше по ключам. Например, ключи "Код заголовка" и "Сотовый телефон". Обратите внимание, что я не думаю, что синтаксический анализ основан на пробелах табуляции, потому что некоторые поля могут быть длиной в несколько строк (например, адрес вызывающего абонента) или вообще не содержать значения (например, альтернативный телефон).
Спасибо.
4 ответа
Один из способов решения этой проблемы заключается в том, чтобы сначала выполнить поиск всех ключей по всему тексту. То есть, построить массив, который выглядит так:
"Header Code:",1
"Contact Phone:",233
"Cell Phone:",-1 // not there
Если вы сортируете этот массив по позиции, то вы знаете, где искать вещи. То есть вы будете знать, какие поля следуют за каждым.
Вам придется что-то делать с дубликатами (например, "Время:" и "Время:" в датах звонков). И вам придется разрешить "Контакт:" и "Вторичный контакт:", хотя это должно быть довольно легко.
Если вы делаете это стандартными строковыми операциями (т.е. IndexOf
), это будет несколько неэффективно, потому что вам придется искать во всем тексте все вхождения каждой строки. Трудно ли сказать, что это проблема для вас. Зависит от того, сколько из них вам нужно сделать.
Если это станет проблемой, вы, вероятно, захотите создать средство сравнения строк Aho-Corasick или что-то подобное. Или вы могли бы создать большое уродливое регулярное выражение:
"(Header Code:)|(Contact Phone:)|(Cell Phone)"
... и т. д. Возможно, с именованными снимками, чтобы вы знали, что захватываете. Это должно работать достаточно хорошо, хотя это может быть трудно поддерживать.
По моему мнению, я бы проанализировал его по определенной последовательности, и после этого изменил бы ваше тело письма соответствующим образом.
Конкретная последовательность
Contact: CONTACT NAME Contact Phone: (111)111-1111
Secondary Contact: SECONDARY CONTACT
Alternate Contact:
Последовательность поиска полей должна начинаться со слов, которые не являются подмножествами других ключевых слов в ваших "Полях" (например, для контактов последовательность должна быть "Вторичный контакт:", "Альтернативный контакт:", а затем, наконец, "Контакт".:")
Измените ваше тело письма, если вы нашли необходимую информацию о поле, вам нужно будет изменить тело письма, чтобы удалить ее. Разбор по определенной последовательности гарантирует (я надеюсь), что у вас не будет всей проблемы несоответствия, так как вы удаляете подмножества последними.
Теперь существует также проблема конечного ключевого поля. Поскольку конечное поле не всегда гарантировано (и я не уверен, что они всегда будут в определенном порядке), вам придется пройтись по всем полям ваших ключевых слов, вернуть индекс и определить ближайшее ключевое слово на основе индекса,
Мне приходилось делать подобные вещи еще в тот день, читая отчеты из базы данных Pick. Если ваши поля основаны на позициях, вы можете просто создать XML-схему вашего сообщения электронной почты:
<message>
<line0>
<element name="Header Code" start="0" end="MAX" type="string"/>
<!-- MAX Indicates whole line -->
</line0>
<line1>
<element name="Ticket No" start="0" end="20" type="string"/>
<element name="Seq. No" start="22" end="40" type="int" />
</line1>
</message>
Затем для разбора электронной почты вы должны прочитать все текстовые строки текста. Для каждой строки (начиная с 0) в схеме вы найдете сущность "строка" + номер индекса.
Создайте временную строку. Элемент foreach в элементе "line" + index выполняет подстроку на всей строке, начиная с значений начала и конца, определенных в элементе элемента....
Сделайте Convert на подстроке, основанной на типе элемента. Сохранить сущность в объект или что-то.
Вы даже можете стать более креативным, сгруппировав различные строки + индексные объекты в своей схеме с помощью классов:
<message>
<header>
<line0>
...
</line0>
</header>
</message>
Сначала я бы разделил почту на строки, используя, например, StringReader, разбирая строку за раз, перепрыгивая через совершенно пустую строку. Поскольку метка, которую вы ищете, является пометкой, переберите потенциальную метку в каждой строке, и, если вы найдете случай, извлеките нужную часть (вы можете использовать для этого пробелы). Не знаю, как с помощью регулярных выражений не вариант, но если бы они использовали на предварительной линии будет работать как очарование.