65.9K
CodeProject 正在变化。 阅读更多。
Home

ParseContext 2.0:更易于手工编写的解析器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2019 年 7 月 22 日

CPOL

5分钟阅读

viewsIcon

13472

downloadIcon

204

使用此即插即用源快速构建简单的解析器

引言

我编写大量的解析器,也编写解析器生成器。尽管能够通过编程方式使用一些出色的工具来生成解析器,但我仍然手工编写大约 80% 的解析代码,因为对于简单的文档来说,这样更快。我们将让这个过程变得更快。

背景

如果您曾经尝试使用 TextReaderIEnumerator<char> 来解析文档,您可能已经遇到过这些常见的问题:枚举器无法在事后检查是否到达枚举的末尾,而文本读取器的 Peek() 函数在某些源(如 NetworkStream)上不可靠。这些限制都需要额外的簿记来克服,这会使解析代码复杂化,并分散了人们对核心职责的注意力——解析输入!

另一个显著的限制是缺乏前瞻性。通常,您只能查看前面一个字符,但有时,您需要查看更远的地方才能完成解析。

在解析过程中,也没有什么可以帮助进行错误处理和报告。

我们将用一个名为 ParseContext 的类来解决这些限制。

Using the Code

ParseContext 的工作方式与 TextReader 非常相似,它将输入字符作为 int 返回。它用 -1 表示输入结束。与 TextReader 不同,它还用 -2 表示输入开始之前,用 -3 表示已释放

Advance() 的工作方式与 TextReader 类上的 Read() 函数相同。它将输入向前移动一个字符,并将结果作为 int 返回。

但是,我们最常通过 Current 属性来获取当前字符,它是一个 int,始终保存光标下的字符(或上面列出的一个负数信号),而只需使用 Advance() 来遍历文本。两者都可以正常工作。请根据您的需要选择最适合您的方法。

为了在不移动光标的情况下向前查看输入,我们提供了 Peek() 方法,该方法接受一个可选的 int lookAhead 参数,该参数指示要向前查看多少个字符。指定零只是检索光标下的字符。无论哪种情况,光标位置都保持不变。此方法对于不可查找的源(如 NetworkStream)是安全的。

为了跟踪输入光标的位置,我们有 LineColumnPositionTabWidth。前三个报告光标的位置,而最后一个应该设置为输入设备的制表符宽度。这样做可以确保在遇到制表符时正确报告 Column。默认值为 8,并且它们的工作方式与控制台窗口上的制表符停止符类似,屏幕以虚拟列布局,这意味着实际的制表符并不总是有相同的宽度。

为了进行错误报告,我们提供了 Expecting() 方法,该方法接受一个可变参数列表,代表光标下允许出现的输入列表。这可以包括 -1 来表示输入结束是允许的值之一,而传递零个参数表示接受除输入结束之外的任何内容。如果当前字符未被接受,则会抛出 ExpectingException,并报告详细的错误消息。

我们有一个基于 StringBuilder 的内部捕获缓冲区,可用于保存我们已解析的输入。这由 CaptureBuffer 属性表示。在 ParseContext 上,我们还提供了 CaptureCurrent()(捕获光标下的当前字符,如果存在),ClearCapture()(清除捕获缓冲区),以及 GetCapture()(检索捕获缓冲区的所有或部分内容作为 string)。

最后,为了创建 ParseContect,我们有几个 static 方法:Create() 方法接受 stringchar 数组CreateFrom() 方法接受文件名或 TextReader,以及 CreateFromUrl() 方法接受 URL。

请记住,ParseContext 实现了 IDisposable,并且在完成解析后,如果不是从 stringchar 数组加载文本,则必须处理它。通常会使用 Close() 方法,但我将其移除,因为有太多以“C”开头的成员,这使 IntelliSense 变得繁琐。请使用 C# 中的 Dispose()using 关键字。

