Парсинг строк на Python 3.x

Я хочу написать парсер строк, но я реально не знаю, какой модуль использовать и как это реализовать
То есть я хочу получить из, например, hello --world -N -w 5 --Num 5 3 2 и т. д. список ["hello", "big-flag:world", "flag:N", ["flag:w", "5"], ["big-flag:Num", "5"], "3", "2"]
Если ещё разделение по пробелам понятно, то как сделать флаги и другие элементы - не очень


Ответы (1 шт):

Автор решения: Shamus Rezol

fargparse

Сделал программу для разбора аргументов командной строки под ваши запросы. Получилась она не маленькой, но рабочей. По ссылке выше я разбил ее на несколько файлов и вообще поместил в отдельный модуль. Ниже приведен аналогичный код одним файлом.

class FToken(object):
    """Единица лексического разбора; абстрактный базовый класс.

    Токен имеет свой тип T, по которому его можно идентифицировать, и
    свое значение value.

    Для использования вам необходимо определить класс токена и как
    параметр передать его тип.

    class MyToken(FToken, type="MyType"):
    
    В этом классе необходимо переопределить статические методы can_parse
    и parse, принимающие один аргумент - экземпляр FParser.

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

    class MyToken(FToken, type="MyType"):
        @staticmethod
        def can_parse(parser):
            return True
        // ...
        
    class TEXT(FToken, type="text"):
        // ...
    """
    T = "unknown"

    def __init__(self, value=None):
        """Конструктор.

        @param value Какое угодно значение токена.
        """
        self.value = value

    def __str__(self):
        """Возвращает интерпретацию токена как строку."""
        return self.T

    _childs = list()
    def __init_subclass__(cls, type: str="unknown"):
        """Инициализирует и регистрирует наследников."""
        super().__init_subclass__()
        cls.T = type
        cls._childs.append(cls)

    @staticmethod
    def can_parse(parser):
        """Проверяет, возможно ли пролексировать этот токен.
        
        @param parser Экземпляр FParser.
        @return Возвращает True, если возможно; иначе, False.
        """
        return False

    @classmethod
    def parse(cls, parser):
        """Лексирует этот токен.

        @param parser Экземпляр FParser.
        @return Возвращает полученный токен или None.
        """
        raise NotImplementedError()

#''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''#

class FTWhitespaces(FToken, type="whitespaces"):

    @staticmethod
    def can_parse(parser):
        return (parser.char.isspace())

    @classmethod
    def parse(cls, parser):
        def _():
            return parser.char.isspace()
        parser.parse_by_condition(_)
        return None

class FTParam(FToken, type="param"):

    def __str__(self):
        return self.T + ":" + self.value

    @staticmethod
    def can_parse(parser):
        return (parser.char in "'\"" or parser.char.isalnum())

    @classmethod
    def parse(cls, parser):
        value = str()
        if parser.char in "'\"":
            pair = parser.char
            parser.advance()
            def _():
                return (parser.char != pair)
            value = parser.parse_by_condition(_)
            parser.eat(pair)
        else:
            value = parser.parse_keyword()
        return cls(value)

class FTBigFlag(FToken, type="big-flag"):

    def __str__(self):
        return self.T + ":" + self.value

    @staticmethod
    def can_parse(parser):
        return (parser.char == '-' and parser.peek() == '-')

    @classmethod
    def parse(cls, parser):
        parser.eat('--')
        key = parser.parse_keyword()
        return cls(key)

class FTFlag(FToken, type="flag"):

    def __str__(self):
        return self.T + ":" + self.value

    @staticmethod
    def can_parse(parser):
        return (parser.char == '-')

    @classmethod
    def parse(cls, parser):
        parser.eat('-')
        key = parser.parse_keyword()
        return cls(key)

#''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''#

