尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

OpenHarmony+Flutter实战:用PetitParser打造规则化文本解析器

OpenHarmony+Flutter实战:用PetitParser打造规则化文本解析器 1. 从一次文本解析噩梦说起为什么我会在 OpenHarmony 上翻出 PetitParser1.1 一次优化引发的崩溃现场年初我在给 OpenHarmony 设备端做一个 Flutter 诊断工具其中有个需求解析设备上报的配置快照。格式长这样# 设备配置快照 wifi { ssid IoT-Lab channel 6 auth wpa2 enable true } ble { advInterval 100 features [eddystone, ibeacon, nfc_tag] }单看结构不复杂我一开始也没当回事直接上了正则加字符串截取。前两百行日志解析得很顺利直到某个真机上报里ble区块比wifi先出现我的解析器就乱了。再往后字段值里出现了带空格和括弧的字符串、注释行混在区块之间、features数组里三种取值混排……我在那个周末改了七版代码最终状态是一堆RegExp嵌套匹配看到就想吐。后来同事提了一句这种规则化文本别自己硬写了试试解析器组合子。我查了一圈在 Dart 生态里找到了一个叫作 PetitParser 的库纯 Dart 实现、没有原生依赖、能在 OpenHarmony 的 Flutter 工程里直接跑起来。这个标题说的规则化文本解析大师其实就是它。1.2 规则化文本解析是什么PetitParser 又是谁先说清楚规则化文本这个说法。它指的不是随便什么字符串而是能够用一套文法规则描述、并且严格按规则生成的文本。比如上面这种区块加键值对的配置、设备的日志格式、自定义协议的报文文本都属于这一类。PetitParser 是一个以解析器组合子parser combinator为核心的库。名字里的 Petit 在法语里是小、轻量的意思。它的核心哲学非常简单一个解析器就是一个吃字符串、吐结果的小函数你可以把这些小解析器用组合算子拼成更大的解析器就像用乐高积木搭房子。它不依赖代码生成不依赖自定义 DSL不依赖反射纯 Dart 写的所以天然适合 Dart/Flutter 生态也天然能跑到 OpenHarmony 上。1.3 这篇实战适合谁这篇内容适合三类人在 OpenHarmony 上做 Flutter 开发遇到了非标准配置、日志、协议文本需要解析的写正则写到怀疑人生想尝试一种更可控、可读、可测试的解析方式的准备 Flutter/鸿蒙相关面试想补一个解析器组合子这类加分知识点顺便有拿得出手的实战作品的。前置知识只需要两点Dart 基础语法、Flutter 基本工程结构。如果你手边有 OpenHarmony 开发环境可以直接跟我走一遍暂时没有也没关系前几章解析器部分的代码在任何 Flutter 工程里都能跑。2. 选型博弈正则、状态机还是组合子解析器2.1 正则表达式的三个死穴在进入 PetitParser 之前我先聊聊为什么我不建议继续在正则这条路上一条道走到黑。正则确实是每个开发者都该掌握的武器但它有三个明显的死穴第一可读性差。比如要匹配上面的键值对正则会写成这样(?m)^(?key[A-Za-z_]\w*)\s*\s*(?value[^]*|\w)\s*$写的那一刻你可能觉得还挺工整两个月后再看基本就是看天书。想改一个分支逻辑你得先花半小时回忆每个括号在干嘛。第二无法优雅处理嵌套。正则本身是有限状态机处理区块里套区块括号里套括号这类递归结构会被迫写出极其丑陋的平衡组或者干脆放弃。上面那个配置文件如果允许server区块里再嵌套server { timeout { ... } }正则基本就告别了。第三错误提示几乎为零。正则匹配失败时你只知道没匹配上至于失败在第几行第几列、Fail 在哪个分支上、期望看到什么字符它给不了你。在大文件解析场景里这种黑盒失败非常致命。2.2 手写状态机的维护成本有人会反驳那我不用正则我手写状态机总行吧。状态机在处理词法层面确实很合适一旦进入语法层面把当前状态 输入字符 - 下一状态 动作这张转移表维护起来你会发现自己干了一件蠢事在业务代码里手工实现了一门解释器。我遇到过一段手写解析逻辑为了支持三种嵌套层次、两种表达式、带注释状态变量膨胀到十几个switch分支接近 80 行。每加一个字段类型就要顺着状态流跑一遍所有分支确认不会串状态。这比维护正则还痛苦因为状态机的问题是隐性状态太多出错了只能靠打断点慢慢看状态变量没法一眼看出文法结构。2.3 组合子解析器用搭积木的方式写文法组合子解析器的思路完全不同。你不需要画状态图也不需要写逐个字符的跳转。你只需要把一个词一个数字一对括号这样的小解析器定义出来然后用.seq()表示先后用.or()表示或者用.star()表示重复。文法长什么样代码就长什么样。这个思想最早来自 Haskell 社区的 Parsec 库后来 Java/Scala/JS 生态都有类似实现。PetitParser 是 Dart 生态里最成熟的一个作者在 GitHub 上维护了十多年版本已经迭代到 6.xAPI 稳定文档和示例都很全。2.4 为什么在 OpenHarmony 上选 PetitParser 而非其他库我也对比过其他方案。评估维度包括是否纯 Dart、是否容易集成、是否支持递归文法、是否有错误位置信息、社区活跃度。方案类型纯 Dart递归支持错误定位在 OH 上集成的成本正则内建能力是差几乎没有低但能力不足手写状态机自研是靠人肉靠人肉低但是维护黑洞antlr代码生成器需要生成目标代码强强高工具链复杂petitparser解析器组合子是强强极低pub 依赖即可在 OpenHarmony 这个前提下petitparser 的优势非常突出。第一它没有任何 C/JNI 原生依赖不需要为 OH 的 ABI 适配操心第二它不会像 antlr 那样给工程引入一堆生成代码和构建插件第三它能提供精确的错误位置这对排查真机上报的脏数据特别有用。后面第五小节我会专门讲 OpenHarmony 落地时的细节。提示如果你只是解析 JSON、YAML、XML 这种通用格式别拿 PetitParser 造轮子老老实实用dart:convert或专用库。PetitParser 的舞台是私有格式临时格式快速原型格式。3. 趁热打铁PetitParser 核心概念与最小可用示例3.1 Parser 是个什么东西Result 里有什么玄机先建立一个最小心智模型一个 Parser 就是一个函数输入一段字符串输出一个 Result。Result 要么是成功带一个值要么是失败带一条消息和一个位置。import package:petitparser/petitparser.dart; void main() { // 识别连续的数字 final parser digit().plus(); final result parser.parse(12345); print(result.isSuccess); // true print(result.value); // [1, 2, 3, 4, 5] print(result.position); // 5 }注意这里的.value是[1, 2, 3, 4, 5]是字符列表不是拼接好的字符串。要用.flatten()把它压平成一个字符串final parser digit().plus().flatten(); final result parser.parse(12345); print(result.value); // 12345失败的情况长这样final result parser.parse(abc); print(result.isSuccess); // false print(result.message); // digit expected print(result.position); // 0这个position是核心它是从 0 开始的字符下标。后面我会写一个工具函数把 position 转成第几行第几列这样给用户报错就很友好。3.2 常用基础 Parser 和组合操作符PetitParser 的基础 Parser 大体分三类字符匹配、结构组合、结果转换。类别常用 API作用字符匹配any()任意单个字符char(a)精确匹配单个字符digit()/letter()/word()数字 / 字母 / 单词字符whitespace()空白字符string(abc)精确匹配一个字符串结构组合p1.seq(p2)先 p1 再 p2p1.or(p2)优先 p1失败则尝试 p2p.star()重复 0 次或多次p.plus()重复 1 次或多次p.times(n)精确重复 n 次p.optional()可选失败时返回 null结果转换p.flatten()把匹配到的输入转成字符串p.map((v) { ... })把结果转成业务对象p.trim()丢弃两侧默认空白p.separatedBy(sep)用分隔符隔开重复p.end()必须匹配到输入末尾这些操作符几乎是所有解析器组合子库的公共词汇。一旦你学会 PetitParser将来接触 Swift 生态的 Parsing、Rust 生态的 Nom上手成本都很低因为思路是相通的。3.3 一个最小但完整的例子日期解析器先不碰 OpenHarmony我们来写一个日期解析器输入2025-04-06这样固定格式的字符串解析成DateTime对象。final dateParser digit().times(4).flatten() .seq(char(-)) .seq(digit().times(2).flatten()) .seq(char(-)) .seq(digit().times(2).flatten()) .map((values) { final year int.parse(values[0] as String); final month int.parse(values[2] as String); final day int.parse(values[4] as String); return DateTime(year, month, day); }) .end(); void main() { final ok dateParser.parse(2025-04-06); print(ok.value); // 2025-04-06 00:00:00.000 final bad dateParser.parse(2025-4-6); print(bad.message); // digit expected print(bad.position); // 5 }.seq会把每个子解析器的结果按顺序放进一个 List所以我用values[0]、values[2]、values[4]取到年月日。这个写法虽然能跑但索引取值得靠猜。更优雅的做法是用.map的具名参数或者干脆在.map里解构元组.map((v) DateTime(int.parse(v[0]), int.parse(v[2]), int.parse(v[4])));这只是热身。真正有挑战性的是递归文法比如表达式求值。3.4 真正的递归要靠 Grammar表达式求值假设要解析一个只包含整数和加号的简单表达式123你可能会想digit().plus().flatten().seq(char()).seq(...)无限循环下去吗不行。star()可以处理重复但没法处理表达式里面嵌套表达式这种递归关系。比如括号(12)*3。PetitParser 提供了GrammarDefinition类来组织这种递归文法。以加减法表达式为例经典的三层优先级结构是class ExprParserDefinition extends GrammarDefinition { // 文法起点 Parser start() ref0(() expr()).end(); // 表达式 项 (( | -) 项)* Parser expr() ref0(() term()) .seq(char().or(char(-)).seq(ref0(() term())).star()) .map((values) { var value values[0] as int; for (final op in (values[1] as List)) { final opChar op[0] as String; final operand op[1] as int; if (opChar ) { value operand; } else { value - operand; } } return value; }); // 项 因子 ((* | /) 因子)* Parser term() ref0(() factor()); // 因子 数字 | ( 表达式 ) Parser factor() digit().plus().flatten().map(int.parse) .or(char(().seq(ref0(() expr())).seq(char())) .map((v) v[1] as int)); Parser token(Parser parser) parser.trim(); } void main() { final parser ExprParserDefinition().build(); final result parser.parse(1(2-3)4); print(result.value); // 4 }如果你用的是 PetitParser 6.x递归引用用ref0(() parser())这种写法。老版本5.x 之前的define语法在新项目里就不推荐了API 变化比较大建议直接用 6.x。expr、term、factor这三层是表达式解析的经典套路每一层负责一个运算符优先级递归下降时通过ref0互相引用。有了这个骨架你可以扩展乘除、括号、负数甚至函数调用。这正是规则化文本解析最爽的地方——文法结构直接映射成代码结构后期改优先级就是在对应层次上改一行。4. 实战用 PetitParser 写一个迷你配置语言解析器4.1 目标文法带嵌套区块、数组和注释的迷你配置文件现在进入正题把第一节那次设备配置快照的解析完整实现一遍。我们要解析的格式有两个核心特点区块可以嵌套键值对的 value 支持多种类型。具体文法定义如下program : section* section : name { entry* } name : 标识符 entry : 标识符 value value : string | number | boolean | array | identifier array : [ value (, value)* ] string : 任意字符 或 任意字符 boolean : true | false identifier: 字母开头包含字母数字下划线注释用#开头行内从#到行尾都忽略。空白字符空格、Tab、换行在文法记号之间随意出现。我们的目标输出是一个MapString, Object?比如{ wifi: { ssid: IoT-Lab, channel: 6, auth: wpa2, enable: true, }, ble: { advInterval: 100, features: [eddystone, ibeacon, nfc_tag], } }4.2 逐步实现从 Token 到 AST实现解析器的第一步永远不是写 Parser而是先想清楚文法层和转换层怎么分开。我的建议是先写识别结构的 Parser确认匹配没问题再写.map()做数据转换。两步走能避免把解析逻辑和业务转换混在一起后期调试会舒服很多。下面是完整实现。我把功能拆成几个私有方法每个方法对应文法里的一条规则import package:petitparser/petitparser.dart; class ConfigParser { static ParserMapString, Object? create() { // 基础 token final identifier (letter().or(char(_))) .seq(word().or(char(_)).star()) .flatten() .trim(); final comment char(#).seq(any().starLazy(char(\n))).or(char(\n)); final ignored (whitespace().or(comment)).star(); final stringValue char() .seq(any().starLazy(char()).flatten()) .seq(char()) .map((v) v[1] as String) .or( char() .seq(any().starLazy(char()).flatten()) .seq(char()) .map((v) v[1] as String), ); final numberValue char(-).optional() .seq(digit().plus()) .seq(char(.).seq(digit().plus()).optional()) .flatten() .map((v) { if (v.contains(.)) { return double.parse(v); } return int.parse(v); }); final booleanValue string(true).map((_) true) .or(string(false).map((_) false)); // 关键数组和 value 互相引用所以要用 Ref 延迟解析 late final ParserObject? value; late final ParserListObject? array; value stringValue .mapObject?((v) v) .or(numberValue.mapObject?((v) v)) .or(booleanValue.mapObject?((v) v)) .or(identifier.mapObject?((v) v)) .or(Ref(() array).mapObject?((v) v)) .trim(); array char([) .seq(value.separatedBy(char(,).trim()).optional()) .seq(char(])) .map((v) (v[1] as ListObject??) ?? const []); final entry identifier .seq(char().trim()) .seq(value) .map((v) (key: v[0] as String, value: v[2] as Object?)); final section identifier .seq(char({).trim()) .seq(entry.star().trim()) .seq(char(}).trim()) .map((v) ( name: v[0] as String, entries: (v[2] as List).map((e) (e as ({String key, Object? value}))); )) .map((section) MapEntry( section.name, Map.fromEntries(section.entries as IterableMapEntryString, Object?), )); return ignored .seq(section.star()) .seq(ignored) .map((v) Map.fromEntries(v[1] as ListMapEntryString, Object?)) .end(); } }这段代码看起来有点长但每一层都很机械identifier 负责词法section 负责语法seq负责把子结果拼装map负责把拼装结果转成业务对象。有几个地方需要特别说明第一数组和 value 的互相引用。value 的值类型包含数组数组的元素又可能是 value这是递归结构。Dart 的变量声明顺序没法直接让value引用自身所以用了late final声明加Ref(() array)延迟解析。这是 PetitParser 处理左递归、互相引用的标准姿势。第二starLazy的妙用。匹配字符串内容时我用了any().starLazy(char())意思是一直消费任意字符直到遇到下一个双引号就停下。如果写成贪婪的any().star()它会一路吃到字符串结尾把后面的引号也吞进去。第三trim()的位置。我把value整体做了一次.trim()这样[api, core]里的空格、等号前后的空格就都不用单独处理了。token 层该管的空白别让语法层再管一遍。4.3 转换与错误提示让解析器学会说人话上面代码里.map()已经把文本转成了 Dart 对象但还有一个关键问题解析失败时用户看到什么来看一个错误示例void main() { const input wifi { ssid IoT-Lab channel 6 // 这里少了等号 } ; final parser ConfigParser.create(); final result parser.parse(input); if (result.isSuccess) { print(result.value); } else { print(formatError(input, result)); } } String formatError(String input, Result result) { final pos result.position; final line input.substring(0, pos).split(\n).length; final lastNewline input.lastIndexOf(\n, pos - 1); final column pos - lastNewline; return 第 $line 行第 $column 列解析失败${result.message}; }输出是第 3 行第 10 列解析失败 expected。这个定位能力是正则和手写状态机很难给的。在实际项目里我会把这种错误信息直接展示在诊断界面上用户能把错误行号对应到上行报文问题定位效率翻倍。4.4 在 Flutter UI 里用起来解析器写好之后接入 Flutter 就是一件非常顺滑的事。我当时的做法是先把一份示例配置作为 asset 打包进应用用rootBundle读出来然后交给 parser 解析再调setState渲染成参数列表。Futurevoid loadConfig() async { final raw await rootBundle.loadString(assets/config.snapshot); final result ConfigParser.create().parse(raw); if (result.isSuccess) { setState(() { config result.value as MapString, Object?; }); } }有一点要提醒在 OpenHarmony 上path_provider这类读取实际文件路径的插件不一定全都适配完整所以如果只是演示推荐先把配置文件放进 assets等确认平台适配没问题再切换到运行时动态读文件。后者属于平台通道的兼容性问题解析器本身完全不依赖这些这是纯 Dart 库带来的安心感。5. 落地 OpenHarmony环境配置与平台踩坑记录5.1 从零准备的 OpenHarmony Flutter 环境如果你手边有一个 OpenHarmony 设备或模拟器要把上面的解析器跑起来得先有一个能构建 OH 应用的 Flutter 环境。目前 OpenHarmony 生态的 Flutter 适配通常由社区版本或者厂商分支提供步骤大致如下第一步安装 OpenHarmony SDK 和工具链。官方 IDE 是 DevEco Studio它会内置 SDK、hdc工具类似 Android 的 adb。装好后在环境变量里配上DEVECO_SDK_HOME指向 SDK 目录。命令行编译时会用到。第二步安装适配了 OH 平台的 Flutter SDK。这里强烈建议不要直接动你日常开发用的 Flutter 版本而是用fvm管理多版本fvm install 3.27.3-ohos fvm use 3.27.3-ohos flutter doctorflutter doctor输出里如果能看到 OpenHarmony 相关的检查项说明 SDK 路径已经被这个 Flutter 分支识别了。如果看不到大概率是DEVECO_SDK_HOME没配对。第三步创建项目。个别分支需要你手动指定平台参数flutter create --platforms ohos my_config_parser cd my_config_parser创建完成后工程结构里会多出一个ohos目录类似 Android 的android目录里面是鸿蒙侧的原生壳工程。第四步连接设备或模拟器。用hdc list targets查看设备列表确认设备在线后就可以用flutter run -d device-id启动。5.2 pubspec 里加 petitparser这一步没有秘密就是加一行依赖dependencies: flutter: sdk: flutter petitparser: ^6.0.2执行flutter pub get。由于 petitparser 是纯 Dart 包它在 pub 解析阶段不需要任何原生构建也不会触发 CMake、NDK 这类编译流程这是它跟很多 Flutter 插件最大的区别。然后写一个最简单的主页把 4.2 节的解析器放进去解析写死的配置字符串把结果打印在屏幕上。你会在 OH 模拟器和真机上看到一个朴素的Text控件显示解析后的 Map。能跑到这个程度说明整个环境链路已经通了。5.3 从跑不起来到UI 正常我踩过的坑环境搭建阶段我遇到了三个印象深刻的坑逐个记录一下。坑一Windows 下报unable to find suitable visual studio toolc...这个报错我第一次看到是在 Android 原生构建阶段移植到 OH 工程后也出现过类似提示。报错的本质都一样Flutter 要编译包含 C 代码的原生模块时找不到合适的 C 工具链而并不是你的 Dart 代码出错了。解决办法是打开 Visual Studio Installer勾选使用 C 的桌面开发工作负载装好后重启编辑器。如果机器上不想装完整 VS装 Build Tools 也行关键是必须有 MSVC 编译器。坑二OpenHarmony 上画面渲染异常有段时间我每次flutter run起来界面会出现黑屏或者局部区域刷新错乱尤其切换页面后残留上一帧的画面。第一反应是解析逻辑或者布局的问题后来排查发现是渲染引擎的 Impeller 路径在 OH 适配版本上还不够稳定。尝试用flutter run --no-enable-impeller关掉新渲染引擎后画面恢复正常。提示遇到渲染异常时先看启动日志有没有 Impeller 相关 warning再尝试关闭重跑。如果问题依旧flutter clean后重新编译很多玄学问题其实是增量构建的缓存导致的。坑三x86 模拟器上三方库缺 ABI 支持OpenHarmony 的模拟器不少是 x86 架构。如果你引入了带有 so 库的 Flutter 插件很可能会遇到x86 OH 设备上找不到对应 ABI 的 so之类的错误。解决方式要么找支持 x86 的插件版本要么换真机调试。但 PetitParser 这类纯 Dart 库完全不会遇到这个问题这也是我在 OH 项目里越来越倾向选纯 Dart 依赖的原因之一。5.4 实测性能解析一大段配置要多久解析器写完了、环境也通了一个绕不开的问题就是性能。我在模拟器上做了一次简单测试生成一份 2 万行左右的配置快照模拟大量设备上报拼接成的文件用Stopwatch计时。final sw Stopwatch()..start(); final result ConfigParser.create().parse(bigString); sw.stop(); print(解析耗时: ${sw.elapsedMilliseconds}ms);实测下来2 万行配置在 Debug 模式下大概 200ms 到 400msRelease 模式下可以压到 100ms 以内。对设备上报配置快照这种低频操作来说完全够用。但这个数字只是参考。PetitParser 的性能跟你的文法结构密切相关尤其是回溯场景。如果同一个位置反复尝试多个.or()分支耗时可能指数级增长。这点在下一节展开讲。6. 性能与边界它不能干什么以及如何聪明地用它6.1 别让回溯吃掉你的性能解析器组合子的常见性能杀手是回溯backtracking。当你写p1.or(p2)时PetitParser 会先尝试完整的p1如果失败再从头尝试p2。如果p1已经消费了一部分输入但最终失败那输入指针要回到起点重新来。嵌套一多失败 - 回退 - 再试的次数就会膨胀。典型场景是匹配带引号的字符串时用.starLazy(char())还是.seq(char()).star()...的差别。如果用贪婪匹配解析器会一直吃字符直到输入末尾才发现没有终止引号然后整体回溯等于把这个字符串跑了整整一遍才失败。我在写配置文件解析器时特意用starLazy就是为了规避这种无效消耗。另一个技巧是把出现概率高的分支放在.or()的左边。比如上面配置格式里 value 的类型最常见的是字符串和数字我就把它们放在 identifier 和 array 之前。这样大多数情况下解析器走了最短路径不会频繁触发回溯。如果确实遇到复杂的歧义文法PetitParser 还提供了.cached()方法做记忆化把某个位置、某个解析器的结果缓存下来避免重复解析同一段输入。但缓存有内存开销适合单次解析大文件的场景不适合频繁创建 parser 实例的短文本。6.2 大文本的取舍如果你的输入文本真的很大——单次解析几十 MB 甚至更大我建议做两件事。第一按记录分块。像配置快照这种格式天然是一条配置记录接一条记录完全可以按section切块逐块解析而不是把整个文件喂给一个 parser。这样内存占用小单次失败也不会拖垮全部数据还能做到解析到第 N 条出错就只处理前 N-1 条。第二把 parser 实例缓存为静态 final。解析器一旦构建完成它的结构是固定的不会因为你调用parse()就改变。如果每次解析都重新ConfigParser.create()等于把文法重新组装一遍纯浪费。final _configParser ConfigParser.create(); FutureMapString, Object? parseConfig(String raw) { final result _configParser.parse(raw); // ... }6.3 什么时候应该放弃 PetitParser虽然我一直在安利它但 PetitParser 不是万能的。有三类场景它并不合适一类是完整编程语言的前端。如果你要写的是编译器、解释器涉及类型系统、作用域解析、语法高亮、增量编译这些能力建议直接上 ANTLR 这种成熟的解析器生成器它们对文法诊断、错误恢复、符号表管理都有系统性支持。PetitParser 更适合要把一段文本变成数据这个层面而不是要维护一个语言生态。二类是超高吞吐的流式场景。如果每毫秒都要解析上百条紧凑报文组合子解析器的栈式结构不如手工字节解析来得干脆。当然你也可以先用 PetitParser 快速生成原型再针对热点路径做字节级优化。三类是已有专用格式库的格式。解析 JSON 用dart:convert解析 YAML 用yaml包解析 CSV 用csv包。重复造轮子不会让你的代码更好维护。我的判断标准很简单格式是自有/临时/可能频繁变还是标准/稳定/有现成库。前者给 PetitParser后者找专用库。6.4 把文法写成类把错误信息当产品来做最后一节分享两个让我少走弯路的工程实践。第一用 GrammarDefinition 而不是一堆顶层函数组织文法。我的第一个 PetitParser 项目把所有解析器都定义成顶层final变量小规模还行等到文法到了十几条规则时变量之间的依赖顺序就成了玄学。换成GrammarDefinition子类后每条文法规则一个方法互相引用一目了然还能利用类的成员变量传递全局配置。第二错误信息要当产品功能来做。上面 4.3 节那个formatError函数虽然只有五行但它直接决定了解析器在业务侧的使用体验。我在项目里通常还会把错误信息分成三级用户能看懂的、开发者能定位的、内部排查用的原始位置。PetitParser 在错误消息这里其实还能再挖一层——你可以给每个子解析器通过.map()之前的Failure对象自定义 message但我建议先保持简单等确实需要再扩展。第三每个规则都配一个单元测试。petitparser 文档里推荐直接对每个基础 Parser 做断言test(解析 wifi 区块, () { final parser ConfigParser.create(); final result parser.parse(wifi { ssid lab }); expect(result.isSuccess, isTrue); expect((result.value as Map)[wifi][ssid], lab); });这部分投入的时间会在你后面调整文法时十倍回收。我在实际项目中还有一个体会PetitParser 最大的价值不是让你写出更快的解析器而是把一个不确定的、黑盒的解析过程变成一个确定性的、可推演的文法描述。配置格式变了你改一个方法跑一遍测试单就知道哪里断了。OpenHarmony 生态还比较年轻类似设备配置快照这种各方定义各说各话的文本格式会越来越多。掌握解析器组合子这个工具你在面对新格式时就不会再慌。
返回列表