Separando dados da representação de string de objetos, com extras adicionados

Oct 15 2020

Dada uma representação de string de dados, desejo extrair as informações em seu objeto correspondente.

Contudo,

Se a string tiver "|" separadores, então, eles devem ser considerados opções e precisam ser escolhidos aleatoriamente.

Se os dados da string têm números mostrados como um intervalo "1-10", um valor aleatório deve ser escolhido entre o intervalo. Deve também preservar o tipo de dados numéricos, ou seja, int ou float

Ie

"(1-3,1,1)" retorna (1, 1, 1), (2, 1, 1) ou (3, 1, 1)

"(0,2-0,4,1,1)" retorna (0,2, 1, 1), (0,3, 1, 1) ou (0,4, 1, 1)

"foo | bar | foobar" retorna "foo", "bar" ou "foobar"

"[1-2,1,2] | foo | bar | [1,8-10,99]" pode retornar:

"foo", "bar", [1, 1, 2], [2, 1, 2], [1, 8, 99], [1, 9, 99] ou [1, 10, 99]

Isso é o que eu tenho e funciona bem. Mas não posso deixar de pensar que isso poderia ser alcançado de uma forma mais concisa. Deixe-me saber o que eu poderia ter feito melhor.

import re
import random
import ast

def randomize_by_pipe(st_value):
    """
    Used to split strings with the pipe character and randomly choose and option.

    :param: st_value - (str)

    """
    if not st_value is None:
        st_arr = st_value.split("|")
        random.shuffle(st_arr)
        return st_arr[0]
    else:
        return st_value

def randomise_range(text):
    if text is None:
        return text
    else:
        matches = re.findall("\d*\.*\d*-{1}\d*\.*\d*",text)
        
        for match in matches:
            startingPos = 0
            position = text.find(match, startingPos)
            while True:
                position = text.find(match, startingPos)
                if position > -1:
                    txt = text[position:position+len(match)]
                    txt = rand_no_from_string(txt)

                    new_text = text[0:position+len(match)].replace(match,str(txt))
                    text = new_text + text[position+len(match):]
                else:
                    break

        try:
            return ast.literal_eval(text)
        except ValueError:
            return text

def rand_no_from_string(txt):
    
    is_int = False
    txt_arr = txt.split("-")
    num_arr = [float(x) for x in txt_arr]
    if int(num_arr[0]) == num_arr[0]:
        mul = 1
        is_int = True
    else:
        #new section to deal with the decimals
        mul = 10 ** len(str(num_arr[0]).split(".")[1])
        num_arr = [x*mul for x in num_arr]
    
    if num_arr[0] > num_arr[1]:
        num_arr[1], num_arr[0] = num_arr[0], num_arr[1]
        
    val = random.randint(num_arr[0],num_arr[1])/mul
    return int(val) if is_int else val
    

Correr com:

text="(108-100,0.25-0.75,100)|Foo|Bar|[123,234,234-250]"
randomise_range(randomize_by_pipe(text))

Respostas

6 hjpotter92 Oct 15 2020 at 19:52

Sugestão de tipo

Em vez de ter helpdocs declarando os tipos de parâmetros de função, por que não ir com dicas de tipo?

Complexidade

Atualmente, seu código tem muitas partes móveis. Você define 2 funções diferentes para analisar os dados, e ambas precisam ser chamadas em cadeia. Isso deve ser feito por uma única função de análise.

Deixe o analisador obter o texto dos dados, então o analisador deve lidar primeiro com a análise sintática pipee, posteriormente, usando os intervalos numéricos.

Seleção de uma lista

Você randomize_by_pipeembaralha a lista e seleciona o 0º valor. Em vez disso, você pode deixar random.choicefazer o trabalho.

range análise

Acho que a análise de alcance pode ser melhorada um pouco. Que tal o seguinte fluxo:

  1. Remova [e ]do texto fornecido.
  2. Separar de ,.
  3. Para cada seção da divisão, tente analisar como float(ou int, dependendo do seu conjunto de dados)
  4. Em caso de erro de conversão float, rand_no_from_stringobtenha um valor.

regex

Você tem uma regex, mas não está fazendo uso completo / elegante dela. Em vez de correspondências, você pode agrupar os resultados e operar nesses grupos. O próprio padrão também pode ser um pouco otimizado :

