@denislysenko
data engineer

Как распарсить первый 'слой' строки 'struct'?

Например: у меня есть такой тюпл
('food', 'struct<Milk:array<struct<id:string,type:string>>,Oil:string,batter:array<struct<id:string,type:string>>>')


первый элемент этого тюпла это название столбца, а второй это его структура. То есть, по второму элементу этого тюпла видно, что столбец 'food' содержит в себе 3 столбца: Milk, Oil и batter, как можно ли написать функцию, которая могла бы выдавать такой результат ['food.Milk', 'food.Oil', 'food.batter'] ???

То есть функция получает на вход этот тюпл, и для каждого подстолбца должна возвращать название первого элемента тюпла потом точка, а потом название подстолбца

Или же как можно распарсить такую строку
'struct<Milk:array<struct<id:string,type:string>>,Oil:string,batter:array<struct<id:string,type:string>>>'

на [food, [Milk, Oil, batter,]]

или же может есть какая нибудь библиотека, которая может такую строку
'struct<Milk:array<struct<id:string,type:string>>,Oil:string,batter:array<struct<id:string,type:string>>>'  распарсить на [Milk,  Oil, batter,]

распарсить на [Milk, Oil, batter,] ?

Также по этой строке видно что подстолбец Milk, содержит в себе array, который содержит в себе другие столбцы: id и type.
Но эти id и type не важны для меня, мне нужно только получить названия "верхних" подстолбцов

То есть, у меня есть тюпл, первый елемент которого это название столбца, который содержит в себе подстолбец или подстолбцы, которые могут тоже содержать в себе подстолбцы, (то есть первый елемент это просто название столбца), а второй елемент, это строка где первое слово struct и в этой строке лежит описание структуры этого столбца, и мне нужно по этой строке получить названия "верхних" подстолбцов.

Основная проблема чтобы распарсить эту строку заключается в этих символах '<>', не понимаю как распарсить эту строку, так как там в этой строке может быть очень сложная структура, где будет содержаться побольше количество открывающихся и закрывающихся знаков '<' и '>'

вот то что у меня есть:
def get_query(tuple):
  result = f'{tuple[0]}.*'
  return result

columns = []
for i in df.dtypes:  # здесь i это и есть наш тюпл и я хочу сделать  так, чтобы функция get_query() выдавала мне не food.* а 'food.Milk', 'food.Oil', 'food.batter'
  if 'struct' in i[1][:6]:
    columns.append(get_query(i))
  else:
     columns.append(i[0])

# в итоге этого кода columns у меня равен ['food.*', 'id', 'name', 'ppu', 'topping', 'type']
# но смог реализовать только возврат food.* , а мне вместо этого  нужно 'food.Milk', 'food.Oil', 'food.batter' 
# Но это просто пример моего кода, здесь не реализован функцияонал по получению из строки 'struct<Milk:array<struct<id:string,type:string>>,Oil:string,batter:array<struct<id:string,type:string>>>'  распарсить на [Milk,  Oil, batter,] того результата который мне нужен
# ничего толкового так и не смог написать
  • Вопрос задан
  • 99 просмотров
Решения вопроса 2
Vindicar
@Vindicar
RTFM!
Вообще очень проблематично будет такое распарсить, в том числе регулярками, так как все возможные разделители (в т.ч. "<", запятая, ">") встречаются не по одному разу на разных уровнях. Это задача того же плана, что и "проверить, является ли последовательность скобок корректной (все скобки закрыты, нет закрывающей скобки до открывающей)". Такое решается с помощью конечных автоматов со стеком.
Код навскидку не приведу - не совсем тривиальная вещь.

EDIT: вот тебе разбивка на слова, может, поможет.
def tokenize(line: str):
    token = []
    for ch in line:
        if ch.isalnum() or ch == '_':
            token.append(ch)
        else:
            if token:
                yield ''.join(token)
                token.clear()
            yield ch
    if token:
        yield ''.join(token)

data = 'struct<Milk:array<struct<id:string,type:string>>,Oil:string,batter:array<struct<id:string,type:string>>>'
tokens = list(tokenize(data))
print(tokens)


EDIT2: вроде получилось. Спёр идею из одного генератора парсеров - состояние (т.е. конструкция, которую мы обрабатываем) описывается методом класса, стек - стеком вызовов питона, переход из состояния в состояние - вызовом/возвратом из метода.
Структура данных не совсем та, которая тебе нужна - но её будет куда проще обойти.
Простыня
import typing as t
import enum


