Сгруппируйте несколько разделов (совпадений) с помощью Pyparsing

Я не могу понять, как сгруппировать ноль или более повторяющихся разделов в тексте с помощью pyparsing. Другими словами, я хочу объединить несколько совпадений в один именованный набор результатов. Обратите внимание, я хочу использовать pyparsing, так как у меня много разделов с разными правилами.

from pyparsing import *    

input_text = """
Projects
project a created in c#

Education
university of college

Projects
project b created in python
"""

project_marker = LineStart() + Literal('Projects') + LineEnd()
education_marker = LineStart() + Literal('Education') + LineEnd()
markers = project_marker ^ education_marker

project_section = Group(
    project_marker + SkipTo(markers | stringEnd).setResultsName('project')
).setResultsName('projects')
education_section = Group(
    education_marker + SkipTo(markers | stringEnd).setResultsName('education')
).setResultsName('educations')
sections = project_section ^ education_section

text = StringStart() + SkipTo(sections | StringEnd())
doc = Optional(text) + ZeroOrMore(sections)
result = doc.parseString(input_text)

print(result)
# ['', ['Projects', '\n', 'project a created in c#'], ['Education', '\n', 'virginia tech'], ['Projects', '\n', 'project b created in python']]
print(result.projects)
# ['Projects', '\n', 'project b created in python']
print(result.projects[0].project)
# AttributeError: 'str' object has no attribute 'project'

person scottwernervt    schedule 17.08.2017    source источник
comment
Может ли раздел «Проекты» содержать более одной строки с описанием проекта? И аналогично для разделов «Образование»?   -  person Bill Bell    schedule 17.08.2017
comment
При вызовах setResultsName в разделах project_section и education_section добавляйте listAllMatches=True. Тогда я думаю, что ваш код будет работать как есть.   -  person PaulMcG    schedule 18.08.2017
comment
@PaulMcG Добавление listAllMatches=True группирует результаты вместе, спасибо!   -  person scottwernervt    schedule 18.08.2017


Ответы (2)


Вот мой предварительный ответ, не то чтобы я им горжусь. Я взял часть этого из https://stackoverflow.com/a/5824309/131187.

>>> import pyparsing as pp
>>> pp.ParserElement.setDefaultWhitespaceChars(" \t")
>>> EOL = pp.LineEnd().suppress()
>>> keyword = pp.Or([pp.Keyword('Projects'), pp.Keyword('Education')])
>>> line = pp.LineStart() + pp.NotAny(keyword) + pp.SkipTo(pp.LineEnd(), failOn=pp.LineStart()+pp.LineEnd()) + EOL
>>> lines = pp.OneOrMore(line)
>>> section = pp.Or([pp.Keyword('Projects'), pp.Keyword('Education')])('section') + EOL + lines('lines')
>>> sections = pp.OneOrMore(section)
>>> r = sections.parseString(input_text)

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

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

Честно говоря, это было бы проще сделать без pyparsing. Прочитайте строку, обратите внимание, является ли это ключевым словом. Если это так, запомните это. Затем, пока вы не прочитаете другое ключевое слово, просто поместите все строки, которые вы прочитали в словаре, под самым последним ключевым словом.

>>> repr(r)
"(['Projects', 'project 1', 'project 2', 'project 3', '', 'Education', 'institution 1', 'institution 2', 'institution 3', 'institution 4', '', 'Projects', 'assignment 5', 'assignment 8', 'assignment 10', ''], {'lines': [(['project 1', 'project 2', 'project 3', ''], {}), (['institution 1', 'institution 2', 'institution 3', 'institution 4', ''], {}), (['assignment 5', 'assignment 8', 'assignment 10', ''], {})], 'section': ['Projects', 'Education', 'Projects']})"
>>> evil_r = eval(repr(r))
>>> evil_r
(['Projects', 'project 1', 'project 2', 'project 3', '', 'Education', 'institution 1', 'institution 2', 'institution 3', 'institution 4', '', 'Projects', 'assignment 5', 'assignment 8', 'assignment 10', ''], {'lines': [(['project 1', 'project 2', 'project 3', ''], {}), (['institution 1', 'institution 2', 'institution 3', 'institution 4', ''], {}), (['assignment 5', 'assignment 8', 'assignment 10', ''], {})], 'section': ['Projects', 'Education', 'Projects']})
>>> evil_r[1]['lines']
[(['project 1', 'project 2', 'project 3', ''], {}), (['institution 1', 'institution 2', 'institution 3', 'institution 4', ''], {}), (['assignment 5', 'assignment 8', 'assignment 10', ''], {})]
>>> evil_r[1]['section']
['Projects', 'Education', 'Projects']
>>> from collections import defaultdict
>>> section_info = defaultdict(list)
>>> for k, kind in enumerate(evil_r[1]['section']):
...     section_info[kind].extend(evil_r[1]['lines'][k][0][:-1])
>>> for section in section_info:
...     section, section_info[section]
...     
('Education', ['institution 1', 'institution 2', 'institution 3', 'institution 4'])
('Projects', ['project 1', 'project 2', 'project 3', 'assignment 5', 'assignment 8', 'assignment 10'])

