自己设计XML格式其实很简单,随手写一个标签嵌套的文档就能用。但一旦这个格式要在多个系统之间交换数据,问题就来了:对方发来的文档里少了一个必填字段怎么办?子元素的顺序乱了怎么办?靠口头约定或者文档说明来约束,永远不如让机器自动校验来得可靠。DTD(Document Type Definition,文档类型定义)就是XML自诞生起就内置的一套结构描述机制,写好一份DTD,任何符合规范的解析器都能据此判断一份XML文档是否合法。本文带你从零开始为自己的XML格式编写一份完整的DTD。

一、DTD能约束什么:先弄清它的能力边界
在动手写之前,先要清楚DTD到底能管哪些事。DTD的核心能力有三个:约束元素的结构(有哪些子元素、顺序如何、出现几次)、约束属性(属性名、类型、默认值、是否必须)以及定义实体(可复用的文本替换)。这三个能力覆盖了大多数数据格式校验的需求。
但DTD也有明显的短板。它没有数据类型的概念,无法区分一个元素的内容是整数还是日期,只能笼统地声明为文本;它不支持命名空间,对复杂的复合结构表达力有限;它不能约束某个元素的内容必须满足某种格式(比如必须是11位手机号)。如果这些限制你能接受,DTD依然是轻量、通用、所有解析器都支持的最好选择;如果需要强类型校验,那就该考虑XML Schema了,这一点后文会详细对比。
二、元素声明:DTD的核心语法
元素声明使用<!ELEMENT>指令,格式为<!ELEMENT 元素名 内容模型>。最简单的情况是声明一个只包含文本的元素,用(#PCDATA)表示,例如<!ELEMENT title (#PCDATA)>表示title元素内部只能是纯文本,不能再嵌套其他标签。
如果元素包含子元素,就要写内容模型。DTD提供了几种符号控制子元素的出现方式:逗号表示按顺序出现,竖线表示二选一,问号表示零次或一次,星号表示零次或多次,加号表示一次或多次。比如<!ELEMENT book (title, author+, price?)>表示book元素必须依次包含一个title、一个或多个author、以及一个可选的price。掌握这几个符号,基本就能表达绝大多数结构需求了。
<!-- 只含文本的元素 --> <!ELEMENT title (#PCDATA)> <!-- 按顺序出现的子元素,price可选 --> <!ELEMENT book (title, author+, price?)> <!-- 二选一:电子书或纸质书 --> <!ELEMENT book (title, (ebook | paperback))> <!-- 任意内容:文本加任意子元素 --> <!ELEMENT description ANY> <!-- 空元素,比如图片占位 --> <!ELEMENT cover EMPTY>
还有两个特殊的取值:ANY表示任意内容,几乎不做约束,一般只在过渡阶段使用;EMPTY表示空元素,常用于<cover/>这类只靠属性携带信息的标签。建议在设计阶段尽量少用ANY,否则DTD的校验价值会大打折扣。
三、属性声明与实体定义
属性用<!ATTLIST>声明,格式是<!ATTLIST 元素名 属性名 属性类型 约束>。属性类型常用的是CDATA(任意文本)、ID(文档内唯一标识)、IDREF/IDREFS(引用其他ID)、NMTOKEN(受限的字符串)。约束部分可以写#REQUIRED(必须出现)、#IMPLIED(可选)或者直接写默认值,写默认值时还可以加#FIXED表示值固定不可改。
<!ATTLIST book
id ID #REQUIRED
category CDATA #IMPLIED
lang CDATA "zh">
<!!-- cover是空元素,图片地址放属性里 -->
<!ATTLIST cover
url CDATA #REQUIRED
format (jpg|png|webp) "jpg">实体定义有点像编程里的常量,用<!ENTITY>声明后在文档中以&实体名;的形式引用。它最大的用处是定义可复用的文本片段,或者把特殊字符包装起来。比如版权信息在多处出现,定义一个实体引用即可,改的时候只改一处。
<!ENTITY copyright "Copyright 2024 Example Press"> <!ENTITY lt "<"> <!-- 文档中使用 --> <footer>©right;</footer>
四、实战:为图书管理系统编写完整DTD
现在把前面的语法串起来,为一个图书管理格式编写完整的DTD。假设文档的根元素是library,下面挂多本书,每本书有标题、若干作者、价格、封面,书籍必须有唯一编号。我们先写成内部DTD,直接嵌入XML文档头部,方便快速测试。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE library [
<!ELEMENT library (book+)>
<!ELEMENT book (title, author+, price?, cover?)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT author (#PCDATA)>
<!ELEMENT price (#PCDATA)>
<!ELEMENT cover EMPTY>
<!ATTLIST book id ID #REQUIRED>
<!ATTLIST cover url CDATA #REQUIRED>
<!ATTLIST price currency CDATA "CNY">
]>
<library>
<book id="b001">
<title>数据结构</title>
<author>张三</author>
<price>59.00</price>
</book>
<book id="b002">
<title>编译原理</title>
<author>李四</author>
<author>王五</author>
</book>
</library>格式稳定之后,应该把DTD抽成独立文件供多方共享,这就是外部DTD。DTD文件单独保存为book.dtd,XML文档通过SYSTEM或PUBLIC标识引用它。SYSTEM后面跟具体路径,PUBLIC用于公开的标准格式,格式为PUBLIC 公共标识符 系统标识符。
<!-- 引用本地DTD文件 --> <!DOCTYPE library SYSTEM "book.dtd"> <!-- 引用公开标准DTD的写法 --> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
写完之后一定要验证。很多编辑器比如VS Code装上XML插件后可以实时提示校验错误;用Python的话,lxml库几行代码就能完成校验,下面这个脚本会打印文档是否合法以及具体错误位置。
from lxml import etree
dtd = etree.DTD("book.dtd")
doc = etree.parse("books.xml")
if dtd.validate(doc):
print("文档合法")
else:
for error in dtd.error_log:
print(error)五、DTD与XML Schema怎么选
最后聊聊选型问题。DTD的优势在于语法简单、篇幅短、历史久远、所有XML工具链都支持,XHTML、早期配置文件格式大量使用它。缺点前面提过:无数据类型、无命名空间支持、语法自成一体和XML风格不一致。
XML Schema(XSD)则补齐了这些短板,支持几十种内置数据类型、自定义类型、命名空间、更精细的出现次数控制,写法本身就是规范的XML。代价是语法复杂得多,一份功能等价的XSD往往比DTD长好几倍,学习成本也高。如果格式简单、只需管住结构和属性,DTD足够了;如果要做严格的数值范围、日期格式、跨命名空间校验,就直接上XSD。此外还有更轻量的RELAX NG,语法比XSD友好,也值得了解。
总的来说,为XML格式编写DTD的思路是:先画出理想文档的样例,再从根元素往下逐层声明元素和属性,最后用工具实际校验几份文档查漏补缺。一套好的结构约束,能让数据交换双方省去大量沟通成本,这比任何口头约定都靠谱。