我是编程乐趣,一个10年.Net开发经验老程序员,点击右上方“关注”,每天为你分享开源项目和编程知识。
推荐一个专为 C# 开发者设计的轻量级、快速且灵活的解析库。
01
项目简介
Pidgin是基于C#的开源项目,是一个解析组合器库,提供了一个高级别的声明性工具来构建解析器,使得编写解析器变得简单而直观。
1、轻量化与高效Pidgin专注于提供轻量级的解决方案,旨在减少内存占用和提高解析速度。通过精心设计的数据结构和算法,Pidgin 能够在不牺牲功能的前提下实现高效的解析。
2、灵活性
Pidgin 支持解析各种复杂的数据格式,不仅限于文本数据。由于其能够处理任意类型的输入令牌(tokens),Pidgin 可以用于解析二进制协议、标记化输入等多种场景。
3、易于使用
与正则表达式相比,Pidgin 提供了更强大的解析能力,而与 ANTLR 等解析生成器相比,它又更简单易用。
Pidgin 的 API 设计直观,允许开发者以声明性的方式定义语法规则,而无需编写复杂的代码。
02
解析器分类
1、原始解析器
Any:表示一个使用单个字符并返回该字符的解析器。
Char:指定特定字符并返回该字符。如果遇到其他字符,则失败。
Digit:解析并返回单个数字字符。
String:解析并返回特定字符串。
Return:返回给定值。
// 使用一个解析器Char('a')来检查输入字符串"a"是否可以被解析为单个字符'a'。// 如果可以,Assert.AreEqual将验证解析结果是否为'a'。Parser parser = Char('a'); Assert.AreEqual('a', parser.ParseOrThrow("a"));// 使用Digit解析器来检查输入字符串"3"是否可以被解析为一个数字字符,并期望得到整数形式的'3'(尽管这里实际上得到的是字符'3')。// 注意:如果目标是得到整数类型的3,而不是字符'3',则示例可能需要调整或明确说明转换。// 但按照代码字面意思,Assert.AreEqual验证的是字符'3'。Assert.AreEqual('3', Digit.ParseOrThrow("3"));// 创建一个解析器String("foo"),该解析器尝试从输入字符串中提取子字符串"foo"。// 如果输入字符串确实是"foo",则Assert.AreEqual将验证解析结果是否为字符串"foo"。Parser parser = String("foo"); Assert.AreEqual("foo", parser.ParseOrThrow("foo"));// 使用Return(3)创建一个解析器,该解析器不依赖于输入字符串,而是直接返回整数3。// 这意味着无论输入字符串是什么,解析结果都将是3。// 因此,即使输入是"foo",Assert.AreEqual仍然验证解析结果是否为整数3。Parser parser = Return(3); Assert.AreEqual(3, parser.ParseOrThrow("foo"));2、排序解析器
Then:顺序执行两个解析器,仅保留第二个的结果。
Before:顺序执行两个解析器,但仅保留第一个的结果。
Map:对解析器结果进行转换,不改变解析流程。
Bind:基于第一个解析器的结果选择并执行第二个解析器。
// 示例1: 使用Then(假设它保留第二个解析器的结果) Parser parser1 = String("foo"); Parser parser2 = String("bar"); // 注意:这里可能需要一个特殊的Then实现,因为标准的Then可能不直接丢弃第一个结果 // 假设有一个名为ThenThatOnlyKeepsSecond的扩展方法 Parser sequencedParser = parser1.ThenThatOnlyKeepsSecond(parser2); // 假设的方法名 Assert.AreEqual("bar", sequencedParser.ParseOrThrow("foobar")); // 示例2: 使用Before(假设它存在且保留第一个解析器的结果) // 注意:这通常不是标准库中的操作,但我们可以假设它存在 Parser beforeParser = parser1.Before(parser2); // 假设的Before实现 Assert.AreEqual("foo", beforeParser.ParseOrThrow("foobar")); // 示例3: 使用Map来合并两个解析器的结果 // 注意:这里我们假设有一个能够处理两个解析器的Map实现 Parser mappedParser = Map((foo, bar) => bar + foo, parser1, parser2); // 假设的Map实现 Assert.AreEqual("barfoo", mappedParser.ParseOrThrow("foobar")); // 示例4: Bind的使用(但这里给出的例子并不典型) // 假设我们想要根据输入的第一个字符来构造一个解析器,但这里我们简化它 Parser anyCharParser = AnyChar(); // 假设的AnyChar解析器 Parser specificCharParser = anyCharParser.Bind(c => Char(c)); // 但为了符合您的示例,我们假设它工作如下(实际上这并不实用) Assert.AreEqual('a', specificCharParser.ParseOrThrow("a")); // 这将总是返回输入的第一个字符
3、选择替代方案
Or:表示可以解析两个备选方案之一的解析器。
OneOf:等同于Or,只不过它接受可变数量的参数。
// 定义一个能够解析字符串 "foo" 或 "bar" 的解析器 // 使用 Or 方法组合两个 String 解析器 Parser parser = String("foo").Or(String("bar")); // 测试解析器是否能正确解析 "foo" Assert.AreEqual("foo", parser.ParseOrThrow("foo")); // 应该成功解析并返回 "foo" // 测试解析器是否能正确解析 "bar" Assert.AreEqual("bar", parser.ParseOrThrow("bar")); // 应该成功解析并返回 "bar"// 用于创建一个能够解析多个选项中任意一个的解析器 Parser parser = OneOf(String("foo"), String("bar")); // 测试解析器是否能正确解析 "foo" Assert.AreEqual("foo", parser.ParseOrThrow("foo")); // 应该成功解析并返回 "foo" // 测试解析器是否能正确解析 "bar" Assert.AreEqual("bar", parser.ParseOrThrow("bar")); // 应该成功解析并返回 "bar"4、LINQ语法
Parser parser = from c in Any from c2 in Char(c) select c25、递归语法
Parser expr = null;Parser parenthesised = Char('(') .Then(Rec(() => expr)) .Before(Char(')'));expr = Digit.Or(parenthesised);Assert.AreEqual('1', expr.ParseOrThrow("1"));Assert.AreEqual('1', expr.ParseOrThrow("(1)"));Assert.AreEqual('1', expr.ParseOrThrow("(((1)))"));
03
与解析工具对比
Pidgin vs Sprache
Sprache 是另一个 C# 解析组合器库,但与 Pidgin 相比,Sprache 存在一些限制:
输入类型:Sprache 只支持字符串输入,而 Pidgin 支持任意类型的输入令牌。
性能:Pidgin 在速度和内存分配方面优于 Sprache。
文档覆盖:Pidgin 提供了更全面的文档覆盖。
Pidgin vs FParsec
FParsec 是一个为 F# 设计的解析组合器库,与 Pidgin 相比,它有以下不同:
语言支持:FParsec 是 F# 库,从 C# 调用可能不太方便。Pidgin 是为 C# 设计的。
输入类型:FParsec 仅支持字符输入流,而 Pidgin 支持任意类型的输入令牌。
性能:虽然 FParsec 在某些情况下更快,但 Pidgin 正在不断改进其性能。
04
项目地址
https://github.com/benjamin-hodgson/Pidgin
- End -
.Net开源项目合集:https://github.com/bianchenglequ/NetCodeTop
推荐阅读
ZoneTree: 高性能ACID兼容的.NET有序键值数据库
SubtitleEdit:一个基于.Net开发的开源字幕编辑器
AutoFixture:.NET 的假数据生成工具
一个C#开源工具库,集成了超过1000个扩展方法
Plotly.NET:一个强大的、漂亮的.NET开源交互式图表库