class Parser:
    @staticmethod
    def tokenize(line: str) -> t.Iterable[str]:
        token = []
        for ch in line:
            if ch.isalnum() or ch == '_':
                token.append(ch)
            else:
                if token:
                    yield ''.join(token)
                    token.clear()
                yield ch
        if token:
            yield ''.join(token)
    
    def process(self, line: str):
        tokens = list(self.tokenize(line))
        result = self.do_type(tokens)
        if tokens:
            raise ValueError('Extra tokens at the end of the line: {0!r}'.format(''.join(tokens)))
        return result
    
    def _consume(self, tokens: t.List[str], expected: t.Optional[str] = None):
        token = tokens.pop(0)
        if expected is not None and token != expected:
            raise ValueError(f'Expected {expected!r}, got {token!r}')
        return token
    
    def do_type(self, tokens: t.List[str]) -> t.Union[str, t.Dict]:
        if tokens[0] == 'struct':
            return self.do_struct(tokens)
        elif tokens[0] == 'array':
            return self.do_array(tokens)
        else:
            return self._consume(tokens)
    
    def do_struct(self, tokens: t.List[str]) -> t.Dict:
        self._consume(tokens, 'struct')
        self._consume(tokens, '<')
        result = {}
        while True:
            name, value = self.do_struct_part(tokens)
            result[name] = value
            if tokens[0] == ',':
                self._consume(tokens, ',')
            else:
                break
        self._consume(tokens, '>')
        return result
    
    def do_struct_part(self, tokens: t.List[str]) -> t.Tuple[str, t.Union[str, t.Dict]]:
        name = self._consume(tokens)
        self._consume(tokens, ':')
        value = self.do_type(tokens)
        return name, value
    
    def do_array(self, tokens: t.List[str]) -> t.Dict:
        self._consume(tokens, 'array')
        self._consume(tokens, '<')
        result = { None: self.do_type(tokens) }
        self._consume(tokens, '>')
        return result


data = 'struct<Milk:array<struct<id:string,type:string>>,Oil:string,batter:array<struct<id:string,type:string>>>'
p = Parser()
print(p.process(data))
Ответ написан
Комментировать
phaggi
@phaggi Куратор тега Python
лужу, паяю, ЭВМы починяю
Я не приходя в сознание наклепал свой велосипед на костылях, он делает что-то похожее.
Простите за чудовищно кривой код... мне очень стыдно.
spoiler
import re
from pprint import pprint


def replace_old_struct(struct):
    changes = {'struct<': 'dict(',
               'array<': 'list(',
               '>': ')',
               ':': ': ',
               ',': ', '}
    for key in changes:
        struct = struct.replace(key, changes[key])
    return struct


def set_mark(struct, patterns):
    for pattern in patterns:
        regex = re.compile(pattern)
        words = set(regex.findall(struct))
        for word in words:
            repl = f"'{word}'"
            struct = struct.replace(word, repl)
    return struct


def pair_rbracket(struct: str, rbracket):
    counter = 0
    for number, symbol in enumerate(struct):
        if symbol == '(':
            counter += 1
        elif symbol == ')':
            counter -= 1
        if counter < 0:
            return struct[:number] + rbracket + struct[number + 1:]

def pair_lbracket(struct, lbracket):
    return f'{lbracket}{struct[1:]}'

def pair_brackets(struct, struct_type='dict'):
    changes = {'dict': {'(': '{', ')': '}'},
               'list': {'(': '[', ')': ']'}}
    lbracket = changes[struct_type]['(']
    rbracket = changes[struct_type][')']
    if struct[0] == '(':
        struct = pair_lbracket(struct, lbracket)
        struct = pair_rbracket(struct, rbracket)
    return struct


def set_struct(struct, struct_type):
    structs = struct.split(struct_type, 1)
    structs[1] = pair_brackets(structs[1], struct_type)
    return ''.join(structs)


def replace_struct(struct, struct_type):
    for _ in range(struct.count(struct_type)):
        struct = set_struct(struct, struct_type)
    return struct


if __name__ == '__main__':
    data = ('food',
            'struct<Milk:array<struct<id:string,type:string>>,Oil:string,batter:array<struct<id:string,type:string>>>')
    my_struct = data[1]
    patterns = [r"([\w]+)\:", r"\: ([\w]+)\)"]
    struct = set_mark(replace_old_struct(my_struct), patterns)
    struct_types = ['dict', 'list']
    for struct_type in struct_types:
        struct = replace_struct(struct, struct_type)
    struct = eval(struct)
    pprint(struct)

Хотел рефакторить, переделать покрасивее, в класс, как у старших товарищей, но уже спатеньки хочется...
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы