Элегантный структурированный текстовый файл - PullRequest
20 голосов
/ 22 октября 2008

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

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

Стенограммы генерируются www.providesupport.com и отправляются по электронной почте на учетную запись, затем я извлекаю текстовое вложение в виде простого текста из электронного письма.

Причиной синтаксического анализа файла является извлечение текста беседы на более поздний срок, а также идентификация имен посетителей и операторов, чтобы информация могла быть доступна через CRM.

Вот пример файла расшифровки:

Chat Transcript

Visitor: Random Website Visitor 
Operator: Milton
Company: Initech
Started: 16 Oct 2008 9:13:58
Finished: 16 Oct 2008 9:45:44

Random Website Visitor: Where do i get the cover sheet for the TPS report?
* There are no operators available at the moment. If you would like to leave a message, please type it in the input field below and click "Send" button
* Call accepted by operator Milton. Currently in room: Milton, Random Website Visitor.
Milton: Y-- Excuse me. You-- I believe you have my stapler?
Random Website Visitor: I really just need the cover sheet, okay?
Milton: it's not okay because if they take my stapler then I'll, I'll, I'll set the building on fire...
Random Website Visitor: oh i found it, thanks anyway.
* Random Website Visitor is now off-line and may not reply. Currently in room: Milton.
Milton: Well, Ok. But… that's the last straw.
* Milton has left the conversation. Currently in room:  room is empty.

Visitor Details
---------------
Your Name: Random Website Visitor
Your Question: Where do i get the cover sheet for the TPS report?
IP Address: 255.255.255.255
Host Name: 255.255.255.255
Referrer: Unknown
Browser/OS: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2; .NET CLR 1.1.4322; InfoPath.1; .NET CLR 2.0.50727)

Ответы [ 9 ]

12 голосов
/ 22 октября 2008

Нет, и на самом деле, для конкретного типа задачи, которую вы описываете, я сомневаюсь, что есть более «чистый» способ сделать это, чем регулярные выражения. Похоже, что ваши файлы имеют встроенные разрывы строк, поэтому, как правило, здесь мы делаем строку вашей единицей разложения, применяя регулярные выражения для каждой строки. Тем временем вы создаете небольшой конечный автомат и используете совпадения регулярных выражений для запуска переходов в этом конечном автомате. Таким образом, вы узнаете, где вы находитесь в файле, и какие типы символьных данных вы можете ожидать. Также рассмотрите возможность использования именованных групп захвата и загрузки регулярных выражений из внешнего файла. Таким образом, если формат вашего транскрипта изменится, это будет просто настроить регулярное выражение, а не писать новый специфичный для синтаксического анализа код.

11 голосов
/ 22 октября 2008

С Perl вы можете использовать Parse :: RecDescent

Это просто, и ваша грамматика будет поддерживаться позже.

6 голосов
/ 01 ноября 2009

Вот два парсера, основанные на lepl библиотеке генератора парсеров. Они оба дают одинаковый результат.

from pprint import pprint
from lepl import AnyBut, Drop, Eos, Newline, Separator, SkipTo, Space

# field = name , ":" , value
name, value = AnyBut(':\n')[1:,...], AnyBut('\n')[::'n',...]    
with Separator(~Space()[:]):
    field = name & Drop(':') & value & ~(Newline() | Eos()) > tuple

header_start   = SkipTo('Chat Transcript' & Newline()[2])
header         = ~header_start & field[1:] > dict
server_message = Drop('* ') & AnyBut('\n')[:,...] & ~Newline() > 'Server'
conversation   = (server_message | field)[1:] > list
footer_start   = 'Visitor Details' & Newline() & '-'*15 & Newline()
footer         = ~footer_start & field[1:] > dict
chat_log       = header & ~Newline() & conversation & ~Newline() & footer

pprint(chat_log.parse_file(open('chat.log')))

Более строгий парсер

from pprint import pprint
from lepl import And, Drop, Newline, Or, Regexp, SkipTo

def Field(name, value=Regexp(r'\s*(.*?)\s*?\n')):
    """'name , ":" , value' matcher"""
    return name & Drop(':') & value > tuple

