将文本字符串转换为XML
我有一个类似下面的文本字符串:
statistics:
time-started: Tue Feb 5 15:33:35 2013
time-sampled: Thu Feb 7 12:25:39 2013
statistic:
active: 0
interactive: 0
count: 0
up:
packets: 0
bytes: 0
down:
packets: 0
bytes: 0
我需要解析像上面这样的字符串(我需要解析的字符串实际上要大得多,这里只是给了一个例子)。我觉得解析一些元素最简单的方法就是把这个字符串转换成XML格式的字符串,然后使用xml.etree.ElementTree
来选择我想要的元素。
所以我想把上面的字符串转换成下面这样的XML字符串:
<statistics>
<time-started>Tue Feb 5 15:33:35 2013</time-started>
<time-sampled>Thu Feb 7 12:25:39 2013</time-sampled>
<statistic>
<active>0</active>
<interactive>0</interactive>
</statistic>
<count>0</count>
<up>
<packets>0</packets>
<bytes>0</bytes>
</up>
<down>
<packets>0</packets>
<bytes>0</bytes>
</down>
</statistics>
如你所见,字符串中包含了所有可以转换成XML的信息。如果有简单的方法或者模块可以做到这一点,我不想重新发明轮子。
2 个回答
0
user2050283 说得没错,这确实是 YAML 格式,这让解析变得简单。出于学习的目的,我尝试自己解析一下。期待一些反馈。
你的数据结构是层级的,像树一样。所以我们在 Python 中定义一个树,尽量简单 (参考):
from collections import defaultdict
def tree(): return defaultdict(tree)
接下来,我们在一个解析函数中使用这个树。这个函数会逐行读取,查看缩进情况,记录当前的路径(也就是面包屑导航),并尝试把一行分成键和值(如果有的话),然后填充我们的树。如果某行的缩进和之前的缩进不匹配,就会报错——这和 Python 对其源代码的处理方式类似。
def load_data(f):
doc = tree()
previous_indents = [""]
path = [""]
for line in map(lambda x: x.rstrip("\n"),
filter( is_valid_line, f)
):
line_wo_indent = line.lstrip(" ")
indent = line[:(len(line) - len(line_wo_indent))]
k, v = read_key_and_value(line_wo_indent)
if len(indent) > len(previous_indents[-1]):
previous_indents.append(indent)
path.append(k)
elif len(indent) == len(previous_indents[-1]):
path[-1] = k
else: # indent is shorter
try:
while previous_indents[-1] != indent:
previous_indents.pop()
path.pop()
except IndexError:
raise IndentationError("Indent doesn't match any previous indent.")
path[-1] = k
if v is not None:
set_leaf_value_from_path(doc, path, v)
return doc
我创建的辅助函数有:
- set_leaf_value_from_path: 接受一个树、一个路径(键的列表)和一个值。它使用递归向下遍历树,并设置路径所定义的叶子节点的值。
- read_key_and_value: 将一行按第一个 ":" 分成键和值。
- is_valid_line: 用来检查一行是否不为空或者是否以数字符号开头。
这是完整的脚本:
from collections import defaultdict
def tree(): return defaultdict(tree)
def dicts(t):
if isinstance(t, dict):
return {k: dicts(t[k]) for k in t}
else:
return t
def load_data(f):
doc = tree()
previous_indents = [""]
path = [""]
for line in map(lambda x: x.rstrip("\n"),
filter( is_valid_line, f)
):
line_wo_indent = line.lstrip(" ")
indent = line[:(len(line) - len(line_wo_indent))]
k, v = read_key_and_value(line_wo_indent)
if len(indent) > len(previous_indents[-1]):
previous_indents.append(indent)
path.append(k)
elif len(indent) == len(previous_indents[-1]):
path[-1] = k
else: # indent is shorter
try:
while previous_indents[-1] != indent:
previous_indents.pop()
path.pop()
except IndexError:
raise IndentationError("Indent doesn't match any previous indent.")
path[-1] = k
if v is not None:
set_leaf_value_from_path(doc, path, v)
return doc
def set_leaf_value_from_path(tree_, path, value):
if len(path)==1:
tree_[path[0]] = value
else:
set_leaf_value_from_path(tree_[path[0]], path[1:], value)
def read_key_and_value(line):
pos_of_first_column = line.index(":")
k = line[:pos_of_first_column].strip()
v = line[pos_of_first_column+1:].strip()
return k, v if len(v) > 0 else None
def is_valid_line(line):
if line.strip() == "":
return False
if line.lstrip().startswith("#"):
return False
return True
if __name__ == "__main__":
import cStringIO
document_str = """
statistics:
time-started: Tue Feb 5 15:33:35 2013
time-sampled: Thu Feb 7 12:25:39 2013
statistic:
active: 0
interactive: 0
count: 1
up:
packets: 2
bytes: 2
down:
packets: 3
bytes: 3
"""
f = cStringIO.StringIO(document_str)
doc = load_data(f)
from pprint import pprint
pprint(dicts(doc))
已知的限制:
- 只支持标量作为值。
- 只支持字符串标量作为值。
- 不支持多行标量。
- 注释没有按照定义实现,也就是说,注释不能在行的任何位置开始;只有以数字符号开头的行才会被视为注释。
这些只是已知的限制。我相信 YAML 的其他部分也不被支持。但这似乎足够处理你的数据了。
2
你其实是在把YAML格式的数据转换成XML格式。你可以使用PyYAML这个工具来把你的输入字符串解析成Python字典,然后再用一个XML生成器把这个字典转换成XML格式。