Расширенный Python Regex: как оценить и извлечь вложенные списки и числа из многострочной строки? - PullRequest
0 голосов
/ 29 ноября 2018

Я пытался отделить элементы от многострочной строки:

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

Ответы [ 2 ]

0 голосов
/ 29 ноября 2018

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

Код

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]]

Пояснение

Сначала мы импортируем более новый модуль regex и функцию literal_eval из модуля ast, который потребуетсяпреобразовать найденные совпадения в реальный код.Более новый модуль regex обладает гораздо большей мощностью, чем модуль re, и обеспечивает рекурсивную функциональность и мощную (но не очень известную) конструкцию DEFINE для подпрограмм.

Мы определяем два типа элементов:первый - «простой» элемент, последний - «элемент списка», см. демонстрацию на regex101.com .

На втором шаге мы добавляем кавычки для тех элементов, которым они нужны (то есть элементы без кавычек, начинающиеся с символа).Все вводится в literal_eval, а затем сохраняется в понимании списка.

0 голосов
/ 29 ноября 2018

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

Одним из способов создания парсера является 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

Существуют также генераторы синтаксического анализатора PEG для Python, например, 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() выполняет ту же задачу, что и функция list() в приведенном выше коде Javascript, другие функции также имеют свои эквиваленты в подходе 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']]]
]

В долгосрочной перспективе я бы ожидал такой подходбыть более понятным и гибким, чем хакерство регулярных выражений.Например, было легко добавить явную поддержку NaN и логических значений (которых нет в приведенном выше peg.js-Solution - там они анализируются как строки).

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