Fields = lambda names: reduce(And, map(Field, names))

header_start   = SkipTo(Regexp(r'^Chat Transcript$') & Newline()[2])
header_fields  = Fields("Visitor Operator Company Started Finished".split())
server_message = Regexp(r'^\* (.*?)\n') > 'Server'
footer_fields  = Fields(("Your Name, Your Question, IP Address, "
                         "Host Name, Referrer, Browser/OS").split(', '))

with open('chat.log') as f:
    # parse header to find Visitor and Operator's names
    headers, = (~header_start & header_fields > dict).parse_file(f)
    # only Visitor, Operator and Server may take part in the conversation
    message = reduce(Or, [Field(headers[name])
                          for name in "Visitor Operator".split()])
    conversation = (message | server_message)[1:]
    messages, footers = ((conversation > list)
                         & Drop('\nVisitor Details\n---------------\n')
                         & (footer_fields > dict)).parse_file(f)

pprint((headers, messages, footers))

Выход:

({'Company': 'Initech',
  'Finished': '16 Oct 2008 9:45:44',
  'Operator': 'Milton',
  'Started': '16 Oct 2008 9:13:58',
  'Visitor': 'Random Website Visitor'},
 [('Random Website Visitor',
   'Where do i get the cover sheet for the TPS report?'),
  ('Server',
   'There are no operators available at the moment. If you would like to leave a message, please type it in the input field below and click "Send" button'),
  ('Server',
   'Call accepted by operator Milton. Currently in room: Milton, Random Website Visitor.'),
  ('Milton', 'Y-- Excuse me. You-- I believe you have my stapler?'),
  ('Random Website Visitor', 'I really just need the cover sheet, okay?'),
  ('Milton',
   "it's not okay because if they take my stapler then I'll, I'll, I'll set the building on fire..."),
  ('Random Website Visitor', 'oh i found it, thanks anyway.'),
  ('Server',
   'Random Website Visitor is now off-line and may not reply. Currently in room: Milton.'),
  ('Milton', "Well, Ok. But… that's the last straw."),
  ('Server',
   'Milton has left the conversation. Currently in room:  room is empty.')],
 {'Browser/OS': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2; .NET CLR 1.1.4322; InfoPath.1; .NET CLR 2.0.50727)',
  'Host Name': '255.255.255.255',
  'IP Address': '255.255.255.255',
  'Referrer': 'Unknown',
  'Your Name': 'Random Website Visitor',
  'Your Question': 'Where do i get the cover sheet for the TPS report?'})
6 голосов
/ 22 октября 2008

Возможно, вы захотите рассмотреть полный генератор парсера.

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

Их особенно недостаточно, если важен контекст подстроки.

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

Регулярные выражения великолепны и все такое, но если вам нужен парсер, он не заменит.

5 голосов
/ 22 октября 2008

Создать парсер ? Я не могу решить, достаточно ли регулярны ваши данные для этого, но, возможно, стоит посмотреть.

4 голосов
/ 22 октября 2008

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

Кроме того, рассмотрите возможность разбиения регулярного выражения на отдельные задачи, по одной на каждую «вещь», которую вы хотите получить. например.

visitor = text.find(/Visitor:(.*)/)
operator = text.find(/Operator:(.*)/)
body = text.find(/whatever....)

вместо

text.match(/Visitor:(.*)\nOperator:(.*)...whatever to giant regex/m) do
  visitor = $1
  operator = $2
  etc.
end

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

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

2 голосов
/ 22 октября 2008

Я использовал библиотеку классов pyParsing Пола Макгуайра, и она продолжает меня впечатлять, поскольку она хорошо документирована, ее легко начать, а правила легко настраивать и поддерживать. Кстати, правила выражены в вашем коде Python. Очевидно, что файл журнала имеет достаточную регулярность, чтобы анализировать каждую строку как отдельную единицу.

2 голосов
/ 22 октября 2008

Рассмотрите возможность использования Ragel http://www.complang.org/ragel/

Вот что питает дворнягу под капотом. Многократный синтаксический анализ строки резко замедлит процесс.

0 голосов
/ 22 октября 2008

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

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