跳转到主要内容

使用类型让解析变得有趣...

项目描述

Latest Version License Python Versions CI LINTER Coverage

flexparser

为什么要编写另一个解析器?在开发此项目时,我也问过自己同样的问题。显然,已经有非常优秀的解析器,但我希望尝试用另一种方式编写它们。

想法很简单。您为需要解析的每种内容类型(在此称为ParsedStatement)编写一个类。每个类都应该有一个from_string构造函数。我们广泛使用了typing模块来使输出结构易于使用且更少出错。

例如

from dataclasses import dataclass

import flexparser as fp

@dataclass(frozen=True)
class Assigment(fp.ParsedStatement):
    """Parses the following `this <- other`
    """

    lhs: str
    rhs: str

    @classmethod
    def from_string(cls, s):
        lhs, rhs = s.split("<-")
        return cls(lhs.strip(), rhs.strip())

(使用冻结的数据类不是必需的,但很方便。作为数据类,您可以免费获得init、str、repr等。作为冻结的、类似于不可变的,使它们更容易推理)

在某些情况下,您可能希望通知解析器,他的类不适用于解析该语句。

@dataclass(frozen=True)
class Assigment(fp.ParsedStatement):
    """Parses the following `this <- other`
    """

    lhs: str
    rhs: str

    @classmethod
    def from_string(cls, s):
        if "<-" not in s:
            # This means: I do not know how to parse it
            # try with another ParsedStatement class.
            return None
        lhs, rhs = s.split("<-")
        return cls(lhs.strip(), rhs.strip())

您可能还需要指出这是正确的 ParsedStatement,但某些地方并不正确

@dataclass(frozen=True)
class InvalidIdentifier(fp.ParsingError):
    value: str


@dataclass(frozen=True)
class Assigment(fp.ParsedStatement):
    """Parses the following `this <- other`
    """

    lhs: str
    rhs: str

    @classmethod
    def from_string(cls, s):
        if "<-" not in s:
            # This means: I do not know how to parse it
            # try with another ParsedStatement class.
            return None
        lhs, rhs = (p.strip() for p in s.split("<-"))

        if not str.isidentifier(lhs):
            return InvalidIdentifier(lhs)

        return cls(lhs, rhs)

将此内容放入 source.txt

one <- other
2two <- new
three <- newvalue
one == three

然后运行以下代码

parsed = fp.parse("source.txt", Assigment)
for el in parsed.iter_statements():
    print(repr(el))

将产生以下输出

BOF(start_line=0, start_col=0, end_line=0, end_col=0, raw=None, content_hash=Hash(algorithm_name='blake2b', hexdigest='37bc23cde7cad3ece96b7abf64906c84decc116de1e0486679eb6ca696f233a403f756e2e431063c82abed4f0e342294c2fe71af69111faea3765b78cb90c03f'), path=PosixPath('/Users/grecco/Documents/code/flexparser/examples/in_readme/source1.txt'), mtime=1658550284.9419456)
Assigment(start_line=1, start_col=0, end_line=1, end_col=12, raw='one <- other', lhs='one', rhs='other')
InvalidIdentifier(start_line=2, start_col=0, end_line=2, end_col=11, raw='2two <- new', value='2two')
Assigment(start_line=3, start_col=0, end_line=3, end_col=17, raw='three <- newvalue', lhs='three', rhs='newvalue')
UnknownStatement(start_line=4, start_col=0, end_line=4, end_col=12, raw='one == three')
EOS(start_line=5, start_col=0, end_line=5, end_col=0, raw=None)

结果是包含 ParsedStatementParsingError 的集合(分别由 BOFEOS 表示文件的开始和流的结束。另一种情况,它可以从 BOR 开始,这意味着资源的开始,它用于解析与包一起提供的 Python 资源)。

请注意,有两个正确解析的语句(Assigment),发现一个错误(InvalidIdentifier)和一个未知(UnknownStatement)。

