加载中...

本篇要回答的问题:以"文本处理"这个具体问题域为例,怎么从不可穷尽的多变需求场景中,抽出正交分解后可复用的架构范式

上一讲说架构师的武器库是不断完善的架构范式,并预告用"文本处理"做示范。本讲就围绕这一个问题域,先回顾作者二十年的技术栈演进,再提炼出文本内容处理二进制内容处理两套通用范式。这里的"文本"指写入磁盘的非结构化数据,可以是真文本(HTML/CSS),也可以是二进制(Word/Excel)。

文本处理的典型需求场景:

场景 含义
数据验证(Data Validation) 判断输入文本是否合法、值范围是否符合期望
数据抽取(Data Extraction) 从 HTML 抽取结构化信息(如机票时间/出发地/价格)
编译器(Compiler) 文本是代码时,编译成机器码/字节码或边解释边执行

我的文本处理技术栈演进

作者文本处理技术栈二十年演进时间线

时间 产物 关键意义
2000 初 ExcelViewer / DocViewer 用程序固化对二进制文件格式的理解,利于知识传承;只抽象界面呈现,但尚未抽象出文本处理范式
同期 mk 程序 解析类 ini 配置 + C/C++ 依赖表做增量编译;仍无通用范式
2004 KSDN 1.0 从源码生成全局文档;首次引入通用脚本(解析→XML→XSLT 渲染 HTML)
2006 C++ 版 TPL(KSDN 2.0) Text Processing Language,类似 Boost Spirit 但更强;能力不弱于 LEX+YACC 却更轻量
2009 SDL(CERL 网络库) 用 TPL+JSPT 解析服务器网络协议描述语言
2011 转 Go 大部分 C++ 基础库被 Go 标准库取代
2015 Go 版 TPL + qlang 爬虫抽取结构化信息催生;顺手实现 qlang 语言、eql 模板
2017 BPL Binary Processing Language,处理二进制文档/TCP 协议流,依赖 qlang 才得以诞生

洞见(Viewer 为什么重要):先有 Viewer 理解格式、再设计 Reader 才合理。Reader 夹带大量业务逻辑会干扰对格式本身的理解,且不支持的功能没有解析代码;Viewer 则尽可能记录对格式的理解、形成可传承知识,还能输出纯文本结果供 diff 工具对比。

文本内容的处理范式

标准方式分两阶段:词法分析(Lex)语法分析(Parser),UNIX 提供经典的 lex 和 yacc。

阶段 角色 输入→输出 说明
词法分析 Lex Scanner / Tokenizer 字节流(Byte Stream)→ Token 流 很基础,平时不直接打交道(如 go/scannerScan()html.Tokenizer
语法分析 Parser Parser Token 流 → 结构 平时主要打交道的对象,输出 AST 等

Parser 的两种使用模型:

模型 机制 倾向 例子
SAX 基于事件 —— TPL 默认属于 SAX
DOM 基于结构化数据访问(如 AST 树) 通常更倾向 DOM go/parser 输出 AST、html.Parse 返回 *Node

TPL 实现了通用 Scanner+Parser:词法上抽象 Tokenizer 接口(内置类 Go 词法、增加 ? ~ @ 等操作符);语法上用类 EBNF 文法表达。其类 EBNF 的约定(前缀化,便于书写):

写法 含义 与教科书区别
*G / +G 重复 教科书是 G* / G+(后缀)
?G 可选 教科书是 G?
G1 G2 串接 不用逗号
G1 % G2 / G1 %= G2 列表(后者可空) ——
G/action G 匹配成功后执行动作(回调 Go 函数) 文法与执行代码尽量分离,比 yacc 可读

关键判断:TPL 默认是 SAX 模型,但在 extractor 模式G/action 被视为 G/marker,TPL 即变成 DOM 模型——同一套文法可在两种模型间切换。正则表达式是另一分支,简单场景方便,但可伸缩性与可读性都不强

二进制内容的处理范式

二进制处理整体"容易但繁琐"。其基础是序列化机制(二进制 I/O 框架),逐字段 readUint32 / readString...;C++ 靠操作符重载更简洁,但遇到可选/重复/数组就不简单(Go 缺泛型,没有现成 readArray,C++ 可用模板)。

关键判断(反对"智能"序列化):常规序列化提供的 Object 动态序列化/反序列化属于过度设计。它的便捷背后是让使用者放弃了对磁盘文件格式的思考数据是软件的灵魂,文件是软件最重要的资产——序列化最重要的是定义严谨的数据格式,而非耍智能。只保留序列化的形式即可。

由此诞生 BPL:用类 EBNF 文法(如 Foo = { N uint16; Bars [N]Bar })自动生成序列化代码。

特性 说明
设计核心 不破坏 TPL 的 EBNF 任何语义,作为 TPL 的扩展(“TPL 是 C,BPL 是 C++”)
能力 处理任意二进制文件,也处理任意 TCP 协议流;可轻松实现 Excel/DocViewer
文法贴近官方伪代码 以 MongoDB 线协议为例,BPL 文法几乎与官方伪代码一致({...} 为 Go 结构体文法,{/C ...} 为 C 结构体文法)
执行 当前仅解释执行(暂时),TPL 已有 generator 生成 Go 代码静态编译

总结

文本处理虽庞大,但可正交分解为两套范式:文本内容走 Lex(Scanner)→ Parser(SAX/DOM),TPL 用类 EBNF 文法统一表达;二进制内容走严谨的序列化,BPL 把文法扩展到二进制。更重要的元方法是:有意识地从日常业务场景中提炼通用需求,沉淀成自己的架构范式

先把"是什么"回答清楚

概念 一句话说明
Lex / Scanner 词法分析,字节流转 Token 流
Parser 语法分析,Token 流转结构
SAX / DOM 基于事件 / 基于结构化数据访问的两种 Parser 使用模型
TPL 通用文本处理语言,类 EBNF 文法,SAX 默认、extractor 下变 DOM
BPL 二进制处理语言,是 TPL 在二进制领域的扩展
Viewer 优先 先用 Viewer 固化格式理解,再设计 Reader

一句话速记

文本处理 = 词法(Scanner)+ 语法(Parser,DOM 优先);二进制处理 = 严谨序列化(拒绝"智能"),靠 TPL/BPL 把文法与执行分离,沉淀成可复用范式。

几条值得记住的判断

  • 先有 Viewer 再有 Reader:理解格式的代码不应被业务逻辑污染。
  • Object 动态序列化是过度设计,它让人放弃思考文件格式。
  • 文法与执行代码尽量分离(TPL 优于 yacc 之处),提升可读性。

思考题

你工作中是否有过"为了图省事用了某种自动/智能序列化,结果磁盘格式失控"的经历?反过来想:你常打交道的某类数据(日志、配置、协议),能否抽象出一套类 EBNF 的文法,让解析代码自动生成而非手写?

公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关