我有许多带有标题、副标题、子副标题的标记文件。
我有兴趣将它们解析成JSON,将每个标题的文本和"副标题"分开。。
例如,我有下面的markdown文件,我想把它解析成这样的形式:
outer1
outer2
# title 1
text1.1
## title 1.1
text1.1.1
# title 2
text 2.1
:
{
"text": [
"outer1",
"outer2"
],
"inner": [
{
"section": [
{
"title": "title 1",
"inner": [
{
"text": [
"text1.1"
],
"inner": [
{
"section": [
{
"title": "title 1.1",
"inner": [
{
"text": [
"text1.1.1"
]
}
]
}
]
}
]
}
]
},
{
"title": "title 2",
"inner": [
{
"text": [
"text2.1"
]
}
]
}
]
}
]
}
为了进一步说明这种需要,请注意内部标题是如何嵌套在外部标题中,而第二个外部标题不是。
我尝试使用pyparser
来解决这个问题,但在我看来,它无法实现这一点,因为要获得部分"标题2";与title 1"处于同一水平我需要一些"计数逻辑"要检查数字或"#">
这是pyparser
的可表达性问题吗?是否有其他类型的解析器可以实现这一点?
我可以在纯python中实现这个,但我想做一些更好的。
这是我目前的pyparsing
实现,它不像上面解释的那样工作:
section = pp.Forward()("section")
inner_block = pp.Forward()("inner")
start_section = pp.OneOrMore(pp.Word("#"))
title_section = line
title = start_section.suppress() + title_section('title')
line = pp.Combine(
pp.OneOrMore(pp.Word(pp.unicode.Latin1.printables), stop_on=pp.LineEnd()),
join_string=' ', adjacent=False)
text = ~title + pp.OneOrMore(line, stop_on=(pp.LineEnd() + pp.FollowedBy("#")))
inner_block << pp.Group(section | (text('text') + pp.Optional(section.set_parse_action(foo))))
section << pp.Group(title + pp.Optional(inner_block))
markdown = pp.OneOrMore(inner_block)
test = """
out1
out2
# title 1
text1.1
# title 2
text2.1
"""
res = markdown.parse_string(test, parse_all=True).as_dict()
test_eq(res, dict(
inner=[
dict(
text = ["out1", "out2"],
section=[
dict(title="title 1", inner=[
dict(
text=["text1.1"]
),
]),
dict(title="title 2", inner=[
dict(
text=["text2.1"]
),
]),
]
)
]
))
我对这个问题采取了稍微不同的方法,使用scan_string
而不是parse_string
,并在scan_string
循环中而不是在解析器本身的解析操作中进行更多的数据结构管理和存储。
scan_string
扫描输入,对于找到的每个匹配项,返回匹配的令牌作为ParseResults
,以及源字符串中匹配项的开始和结束位置。
从导入开始,我定义了一个标题行表达式:
import pyparsing as pp
# define a pyparsing expression that will match a line with leading '#'s
title = pp.AtLineStart(pp.Word("#")) + pp.rest_of_line
为了准备按标题收集数据,我定义了一个title_stack
列表和一个last_end
int来跟踪找到的最后一个标题的结尾(这样我们就可以切出解析的最后一个标题的内容)。我用一个表示文件开始的假条目初始化这个堆栈:
# initialize title_stack with level-0 title at the start of the file
title_stack.append([0, '<start of file>'])
下面是使用scan_string:
的扫描循环for t, start, end in title.scan_string(sample):
# save content since last title in the last item in title_stack
title_stack[-1].append(sample[last_end:start].lstrip("n"))
# add a new entry to title_stack
marker, title_content = t
level = len(marker)
title_stack.append([level, title_content.lstrip()])
# update last_end to the end of the current match
last_end = end
# add trailing text to the final parsed title
title_stack[-1].append(sample[last_end:])
此时,title_stack包含一个包含3个元素的列表、标题级别、标题文本和标题的正文文本。以下是示例降价的输出:
[[0, '<start of file>', 'outer1nouter2nn'],
[1, 'title 1', 'text1.1nn'],
[2, 'title 1.1', 'text1.1.1nn'],
[3, 'title 1.1.1', 'text 1.1.1nn'],
[1, 'title 2', 'text 2.1']]
从这里,您应该能够遍历该列表并将其转换为所需的树结构。