很酷,对吧?只需编写一个 from_string 方法,输出数据结构即可产生一个可用的解析对象结构。

现在该做什么呢?假设我们想要支持等价比较。只需这样做

@dataclass(frozen=True)
class EqualityComparison(fp.ParsedStatement):
    """Parses the following `this == other`
    """

    lhs: str
    rhs: str

    @classmethod
    def from_string(cls, s):
        if "==" not in s:
            return None
        lhs, rhs = (p.strip() for p in s.split("=="))

        return cls(lhs, rhs)

parsed = fp.parse("source.txt", (Assigment, Equality))
for el in parsed.iter_statements():
    print(repr(el))

然后再次运行它

BOF(start_line=0, start_col=0, end_line=0, end_col=0, raw=None, content_hash=Hash(algorithm_name='blake2b', hexdigest='37bc23cde7cad3ece96b7abf64906c84decc116de1e0486679eb6ca696f233a403f756e2e431063c82abed4f0e342294c2fe71af69111faea3765b78cb90c03f'), path=PosixPath('/Users/grecco/Documents/code/flexparser/examples/in_readme/source1.txt'), mtime=1658550284.9419456)
Assigment(start_line=1, start_col=0, end_line=1, end_col=12, raw='one <- other', lhs='one', rhs='other')
InvalidIdentifier(start_line=2, start_col=0, end_line=2, end_col=11, raw='2two <- new', value='2two')
Assigment(start_line=3, start_col=0, end_line=3, end_col=17, raw='three <- newvalue', lhs='three', rhs='newvalue')
EqualityComparison(start_line=4, start_col=0, end_line=4, end_col=12, raw='one == three', lhs='one', rhs='three')
EOS(start_line=5, start_col=0, end_line=5, end_col=0, raw=None)

您需要将某些语句分组在一起:欢迎来到 Block。这个构造允许您分组

class Begin(fp.ParsedStatement):

    @classmethod
    def from_string(cls, s):
        if s == "begin":
            return cls()

        return None

class End(fp.ParsedStatement):

    @classmethod
    def from_string(cls, s):
        if s == "end":
            return cls()

        return None

class ParserConfig:
    pass

class AssigmentBlock(fp.Block[Begin, Assigment, End, ParserConfig]):
    pass

parsed = fp.parse("source.txt", (AssigmentBlock, Equality))

运行代码

BOF(start_line=0, start_col=0, end_line=0, end_col=0, raw=None, content_hash=Hash(algorithm_name='blake2b', hexdigest='37bc23cde7cad3ece96b7abf64906c84decc116de1e0486679eb6ca696f233a403f756e2e431063c82abed4f0e342294c2fe71af69111faea3765b78cb90c03f'), path=PosixPath('/Users/grecco/Documents/code/flexparser/examples/in_readme/source1.txt'), mtime=1658550284.9419456)
UnknownStatement(start_line=1, start_col=0, end_line=1, end_col=12, raw='one <- other')
UnknownStatement(start_line=2, start_col=0, end_line=2, end_col=11, raw='2two <- new')
UnknownStatement(start_line=3, start_col=0, end_line=3, end_col=17, raw='three <- newvalue')
UnknownStatement(start_line=4, start_col=0, end_line=4, end_col=12, raw='one == three')
EOS(start_line=5, start_col=0, end_line=5, end_col=0, raw=None)

请注意,现在有很多 UnknownStatement,因为我们指示解析器只查找块内的赋值。所以将您的文本文件改为

begin
one <- other
2two <- new
three <- newvalue
end
one == three

然后再次尝试

