在软件开发中,解析文档是一个常见且重要的任务。尽管现代编程语言提供了许多自动化生成解析器的工具,但很多时候,为了效率和灵活性,仍然选择手动编写解析代码。本文将介绍一种新的解析上下文工具,它可以显著提高解析文档的效率和便捷性。
在解析文档时,经常会遇到一些常见的问题。例如,使用TextReader
或IEnumerator<char>
时,无法在事后检查是否已经到达了枚举的末尾,而且Peek()
函数在某些源上(如NetworkStream
)不可靠。这些限制要求进行额外的记录工作,这不仅增加了解析代码的复杂性,而且分散了对核心任务——解析输入的注意力。
另一个显著的限制是缺乏前瞻性。通常情况下,只能查看当前字符前面的一个字符,但有时为了完成解析,需要向前看更多字符。此外,在解析过程中,也没有内置的错误处理和报告机制。
为了解决这些限制,设计了一个名为ParseContext
的类。这个类的工作方式与TextReader
类似,它以int
的形式返回输入字符。它使用-1来表示输入的结束,-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
,有几个静态方法:Create()
方法接受一个string
或一个char
数组,CreateFrom()
接受一个文件名或一个TextReader
,以及CreateFromUrl()
接受一个URL。
请记住,ParseContext
实现了IDisposable
,如果从任何非string
或char
数组加载文本,使用完毕后非常重要要对其进行处理。通常的做法是使用Close()
方法,但删除了它,因为以"C"开头的成员太多,使得智能感知变得困难。在C#中使用Dispose()
或using
关键字。
此外,还包括了一个撕裂的部分类,包括TrySkipWhitespace()
、TryReadUntil()
和各种其他方法,用于常见的解析任务。这在ParseContext.Helpers.cs
中。它不是基础功能所必需的,但在实践中,它可能在几乎所有解析器中都非常有用。
为了演示这个类的能力,而不让太多额外的东西干扰,示例项目简单地解析并压缩了一个大型JSON文件。JSON语法相当简单,可以在json.org上看到所有的细节。解析实现如下。大致来说,它被分为三个主要部分,代表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) {
var result = new Dictionary<string, object>();
pc.TrySkipWhiteSpace();
pc.Expecting('{');
pc.Advance();
while (pc.Current != '}' && pc.Current != -1) {
pc.TrySkipWhiteSpace();
pc.Expecting('\"');
var fn = _ParseJsonValue(pc);
pc.TrySkipWhiteSpace();
pc.Expecting(':');
pc.Advance();
result.Add(fn, _ParseJson(pc));
pc.TrySkipWhiteSpace();
if (pc.Current == ',') pc.Advance();
}
pc.Expecting('}');
pc.Advance();
return result;
}
static IList<object> _ParseJsonArray(ParseContext pc) {
var result = new List<object>();
pc.TrySkipWhiteSpace();
pc.Expecting('[');
pc.Advance();
while (pc.Current != ']' && pc.Current != -1) {
pc.TrySkipWhiteSpace();
result.Add(_ParseJson(pc));
pc.TrySkipWhiteSpace();
if (pc.Current == ',') pc.Advance();
}
pc.Expecting(']');
pc.Advance();
return result;
}
static string _ParseJsonValue(ParseContext pc) {
pc.TrySkipWhiteSpace();
pc.ClearCapture();
if (pc.Current == '\"') {
pc.CaptureCurrent();
pc.Advance();
pc.TryReadUntil('\"', '\\', true);
return pc.GetCapture();
}
pc.TryReadUntil(false, ',', '}', ']', ' ', '\t', '\r', '\n', '\v', '\f');
return pc.GetCapture();
}