DTD(Document Type Definition,文档类型定义)是 XML 从 SGML 继承的第一代契约语言,用紧凑的专用语法声明元素结构与属性规则。它不能约束数据类型、不认识命名空间,但存量巨大——HTML 的文档类型声明、大量早期行业报文标准至今仍是 DTD。本节解剖它的元素声明、属性声明与实体机制,并做一次完整校验。
一份图书库存的 DTD:
<!ELEMENT inventory (supplier, book+)> <!-- 元素列表:inventory 必须先有一个 supplier,后跟一个或多个 book --> <!ELEMENT supplier (#PCDATA)> <!-- #PCDATA:可解析的字符数据,即纯文本 --> <!ELEMENT book (title, stock, price?)> <!-- title 必须一个;price 用 ? 标记,零个或一个 --> <!ELEMENT title (#PCDATA)> <!ELEMENT stock (#PCDATA)> <!ELEMENT price (#PCDATA)>
四个次数符号是一切结构表达的积木:+ 一或多、* 零或多、? 零或一、无符号恰好一。组合语法三种:(a, b) 顺序序列;(a | b) 二选一;(a | b)* 任意混排任意次——最后一种最宽松,等于放弃顺序约束,"混合内容"文档(正文嵌标签)只能用它。
一个容易被忽略的坑:(title, stock, price?) 里 title、stock 都是必选。DTD 的"必选"只看出现与否,<stock></stock>(空标签)也算出现——内容是不是数字、是不是为空,DTD 一概不管,这是它最大的能力缺口。
<!ATTLIST book sku CDATA #REQUIRED <!-- CDATA 此处指普通文本;必填 --> category CDATA "GENERAL"> <!-- 缺省值:不写就按 GENERAL --> <!ENTITY warning "本数据仅限内部使用"> <!-- 内部实体:文档里写 &warning; 处会被替换成这句话 -->
#REQUIRED、#IMPLIED(可选)、#FIXED(固定值)是三种出现约束;类型上 CDATA 之外还有枚举写法 (FRESH|FROZEN),这是 DTD 唯一像"类型"的能力。实体(ENTITY)是文本替换宏,能把重复的长串收拢成短名,也用来做特殊字符的第二套转义。参数实体(% 开头)能把 DTD 自身模块化复用,大型行业标准常用。
DTD 引用进文档有两种方式:
<!-- 内部子集:契约直接嵌在文档里,适合单文件自包含 --> <!DOCTYPE inventory [ <!ELEMENT inventory (book+)> ]> <!-- 外部子集:契约独立成文件,SYSTEM 指明位置 --> <!DOCTYPE inventory SYSTEM "inventory.dtd">
背景:给上面的库存文档配 DTD 并校验。操作:文档写成——
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE inventory SYSTEM "inventory.dtd"> <inventory> <supplier>华东书源</supplier> <book sku="9787111407010"> <title>算法导论</title> <stock>42</stock> <price>128.00</price> </book> </inventory>
用 xmllint(命令行校验器,第 7 章详述)执行校验:
命令:xmllint --valid --noout inventory.xml 输出(通过):无输出,退出码 0 输出(把 stock 删掉再跑): inventory.xml:6: element book: validity error : Element book content does not follow the DTD, expecting (title , stock , price?), got (title , price?)
结果解读:报错给出文档行号、违反的元素、契约期待的序列与实际序列——排错信息相当友好。变式:把 sku 属性删掉,会得到 attribute sku of book: validity error: lack of attribute 类报错,#REQUIRED 生效。
| 能力 | DTD | 说明 |
|---|---|---|
| 结构与次数 | 有 | 完整支持 |
| 枚举型属性 | 有 | 唯一的类型手段 |
| 数据类型 | 无 | 数字日期都是文本 |
| 命名空间 | 无 | 混入命名空间即失效 |
| 契约自身可校验 | 无 | DTD 不是 XML,写错难发现 |
新契约不该选 DTD 的理由全在表的后两行。但要会读它的现实理由同样充分:改一个老系统的 DOCTYPE、看懂 HTML 页面顶部那行声明、维护早期 SOAP 存量,都绕不开。
补一句历史脉络帮助记忆:DTD 比 XML 更老——它是从 SGML 原样继承来的,XML 只是给这个旧规程换了新宿主。这也解释了它"不像 XML"的长相:元素声明、属性列表都是 SGML 时代的语法,当年并没有"用 XML 描述 XML"的想法。等到业界意识到契约自身也该结构化、也该校验时,XSD 才作为"用 XML 写的契约"诞生。知道这层演进,两代规程的语法差异就不再是需要硬记的知识点,而是技术史的自然结果——老工具解决老问题,新需求催生新形态,软件世界的常态而已。
💡 把 DTD 当合同里的"附录一"读:正文(XML)人人可读,附录(契约)定死了条款细节;老合同的附录格式过时了,但你签过字的合同还得能读懂。
+ * ? 与无符号,是 DTD 结构表达的全部积木;第一代规程解剖完,它的两个缺口正是第二代规程的卖点。下一节写一份真正的 XSD。