BOF(start_line=0, start_col=0, end_line=0, end_col=0, raw=None, content_hash=Hash(algorithm_name='blake2b', hexdigest='3d8ce0051dcdd6f0f80ef789a0df179509d927874f242005ac41ed886ae0b71a30b845b9bfcb30194461c0ef6a3ca324c36f411dfafc7e588611f1eb0269bb5a'), path=PosixPath('/Users/grecco/Documents/code/flexparser/examples/in_readme/source2.txt'), mtime=1658550707.1248093)
Begin(start_line=1, start_col=0, end_line=1, end_col=5, raw='begin')
Assigment(start_line=2, start_col=0, end_line=2, end_col=12, raw='one <- other', lhs='one', rhs='other')
InvalidIdentifier(start_line=3, start_col=0, end_line=3, end_col=11, raw='2two <- new', value='2two')
Assigment(start_line=4, start_col=0, end_line=4, end_col=17, raw='three <- newvalue', lhs='three', rhs='newvalue')
End(start_line=5, start_col=0, end_line=5, end_col=3, raw='end')
EqualityComparison(start_line=6, start_col=0, end_line=6, end_col=12, raw='one == three', lhs='one', rhs='three')
EOS(start_line=7, start_col=0, end_line=7, end_col=0, raw=None)

到目前为止,我们已经使用 parsed.iter_statements 遍历了所有解析语句。但是,让我们看看 parsed,这是一个 ParsedProject 类型的对象。它是一个字典的薄包装,将文件映射到解析内容。因为我们提供了一个文件,并且它不包含链接,所以我们的 parsed 对象包含一个元素。键是 None,表示文件 'source.txt' 从根位置(None)加载。内容是一个具有以下属性的 ParsedSourceFile 对象

  • path:源文件的完整路径

  • mtime:源文件的修改时间

  • content_hash:序列化内容的哈希值

  • config:可以提供给解析器的额外参数(见下文)。

ParsedSource(
    parsed_source=parse.<locals>.CustomRootBlock(
        opening=BOF(start_line=0, start_col=0, end_line=0, end_col=0, raw=None, content_hash=Hash(algorithm_name='blake2b', hexdigest='3d8ce0051dcdd6f0f80ef789a0df179509d927874f242005ac41ed886ae0b71a30b845b9bfcb30194461c0ef6a3ca324c36f411dfafc7e588611f1eb0269bb5a'), path=PosixPath('/Users/grecco/Documents/code/flexparser/examples/in_readme/source2.txt'), mtime=1658550707.1248093),
        body=(
            Block.subclass_with.<locals>.CustomBlock(
                opening=Begin(start_line=1, start_col=0, end_line=1, end_col=5, raw='begin'),
                body=(
                    Assigment(start_line=2, start_col=0, end_line=2, end_col=12, raw='one <- other', lhs='one', rhs='other'),
                    InvalidIdentifier(start_line=3, start_col=0, end_line=3, end_col=11, raw='2two <- new', value='2two'),
                    Assigment(start_line=4, start_col=0, end_line=4, end_col=17, raw='three <- newvalue', lhs='three', rhs='newvalue')
                ),
                closing=End(start_line=5, start_col=0, end_line=5, end_col=3, raw='end')),
            EqualityComparison(start_line=6, start_col=0, end_line=6, end_col=12, raw='one == three', lhs='one', rhs='three')),
        closing=EOS(start_line=7, start_col=0, end_line=7, end_col=0, raw=None)),
    config=None
)

需要注意几点

  1. 我们在不知道的情况下使用了一个块。 RootBlock 是一种特殊的块类型,它自动以文件开始和结束。

  2. openingbodyclosing 会自动注解为可能的 ParsedStatement(加上 ParsingError),因此大多数 IDE 中的自动完成功能可以正常工作。

  3. 对于定义的 ParsedStatement 也是如此(我们使用 dataclass 的原因)。这使得使用实际的解析结果变得非常方便。

  4. 那个讨厌的 subclass_with.<locals> 是因为我们使用 Block.subclass_with 时动态构建了一个类。您可以通过在代码中显式地子类化 Block 来消除它(这实际上对于序列化非常有用)(见下文)。

多个源文件

