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





5.00/5 (2投票s)
使用此即插即用源快速构建简单的解析器
引言
我编写大量的解析器,也编写解析器生成器。尽管能够通过编程方式使用一些出色的工具来生成解析器,但我仍然手工编写大约 80% 的解析代码,因为对于简单的文档来说,这样更快。我们将让这个过程变得更快。
背景
如果您曾经尝试使用 TextReader
或 IEnumerator<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
)是安全的。
为了跟踪输入光标的位置,我们有 Line
、Column
、Position
和 TabWidth
。前三个报告光标的位置,而最后一个应该设置为输入设备的制表符宽度。这样做可以确保在遇到制表符时正确报告 Column
。默认值为 8
,并且它们的工作方式与控制台窗口上的制表符停止符类似,屏幕以虚拟列布局,这意味着实际的制表符并不总是有相同的宽度。
为了进行错误报告,我们提供了 Expecting()
方法,该方法接受一个可变参数列表,代表光标下允许出现的输入列表。这可以包括 -1
来表示输入结束是允许的值之一,而传递零个参数表示接受除输入结束之外的任何内容。如果当前字符未被接受,则会抛出 ExpectingException
,并报告详细的错误消息。
我们有一个基于 StringBuilder
的内部捕获缓冲区,可用于保存我们已解析的输入。这由 CaptureBuffer
属性表示。在 ParseContext
上,我们还提供了 CaptureCurrent()
(捕获光标下的当前字符,如果存在),ClearCapture()
(清除捕获缓冲区),以及 GetCapture()
(检索捕获缓冲区的所有或部分内容作为 string
)。
最后,为了创建 ParseContect
,我们有几个 static
方法:Create()
方法接受 string
或 char 数组
,CreateFrom()
方法接受文件名或 TextReader
,以及 CreateFromUrl()
方法接受 URL。
请记住,ParseContext
实现了 IDisposable
,并且在完成解析后,如果不是从 string
或 char
数组加载文本,则必须处理它。通常会使用 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 日 - 初始提交