解析正则表达式语法很困难。我已经编写了很多解析器,并且对于这个解析器,采用了一些我以前没有使用过的新技术。我学到了一些可能引起普遍兴趣的东西。
最初我很惊讶这个问题比看起来更难,但很快意识到我不应该如此,因为我的大脑也总是很难解析它们。
它们绝对是只写语法,仅仅因为我很高兴地编写本系列并不意味着我建议您经常使用 RE 作为工具。
但我敢打赌,我这个行业的大多数人都发现自己经常使用它们,在常见的情况下,它们是从 A 到 B 的最快路径。而且我确信,在某些情况下,它们最终会后悔这个选择。
无论如何,我安慰自己,I-Regexp RE 方言通常比 PCRE 具有更少的语法和更少的枪械。另外,我很享受实施它们。所以,把自己打垮吧。 (不是法律或投资建议。)
样本驱动开发
当我开始认真思考解析器时,我脑海中的第一个想法是“我到底要如何测试这个?”我无法忍受在没有合理答案的情况下编写一行代码的想法。然后我想到,由于 I-Regexp 是XSD 正则表达式的子集,而且 XSD(我最不喜欢的)被广泛部署和使用,也许有人已经编写了一个测试套件?因此,我把头伸进了 XML 社区空间(这么多年了,仍然充满活力)并询问“有人有 XSD regexp 测试套件吗?”
它成功了! (我有时喜欢这个职业。) Michael Kay向我指出了一些值得注意的事情,包括这个 GitHub 存储库。那里的_regex-syntax-test-set.xml
太大而无法显示,包含近千个正则表达式,有些有效,有些无效,许多配备了应该匹配和不应该匹配的字符串。
亲爱的读者,我将其转换为*_test.go
文件的过程并不顺利。我不会分享其中的丑陋之处,其中涉及 awk 和 emacs,以及丑陋且基本上未经测试的一次性 Go 代码。
但我必须说,如果你必须为任何事情编写一个解析器,那么拥有 992 个示例案例会让这项工作变得不那么可怕。
教训:当您编写代码来处理对您来说不熟悉的数据格式时,请在开始之前花时间寻找示例。
递归下降
I-Regexp 规范包含该语法的完整 ABNF 语法。对于编写解析器,我倾向于喜欢基于有限自动机的方法,但对于像这样异常复杂的迷你语言,我向奥林巴斯的语法鞠躬并开始递归下降。
我想在某个时候我理解了正则语言和 LL(1) 等理论,但现在不再理解了。话虽如此,递归下降技术在概念上很简单,所以我继续努力。最终它奏效了。但似乎有很多草率的角落,我不得不向前看一个字节或回溯一个字节。也许如果我更好地理解 LL(1) 的话,事情会更顺利。
“字符类”语法[abc0-9]
特别糟糕。可能的前导-
或^
使情况变得更糟,并且它具有通常的\
– 前缀节。我再次向最初的说明者致敬,他们成功地用可用的语法表达了这一点。
我很受诱惑,但最终没有使用 Go 的regexp
库来帮助我解析 RE。
我不得不说,我不喜欢我最终得到的代码,就像我以前的任何(基于自动机的)解析器一样,也不喜欢 Quamina 代码的其余部分。但似乎工作正常。说到那……
测试覆盖率
当我最终获得为 Michael Kay 的 992 个测试用例执行正确操作的代码时,我感到一阵温暖。然后我运行了测试覆盖率工具,得到了一个令人失望的低数字。一般来说,我不是一个 100% 覆盖率的激进分子,但我支持像这样具有大爆炸半径的超低级别的东西。
这就是教训:代码覆盖工具是你的朋友。我进去看了看绿色和红色的条;他们透露,虽然我的测试已经通过,但我对他们使代码采用的路径的假设确实是错误的。随后进行了大量的重构。
其次,有点令人失望的是,Go 中臭名昭著的if err != nil
节有很多遗漏。这表明我的示例集没有像我希望的那样彻底地涵盖 RE 语法空间。特别是,确实没有报道代码对格式错误的 UTF-8 的反应。
我写这篇文章的原因是为了强调,即使您所在的商店不需要使用代码覆盖工具(遗憾的是),您也应该在基本上每一段重要的代码上使用一个工具。我绝对不会错过惊喜,并因此改进代码。
分享工作
我不知道 I-Regexp 是否会被采用,但如果它被采用,我也不会感到惊讶;这是一个很好处理的子集,可以满足很多用例。不管怎样,现在我已经有了相当健壮且经过良好测试的 I-Regexp 解析代码。我想分享一下,但是有一个问题。
为此,我必须将其放在一个单独的存储库中;没有人会愿意导入所有的 Quamina,这是一个相当大小的库,只是为了解析 RE。但随后另一个存储库将成为 Quamina 依赖项。关于 Quamina 我最喜欢的事情之一是它有0 个依赖项!
正确的做法是什么并不明显;有什么想法吗?
原文: https://www.tbray.org/ongoing/When/202x/2024/12/12/QRS-Parsing-Regular-Expressions