此外,我还包含了一个可拆分的类,其中包含几个辅助函数,包括 TrySkipWhitespace()TryReadUntil() 以及各种其他用于常见解析任务的方法。

这位于 ParseContext.Helpers.cs 中。它不是基础功能所必需的,但在实践中,它几乎可以在任何解析器中发挥作用。

我希望展示该类的功能,而不会受到太多无关内容的干扰,因此演示项目仅解析和最小化了一个大型 JSON 文件。

JSON 语法非常简单,您可以在 json.org 上查看所有详细信息。

下面的代码是解析实现。大致上,它分为 3 个主要部分,代表 JSON 树的主要组件:JSON 对象、JSON 数组和 JSON 值。

请注意,在调用此解析中的子函数时,无需担心传递当前字符——这在使用枚举器或文本读取器时很难做到。总的来说,这使得各种解析函数的分离更加清晰,并且编写起来更容易。注释应解释具体细节。

static object _ParseJson(ParseContext pc)
{
    pc.TrySkipWhiteSpace();
    switch(pc.Current)
    {
        case '{':
            return _ParseJsonObject(pc);
        case '[':
            return _ParseJsonArray(pc);
        default:
            return _ParseJsonValue(pc);
    }
}
static IDictionary<string, object> _ParseJsonObject(ParseContext pc)
{
    // a JSON {} object - our objects are dictionaries
    var result = new Dictionary<string, object>();
    pc.TrySkipWhiteSpace();
    pc.Expecting('{');
    pc.Advance();
    pc.Expecting(); // expecting anything other than end of input
    while ('}' != pc.Current && -1 != pc.Current) // loop until } or end
    {
        pc.TrySkipWhiteSpace();
        // _ParseJsonValue parses any scalar value, but we only want 
        // a string so we check here that there's a quote mark to 
        // ensure the field will be a string.
        pc.Expecting('"');
        var fn = _ParseJsonValue(pc);
        pc.TrySkipWhiteSpace();
        pc.Expecting(':');
        pc.Advance();
        // add the next value to the dictionary
        result.Add(fn, _ParseJson(pc));
        pc.TrySkipWhiteSpace();
        pc.Expecting('}', ',');
        // skip commas
        if (',' == pc.Current) pc.Advance();
    }
    // make sure we're positioned on the end
    pc.Expecting('}');
    // ... and read past it
    pc.Advance();
    return result;
}
static IList<object> _ParseJsonArray(ParseContext pc)
{
    // a JSON [] array - our arrays are lists
    var result = new List<object>();
    pc.TrySkipWhiteSpace();
    pc.Expecting('[');
    pc.Advance();
    pc.Expecting(); // expect anything but end of input
    // loop until end of array or input
    while (-1!=pc.Current && ']'!=pc.Current) 
    {
        pc.TrySkipWhiteSpace();
        // add the next item
        result.Add(_ParseJson(pc));
        pc.TrySkipWhiteSpace();
        pc.Expecting(']', ',');
        // skip the comma
        if (',' == pc.Current) pc.Advance();
    }
    // ensure we're on the final position
    pc.Expecting(']');
    // .. and read past it
    pc.Advance();
    return result;
}
static string _ParseJsonValue(ParseContext pc) {
    // parses a scalar JSON value, represented as a string
    // strings are returned quotes and all, with escapes 
    // embedded
    pc.TrySkipWhiteSpace();
    pc.Expecting(); // expect anything but end of input
    pc.ClearCapture();
    if ('\"' == pc.Current)
    {
        pc.CaptureCurrent();
        pc.Advance();
        // reads until it finds a quote
        // using \ as an escape character
        // and consuming the final quote 
        // at the end
        pc.TryReadUntil('\"', '\\', true);
        // return what we read
        return pc.GetCapture();
    }
    pc.TryReadUntil(false,',', '}', ']', ' ', '\t', '\r', '\n', '\v', '\f');
    return pc.GetCapture();
}

历史

  • 2019 年 7 月 21 日 - 初始提交
© . All rights reserved.