更简单的手动解析器






4.98/5 (17投票s)
更简单的手动解析器
引言
解析是现代软件中一个常见的需求。由于存在如此多的数据交换格式,大多数开发人员迟早都需要编写一个解析器。
大多数具有复杂解析器的商业产品在其解析过程的至少一部分使用了手写解析。
这里的代码旨在提供一个小型、轻量级的解决方案,以简化手写解析器的创建。
背景
手写解析器可能难以编写和维护。主要问题之一是适当的分解,但由于超前(lookahead)的存在,分解变得更加困难。
超前,简单来说,是解析器需要读取游标之前的符号或字符,以便选择下一个分支。
许多解析器只需要1个字符的超前。例如,JSON 语言的语法只需要一个字符的超前就可以解析它。更复杂的语法在某些时候可能需要更多,但通常,大部分只需要1个字符的超前。
处理超前的一种方法是使用 TextReader Peek()
函数,但这会在 NetworkStream
上中断。它需要一定量的“查找”才能工作。这意味着“回溯”(多次遍历相同的字符),这应该是不必要的。
Using the Code
进入 ParseContext
ParseContext
是一个包装底层 TextReader
或 IEnumerable<char>
(包括 string
)的类,并提供几种用于解析和捕获内容的方法。
它通过将流推进一个字符来提供一个字符的“超前”。它不是从流中读取*下一个*字符,而是每次调用 Advance()
都会更改 Current
成员以反映当前输入,使得 Current
始终包含游标下的字符。
这使得从输入读取和解析变得非常容易。
Capture
包含 Capture
缓冲区中的当前内容,而 CaptureCurrent()
将当前字符(如果有)存储到 Capture
缓冲区中。CaptureBuffer
访问用于存储捕获内容的底层 StringBuilder
。
实现 TryParseXXXXX
方法之一
partial class ParseContext
...
public bool TryParseCSharpLiteral(out object result)
{
result = null;
EnsureStarted(); // make sure we've moved the cursor to a valid position.
switch (Current)
{
case '@':case '\"':
string s;
if(TryParseCSharpString( out s))
{
result = s;
return true;
}
break;
case '\'':
if(TryParseCSharpChar(out s))
{
if (1 == s.Length)
result = s[0];
else
result = s;
return true;
}
break;
case '0':case '1':case '2':case '3':case '4':case '5':case '6':
case '7':case '8':case '9':case '.':case '-':case '+':
if (TryParseCSharpNumeric(out result))
return true;
break;
case 't':case 'f':
bool b;
if(TryParseCSharpBool(out b))
{
result = b;
return true;
}
break;
case 'n':
if (TryReadLiteral("null"))
return true;
break;
}
return false;
}
并使用它
object v;
var val =
//@"""\U00000022'foobar'\U00000022""";
//@"""\U00000022\U00000022\t\""""";
//"@\"\"\"foobar\"\"\"";
//"null";
"-"+(long.MaxValue);
Console.WriteLine("Original value: {0}", val);
var pc = ParseContext.Create(val);
Console.WriteLine("TryRead:");
if (pc.TryReadCSharpLiteral())
Console.WriteLine("\tCapture: {0}", pc.Capture);
else
Console.WriteLine("\tError: {0}", pc.Capture);
Console.WriteLine();
pc = ParseContext.Create(val); // reset
Console.WriteLine("TryParse:");
if (pc.TryParseCSharpLiteral(out v))
{
Console.WriteLine("\tCapture: {0}", pc.Capture);
Console.WriteLine("\tValue: {0}, Type {1}",
v??"<null>",(null!=v)?v.GetType().Name:"<void>");
} else
Console.WriteLine("\tError: {0}", pc.Capture);
Console.WriteLine();
Console.WriteLine("Parse:");
pc = ParseContext.Create(val); // reset
v = pc.ParseCSharpLiteral();
Console.WriteLine("\tValue: {0},
Type {1}", v ??"<null>", (null != v) ? v.GetType().Name : "<void>");
此外,示例代码包含一个用于 C# 源代码的合并-最小化器,以及一些用于解析 C# 字面量和标识符的方法。
关注点
在此代码中,请注意以下几点
ParseContext
被分解成一个 partial
类,以便于在 ParseContext
上创建附加的解析方法,作为一个单独的文件,仅在需要时才能包含。
ParseContext
包含诸如 TryReadXXXXX
、TrySkipXXXXX
以及有时 TryParseXXXXX
和 ParseXXXXX
之类的方法。
这些方法尝试读取 - 带有捕获,尝试跳过 - 没有捕获,尝试解析 - 带有捕获,并解析 - 没有捕获。
只有 ParseXXXXX
方法会抛出异常。其他方法如果解析不成功将返回 false
。对于带有捕获的方法,捕获将包含当前解析的所有文本。如果发生错误,Current
将包含解析失败的字符,而 Capture
将包含解析到该点的字符。
建议您在创建新的解析方法时遵循此模式,但这不是必需的。
历史
- 2019年3月18日:初始发布