class FParser(object):
    """Парсер строк аргументов.
    """

    def __init__(self, arguments: str):
        self._tokens = list()

        self.data = arguments
        self.index = -1
        self.char = None
        if len(arguments) != 0:
            self.advance()
            self._parse()
            self._dispose()

    def advance(self, n: int=1):
        """Продвижение вперед.

        Продвигает переменные вперед; переход к следующему символу.

        @param n На сколько символов продвинуться.
        """
        self.index += n

        if self.index >= 0 and self.index < len(self.data):
            self.char = self.data[self.index]
        else:
            self.char = None

    def eat(self, victim: str):
        """Поедает ключевую фразу.

        Если ожидаемая и фактическая фраза расходятся, то вызывается
        синтаксическое исключение.
        """
        for char in victim:
            if self.char != char:
                raise SyntaxError("Unexcepted syntax error.")
            self.advance()

    def peek(self):
        """Предпросмотр следующего символа.

        @return Возвращает следующий символ или None.
        """
        self.advance()
        ch = self.char
        self.advance(-1)
        return ch

    def parse_by_condition(self, condition):
        """Получает ключевую фразу.

        Ключевая фраза состоит из любых символов, добавляемых, если
        функция condition возвращает истину (True).

        Пример использования смотрите в коде FParser.parse_keyword.

        @param condition Функция-условие, отвечающая за остановку ключевой фразы. Для этого должна вернуть False.

        @return Возвращает ключевую фразу.
        """
        keyword = str()
        while self.char != None and condition() == True:
            keyword += self.char
            self.advance()
        return keyword

    def parse_keyword(self):
        """Получает ключевое слово.

        @return Возвращает ключевое слово.
        """
        def _():
            return not self.char.isspace()
        return self.parse_by_condition(_)

    def _parse(self):
        while self.char != None:
            index = self.index
            for token_cls in FToken._childs:
                if token_cls.can_parse(self):
                    self._tokens.append(token_cls.parse(self))
                    break;
            if index == self.index:
                '''
                Что произошло?
                 - был вызван token_cls.parse, но он ни шиша не сделал.
                 - ни один token_cls.can_parse не вернул True.
                Что делать?
                 - Вызовем абстрактное исключение лексирования.
                '''
                print(self._tokens)
                raise SyntaxError(f"Неверный синтаксис на позиции {self.index + 1}")

    def _dispose(self):
        self.index = None
        self.char  = None
        self.data  = None
        # Примечание: self._tokens удаляется при getter'е свойства result.

    @property
    def result(self):
        """Выдает результат лексирования.

        Анализирует полученные токены и превращает их в конечный результат.
        """
        if '_result' in dir(self):
            return self._result
        result = list()
        require_param = False
        for token in self._tokens:
            if token == None:
                continue # whitespaces и другой мусор.

            if require_param == True:
                if not isinstance(token, FTParam):
                    break # Go to raise
                result[len(result) - 1].append(token.value)
                require_param = False
                continue

            if isinstance(token, FTParam):
                result.append(token.value)
            elif isinstance(token, (FTBigFlag, FTFlag)):
                if token.value in ("w", "Num"):
                    result.append([str(token), ])
                    require_param = True
                else:
                    result.append(str(token))
            else:
                '''
                Программист добавил токены, но не добавил способ их
                обработки в конечный результат.
                '''
                raise NotImplementedError()
        if require_param == True:
            raise Exception("Флаг с параметром не имеет параметра.")

        _tokens = None
        self._result = result
        return result

#''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''#

import os
def GetArgsStr():
    result = str()
    for arg in os.sys.argv:
        if ' ' in arg:
            result += "'" + arg + "'"
        else:
            result += arg
        result += ' '
    return result.rstrip(' ')

def main():
    arguments = GetArgsStr()
    #print("Command line: \"" + arguments + "\"")
    parser = FParser(arguments)
    print(parser.result)

if __name__ == "__main__":
    main()

Пример командной строки запуска скрипта:

C:\Users\Shamus_Rezol\Desktop\fargparse>python example.py --world -N -w 5 --Num 5 3 2

Выходные данные:

Command line: "example.py --world -N -w 5 --Num 5 3 2"
['example.py', 'big-flag:world', 'flag:N', ['flag:w', '5'], ['big-flag:Num', '5'], '3', '2']

Примечание

Создание экземпляра FParser и первое получение результатов - места возможного возникновения исключений.

obj = FParser("example.py --world -N -w 5 --Num 5 3 2")
obj.result

Я не учел, что параметр может начинаться со специального символа '/'. Исправить это возможно в tokens.py, FTParam, can_parse. Однако с параметрами вида 'C:/Users' проблем не будет.

Возможно, не учел я и много других мелочей.

→ Ссылка