[英]Advanced Python Regex: how to evaluate and extract nested lists and numbers from a multiline string?
我试图将元素与多行字符串分开:
lines = '''c0 c1 c2 c3 c4 c5
0 10 100.5 [1.5, 2] [[10, 10.4], [c, 10, eee]] [[a , bg], [5.5, ddd, edd]] 100.5
1 20 200.5 [2.5, 2] [[20, 20.4], [d, 20, eee]] [[a , bg], [7.5, udd, edd]] 200.5'''
我的目标是获得一个列表lst
这样的:
# first value is index
lst[0] = ['c0', 'c1', 'c2', 'c3', 'c4','c5']
lst[1] = [0, 10, 100.5, [1.5, 2], [[10, 10.4], ['c', 10, 'eee']], [['a' , 'bg'], [5.5, 'ddd', 'edd']], 100.5 ]
lst[2] = [1, 20, 200.5, [2.5, 2], [[20, 20.4], ['d', 20, 'eee']], [['a' , 'bg'], [7.5, 'udd', 'edd']], 200.5 ]
到目前为止我的尝试是这样的:
import re
lines = '''c0 c1 c2 c3 c4 c5
0 10 100.5 [1.5, 2] [[10, 10.4], [c, 10, eee]] [[a , bg], [5.5, ddd, edd]] 100.5
1 20 200.5 [2.5, 2] [[20, 20.4], [d, 20, eee]] [[a , bg], [7.5, udd, edd]] 200.5'''
# get n elements for n lines and remove empty lines
lines = lines.split('\n')
lines = list(filter(None,lines))
lst = []
lst.append(lines[0].split())
for i in range(1,len(lines)):
change = re.sub('([a-zA-Z]+)', r"'\1'", lines[i])
lst.append(change)
for i in lst[1]:
print(i)
如何修复正则表达式?
更新
测试数据集
data = """
orig shifted not_equal cumsum lst
0 10 NaN True 1 [[10, 10.4], [c, 10, eee]]
1 10 10.0 False 1 [[10, 10.4], [c, 10, eee]]
2 23 10.0 True 2 [[10, 10.4], [c, 10, eee]]
"""
# Gives: ValueError: malformed node or string:
data = """
Name Result Value
0 Name1 5 2
1 Name1 5 3
2 Name2 11 1
"""
# gives same error
data = """
product value
0 A 25
1 B 45
2 C 15
3 C 14
4 C 13
5 B 22
"""
# gives same error
data = '''
c0 c1
0 10 100.5
1 20 200.5
'''
# works perfect
正如评论中所指出的,这个任务与正则表达式无关。 正则表达式从根本上说无法处理嵌套结构。 你需要的是一个解析器。
创建解析器的方法之一是PEG ,它允许您以声明性语言设置令牌列表及其相互之间的关系。 然后将此解析器定义转换为可以处理所描述的输入的实际解析器。 解析成功后,您将获得一个树结构,其中所有项都已正确嵌套。
出于演示目的,我使用了JavaScript实现peg.js,它有一个在线演示页面 ,您可以根据某些输入对解析器进行实时测试。 这个解析器定义:
{
// [value, [[delimiter, value], ...]] => [value, value, ...]
const list = values => [values[0]].concat(values[1].map(i => i[1]));
}
document
= line*
line "line"
= value:(item (whitespace item)*) whitespace? eol { return list(value) }
item "item"
= number / string / group
group "group"
= "[" value:(item (comma item)*) whitespace? "]" { return list(value) }
comma "comma"
= whitespace? "," whitespace?
number "number"
= value:$[0-9.]+ { return +value }
string "string"
= $([^ 0-9\[\]\r\n,] [^ \[\]\r\n,]*)
whitespace "whitespace"
= $" "+
eol "eol"
= [\r]? [\n] / eof
eof "eof"
= !.
可以理解这种输入:
c0 c1 c2 c3 c4 c5 0 10 100.5 [1.5, 2] [[10, 10.4], [c, 10, eee]] [[a , bg], [5.5, ddd, edd]] 1 20 200.5 [2.5, 2] [[20, 20.4], [d, 20, eee]] [[a , bg], [7.5, udd, edd1]]
并生成此对象树(JSON表示法):
[
["c0", "c1", "c2", "c3", "c4", "c5"],
[0, 10, 100.5, [1.5, 2], [[10, 10.4], ["c", 10, "eee"]], [["a", "bg"], [5.5, "ddd", "edd"]]],
[1, 20, 200.5, [2.5, 2], [[20, 20.4], ["d", 20, "eee"]], [["a", "bg"], [7.5, "udd", "edd1"]]]
]
即
然后,您的程序可以处理此树结构。
上面的例子可以用node.js将您的输入转换为JSON。 以下最小JS程序接受来自STDIN的数据并将解析后的结果写入STDOUT:
// reference the parser.js file, e.g. downloaded from https://pegjs.org/online
const parser = require('./parser');
var chunks = [];
// handle STDIN events to slurp up all the input into one big string
process.stdin.on('data', buffer => chunks.push(buffer.toString()));
process.stdin.on('end', function () {
var text = chunks.join('');
var data = parser.parse(text);
var json = JSON.stringify(data, null, 4);
process.stdout.write(json);
});
// start reading from STDIN
process.stdin.resume();
将它保存为text2json.js
或类似的东西,并将一些文本重定向(或管道):
# input redirection (this works on Windows, too)
node text2json.js < input.txt > output.json
# common alternative, but I'd recommend input redirection over this
cat input.txt | node text2json.js > output.json
还有用于Python的PEG解析器生成器,例如https://github.com/erikrose/parsimonious 。 解析器创建语言在实现之间有所不同,因此上面只能用于peg.js,但原理完全相同。
编辑我已经挖到Parsimonious并在Python代码中重新创建了上述解决方案。 方法是相同的,解析器语法是相同的,只有一些微小的语法变化。
from parsimonious.grammar import Grammar
from parsimonious.nodes import NodeVisitor
grammar = Grammar(
r"""
document = line*
line = whitespace? item (whitespace item)* whitespace? eol
item = group / number / boolean / string
group = "[" item (comma item)* whitespace? "]"
comma = whitespace? "," whitespace?
number = "NaN" / ~"[0-9.]+"
boolean = "True" / "False"
string = ~"[^ 0-9\[\]\r\n,][^ \[\]\r\n,]*"
whitespace = ~" +"
eol = ~"\r?\n" / eof
eof = ~"$"
""")
class DataExtractor(NodeVisitor):
@staticmethod
def concat_items(first_item, remaining_items):
""" helper to concat the values of delimited items (lines or goups) """
return first_item + list(map(lambda i: i[1][0], remaining_items))
def generic_visit(self, node, processed_children):
""" in general we just want to see the processed children of any node """
return processed_children
def visit_line(self, node, processed_children):
""" line nodes return an array of their processed_children """
_, first_item, remaining_items, _, _ = processed_children
return self.concat_items(first_item, remaining_items)
def visit_group(self, node, processed_children):
""" group nodes return an array of their processed_children """
_, first_item, remaining_items, _, _ = processed_children
return self.concat_items(first_item, remaining_items)
def visit_number(self, node, processed_children):
""" number nodes return floats (nan is a special value of floats) """
return float(node.text)
def visit_boolean(self, node, processed_children):
""" boolean nodes return return True or False """
return node.text == "True"
def visit_string(self, node, processed_children):
""" string nodes just return their own text """
return node.text
DataExtractor
负责遍历树并从节点中提取数据,返回字符串,数字,布尔值或NaN的列表。
concat_items()
函数执行与上面Javascript代码中的list()
函数相同的任务,其他函数也在peg.js方法中具有等价物,除了peg.js将它们直接集成到解析器定义中并且Parsimonious期望在一个单独的类中的定义,所以它相对来说有点讽刺,但也不是太糟糕。
用法,假设一个名为“data.txt”的输入文件,也反映了JS代码:
de = DataExtractor()
with open("data.txt", encoding="utf8") as f:
text = f.read()
tree = grammar.parse(text)
data = de.visit(tree)
print(data)
输入:
orig shifted not_equal cumsum lst 0 10 NaN True 1 [[10, 10.4], [c, 10, eee]] 1 10 10.0 False 1 [[10, 10.4], [c, 10, eee]] 2 23 10.0 True 2 [[10, 10.4], [c, 10, eee]]
输出:
[ ['orig', 'shifted', 'not_equal', 'cumsum', 'lst'], [0.0, 10.0, nan, True, 1.0, [[10.0, 10.4], ['c', 10.0, 'eee']]], [1.0, 10.0, 10.0, False, 1.0, [[10.0, 10.4], ['c', 10.0, 'eee']]], [2.0, 23.0, 10.0, True, 2.0, [[10.0, 10.4], ['c', 10.0, 'eee']]] ]
从长远来看,我希望这种方法比正则表达式hackery更易于维护和灵活。 添加对NaN和布尔值的明确支持(例如上面的peg.js-Solution没有 - 它们被解析为字符串)很容易。
老实说,我不同意用正则表达式做不可能。 有人可能会更精确地说明单独使用正则表达式是不可能的。
请参阅以下代码,其中包含您想要的内容并进一步阅读说明。
import regex as re
from ast import literal_eval
data = """
c0 c1 c2 c3 c4 c5
0 10 100.5 [1.5, 2] [[10, 10.4], [c, 10, eee]] [[a , bg], [5.5, ddd, edd]] 100.5
1 20 200.5 [2.5, 2] [[20, 20.4], [d, 20, eee]] [[a , bg], [7.5, udd, edd]] 200.5
"""
# regex definition
rx = re.compile(r'''
(?(DEFINE)
(?<item>[.\w]+)
(?<list>\[(?:[^][\n]*|(?R))+\])
)
(?&list)|(?&item)
''', re.X)
# unquoted item
item_rx = re.compile(r"(?<!')\b([a-z][.\w]*)\b(?!')")
# afterwork party
def afterwork(match):
match = item_rx.sub(r"'\1'", match)
return literal_eval(match)
matrix = [
[afterwork(item.group(0)) for item in rx.finditer(line)]
for line in data.split("\n")
if line
]
print(matrix)
这产生了
[['c0', 'c1', 'c2', 'c3', 'c4', 'c5'], [0, 10, 100.5, [1.5, 2], [[10, 10.4], ['c', 10, 'eee']], [['a', 'bg'], [5.5, 'ddd', 'edd']], 100.5], [1, 20, 200.5, [2.5, 2], [[20, 20.4], ['d', 20, 'eee']], [['a', 'bg'], [7.5, 'udd', 'edd']], 200.5]]
首先,我们从ast
模块导入更新的regex
模块和函数literal_eval
,这将是在实际代码中转换找到的匹配所需的。 较新的regex
模块比re
模块具有更多的功能,并且为子例程提供递归功能和功能强大(但不是很熟知)的DEFINE
构造。
我们定义了两种类型的元素,第一种是“简单”项,后者是“列表项”,请参阅regex101.com上的演示 。
在第二步中,我们为需要它们的元素添加引号(即,以字符开头的不带引号的元素)。 所有内容都输入literal_eval
,然后保存在列表理解中。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.