\d+(?:\.\d+)?-\d+(?:\.\d+)?

Uma reescrita, por exemplo:

from re import sub, Match
from random import choice, randint


def randomise_range(match: Match):
    given_range = match.group(0).split("-")
    low, high = map(float, given_range)
    if low > high:
        low, high = high, low
    if low.is_integer():
        return str(randint(int(low), int(high)))
    multiplier = 10 ** len(given_range[0].split(".")[-1])
    low = int(low * multiplier)
    high = int(high * multiplier)
    return str(randint(low, high) / multiplier)


def extract_range(text: str = None):
    if not text:
        return text
    return sub(r"\d+(?:\.\d+)?-\d+(?:\.\d+)?", randomise_range, text)


def parse(text: str = None):
    if not text:
        return text
    selection = choice(text.split("|"))
    if selection[0] in ('[', '('):
        return extract_range(selection)
    return selection


if __name__ == "__main__":
    examples = (
        "(1-3,1,1)",
        "(0.2-0.4,1,1)",
        "foo|bar|foobar",
        "(108-100,0.25-0.75,100)|Foo|Bar|[123,234,234-250]",
        "[1-2,1,2]|foo|bar|[1,8-10,99]",
    )
    for text in examples:
        print(parse(text))
2 Reinderien Oct 16 2020 at 04:12

Aqui está uma implementação cujo principal esforço, quando comparado com a sua implementação e também com a resposta aceita, é a separação da análise e da execução. Não está claro se isso é importante para você, mas geralmente é um bom design e é provavelmente mais rápido para reexecutar depois de analisado:

import re
from numbers import Real
from random import randint, choice
from typing import Union, Callable


class Pattern:
    chunk_pat = re.compile(
        r'([^|]+)'   # group: within a chunk, at least one non-pipe character
        r'(?:'       # non-capturing group for termination character
            r'\||$' # pipe, or end of string r')' # end of termination group ) option_pat = re.compile( r'([^,]+)' # at least one non-comma character in an option r'(?:' # non-capturing group for termination character r',|$'  # comma, or end of string
        r')'        # end of termination group
    )

    range_pat = re.compile(
        r'^'            # start
        r'('
            r'[0-9.]+'  # first number group
        r')-('
            r'[0-9.]+'  # second number group
        r')'
        r'$'            # end
    )

    def __init__(self, pattern: str):
        chunk_strs = Pattern.chunk_pat.finditer(pattern)

        self.tree = tuple(
            self.parse_chunk(chunk[1])
            for chunk in chunk_strs
        )

    @staticmethod
    def choose_in_group(group: tuple) -> tuple:
        for option in group:
            if isinstance(option, Callable):
                yield option()
            else:
                yield option

    def choose(self) -> Union[str, tuple]:
        group = choice(self.tree)

        if isinstance(group, tuple):
            return tuple(self.choose_in_group(group))
        return group

    @staticmethod
    def precis_parse(as_str: str) -> (Real, int):
        if '.' in as_str:
            return float(as_str), len(as_str.rsplit('.', 1)[-1])
        return int(as_str), 0

    @classmethod
    def make_choose(cls, start: Real, end: Real, precis: int):
        if precis:
            factor = 10**precis
            start = int(start * factor)
            end = int(end * factor)
            def choose():
                return randint(start, end) / factor

        else:
            def choose():
                return randint(start, end)

        return choose

    @classmethod
    def parse_options(cls, options: str):
        for option in cls.option_pat.finditer(options):
            range_match = cls.range_pat.match(option[1])
            if range_match:
                start_str, end_str = range_match.groups()
                start, start_n = cls.precis_parse(start_str)
                end, end_n = cls.precis_parse(end_str)
                yield cls.make_choose(start, end, max(start_n, end_n))
            else:
                # Fall back to one raw string
                yield option[1]

    @classmethod
    def parse_chunk(cls, chunk: str):
        if (
            chunk[0] == '(' and chunk[-1] == ')' or
            chunk[0] == '[' and chunk[-1] == ']'
        ):
            return tuple(cls.parse_options(chunk[1:-1]))

        # Fall back to returning the raw string
        return chunk


def test():
    p = Pattern('foo|(bar,3-4,50,6.3-7,92-99)')

    for _ in range(20):
        print(p.choose())

if __name__ == '__main__':
    test()