EDIT: Или вы можете сделать это. Требует уборки. По крайней мере, он не использует ничего необычного.

>>> input_text = open('temp.txt').read()
>>> import pyparsing as pp
>>> pp.ParserElement.setDefaultWhitespaceChars(" \t")
>>> from collections import defaultdict
>>> class Accum:
...     def __init__(self):
...         self.current_section = None
...         self.result = defaultdict(list)
...     def __call__(self, s):
...         if s[0] in ['Projects', 'Education']:
...             self.current_section = s[0]
...         else:
...             self.result[self.current_section].extend(s[:-1])
... 
>>> accum = Accum()
>>> EOL = pp.LineEnd().suppress()
>>> keyword = pp.Or([pp.Keyword('Projects'), pp.Keyword('Education')])
>>> line = pp.LineStart() + pp.NotAny(keyword) + pp.SkipTo(pp.LineEnd(), failOn=pp.LineStart()+pp.LineEnd()) + EOL
>>> lines = pp.OneOrMore(line)
>>> section = pp.Or([pp.Keyword('Projects'), pp.Keyword('Education')]).setParseAction(accum) + EOL + lines.setParseAction(accum)
>>> sections = pp.OneOrMore(section)
>>> r = sections.parseString(input_text)
>>> accum.result['Education']
['institution 1', 'institution 2', 'institution 3', 'institution 4']
>>> accum.result['Projects']
['project 1', 'project 2', 'project 3', 'assignment 5', 'assignment 8', 'assignment 10']
person Bill Bell    schedule 17.08.2017
comment
Добавлен еще один подход. - person Bill Bell; 18.08.2017
comment
Пожалуйста, используйте '|' или операторы '^' для объединения альтернативных выражений, что немного очищает грамматику: keyword = pp.Keyword('Projects') | pp.Keyword('Education'). Pyparsing включает restOfLine как простой способ прочитать все до следующей новой строки, поэтому строка упрощается до: line = pp.LineStart() + pp.NotAny(keyword) + pp.restOfLine + EOL. Последнее, что вам нужно, это сгруппировать каждое ключевое слово и связанные с ним строки, используя класс Group: section = pp.Group(keyword('section') + EOL + lines('lines')). - person PaulMcG; 18.08.2017
comment
Наконец, распечатайте результаты, используя print(r.dump()). Это показывает, как вы можете получить доступ к каждому проанализированному разделу, такому как for section_data in r: print section_data.section. Нет необходимости в repr или eval. Подробнее о классе ParseResults pyparsing читайте на странице pythonhosted.org/pyparsing/pyparsing.ParseResults-class.html. . - person PaulMcG; 18.08.2017
comment
@PaulMcG: Спасибо! Я не смог заставить .Group работать так, как хотел, и не смог найти .restofline в документе. (Должен был опубликовать запрос на SO о последнем.) - person Bill Bell; 18.08.2017
comment
@PaulMcG: За r.asList я получаю [['Projects', 'project 1', 'project 2', 'project 3', ''], ['Education', 'institution 1', 'institution 2', 'institution 3', 'institution 4', ''], ['Projects', 'assignment 5', 'assignment 8', 'assignment 10', '']], а это не то, к чему я стремился. Я надеялся найти способ заставить pyparsing поместить все элементы проекта в один список, а все элементы образования — в отдельный список, как в моем втором сценарии. Есть ли способ, который не слишком загадочен для новичка? - person Bill Bell; 18.08.2017
comment
@PaulMcG: я надеялся, что вы вникнете в этот вопрос. :) - person Bill Bell; 18.08.2017
comment
Посмотрите на мой комментарий к исходному вопросу - OP использует имена результатов в общих разделах «Образование» и «Проекты», но по умолчанию в pyparsing будет просто сохраняться последний (аналогично назначению ключей в dict). Если добавить аргумент 'listAllMatches=True' в setResultsName, проекты и учебные материалы будут собраны вместе для вашего. Поскольку вы используете сокращение (name) для setResultsName, попробуйте просто добавить '*' в конце имени результатов. - person PaulMcG; 18.08.2017
comment
@PaulMcG: Еще раз спасибо. - person Bill Bell; 18.08.2017
comment
Спасибо за помощь и за знакомство с restOfLine. - person scottwernervt; 18.08.2017

Благодаря @PaulMcG решение состоит в том, чтобы добавить listAllMatches=True к setResultsName, см. https://pythonhosted.org/pyparsing/pyparsing.ParserElement-class.html#setResultsName.

project_marker = LineStart() + Literal('Projects') + LineEnd()
education_marker = LineStart() + Literal('Education') + LineEnd()
markers = project_marker ^ education_marker

project_section = Group(
    project_marker + SkipTo(markers | stringEnd).setResultsName('project')
).setResultsName('projects', listAllMatches=True)
education_section = Group(
    education_marker + SkipTo(markers | stringEnd).setResultsName('education')
).setResultsName('educations', listAllMatches=True)
sections = project_section ^ education_section

text = StringStart() + SkipTo(sections | StringEnd())
doc = Optional(text) + ZeroOrMore(sections)
result = doc.parseString(input_text)
person scottwernervt    schedule 18.08.2017