大多数项目内部都有多个源文件,这些文件相互连接。一个文件可能引用另一个需要解析的文件(例如,c 中的 #include 语句)。 flexparser 提供了一个专门为此目的而设计的 IncludeStatement 基类。

@dataclass(frozen=True)
class Include(fp.IncludeStatement):
    """A naive implementation of #include "file"
    """

    value: str

    @classmethod
    def from_string(cls, s):
        if s.startwith("#include "):
            return None

        value = s[len("#include "):].strip().strip('"')

        return cls(value)

    @propery
    def target(self):
        return self.value

唯一的区别是您需要实现一个 target 属性,该属性返回此语句所指的文件名或资源。

自定义语句化

statementi … 是什么?flexparser 通过尝试使用已知的类之一解析每个语句。因此,公平地问一下,在这个上下文中语句是什么,以及您如何配置它以满足您的需求。一个文本文件被分割成非重叠的字符串,称为 语句。解析工作如下

  1. 每个文件都被分割成语句(可以是单行或多行)。

  2. 每个语句都会使用上下文可用且返回 ParsedStatementParsingError 的第一个 ParsedStatement 或 Block 子类进行解析

您可以通过向 parse 函数提供两个参数来自定义如何分割每一行成为语句

  • strip_spaces (bool):表示在尝试解析之前必须删除前导和尾随空格。(默认:True)

  • delimiters (dict):表示如何对每一行进行子分割。(默认:不分割)

一个分隔符的例子可能是 {";": (fp.DelimiterInclude.SKIP, fp.DelimiterAction.CONTINUE)},这告诉语句化器(抱歉)当遇到“;”时,应该开始新的语句。DelimiterMode.SKIP 表示“;”不应添加到上一个语句或下一个语句中。其他有效值是 SPLIT_AFTERSPLIT_BEFORE,用于将分隔符字符附加到上一个或下一个语句。第二个元素告诉语句化器(再次抱歉)接下来要做什么:有效值有:CONTINUECAPTURE_NEXT_TIL_EOLSTOP_PARSING_LINESTOP_PARSING

这对于注释很有用。例如,{"#": (fp.DelimiterMode.WITH_NEXT, fp.DelimiterAction.CAPTURE_NEXT_TIL_EOL))} 告诉语句化器(它不再好笑了)在第一个“#”之后停止分割并捕获所有内容。

这允许

## This will work as a single statement
# This will work as a single statement #
# This will work as # a single statement #
a = 3 # this will produce two statements (a=3, and the rest)

显式块类

class AssigmentBlock(fp.Block[Begin, Assigment, End]):
    pass

class EntryBlock(fp.RootBlock[Union[AssigmentBlock, Equality]]):
    pass

parsed = fp.parse("source.txt", EntryBlock)

自定义解析

在某些情况下,您可能希望将一些配置细节留给用户。我们为此有方法!不是重写 from_string,而是重写 from_string_and_config。第二个参数是一个对象,可以将其提供给解析器,然后解析器将其传递给每个 ParsedStatement 类。

@dataclass(frozen=True)
class NumericAssigment(fp.ParsedStatement):
    """Parses the following `this <- other`
    """

    lhs: str
    rhs: numbers.Number

    @classmethod
    def from_string_and_config(cls, s, config):
        if "==" not in s:
            # This means: I do not know how to parse it
            # try with another ParsedStatement class.
            return None
        lhs, rhs = s.split("==")
        return cls(lhs.strip(), config.numeric_type(rhs.strip()))

class Config:

    numeric_type = float

parsed = fp.parse("source.txt", NumericAssigment, Config)

该项目作为 Pint(Python 单位包)的一部分开始。

请参阅 AUTHORS 获取维护者列表。

要查看每个版本的项目的重要变更列表,请参阅 CHANGES

项目详细信息


下载文件

下载适合您平台的自定义文件。如果您不确定选择哪一个,请了解更多关于 安装包 的信息。

源分发

flexparser-0.3.1.tar.gz (31.4 kB 查看哈希值)

上传于 源代码

构建分发

flexparser-0.3.1-py3-none-any.whl (27.3 kB 查看哈希值)

上传于 Python 3

由以下机构支持