什么是DTD及其在XML生态系统中的角色

文档类型定义(Document Type Definition,简称DTD)是XML和HTML文档中用于定义文档结构的机制。它是一套关于XML文档中允许包含哪些元素、元素之间如何嵌套、每个元素可以拥有哪些属性以及属性值的约束规则的规范。DTD作为XML 1.0规范的核心组成部分,为XML文档提供了结构验证的基础,确保数据交换的一致性和准确性。

在XML生态系统中,DTD扮演着”语法警察”的角色。当XML解析器处理文档时,如果文档引用了DTD,解析器会根据DTD中定义的规则来验证XML文档的结构是否合规。这种验证机制对于数据交换至关重要,特别是在B2B(企业对企业)集成、配置文件管理、文档格式标准化等场景中。

DTD的主要功能包括:

  1. 定义元素及其层次结构:规定文档中可以使用哪些元素,以及元素之间的父子关系
  2. 定义元素内容模型:指定元素可以包含文本、子元素还是空内容
  3. 定义属性列表:声明元素可以拥有的属性及其类型、默认值
  4. 定义实体:提供文本替换机制,包括预定义实体和自定义实体

尽管近年来XML Schema(XSD)和Relax NG等更强大的模式语言逐渐流行,DTD仍然因其简单性、广泛支持性和历史地位而在许多遗留系统和特定领域(如SGML兼容性、HTML文档类型声明)中保持重要地位。

DTD的基本语法结构

DTD可以内嵌在XML文档中(内部子集),也可以作为独立的外部文件引用(外部子集)。无论哪种形式,DTD都使用一套特定的语法来定义文档结构规则。

内部DTD声明

内部DTD直接包含在XML文档的序言部分,位于XML声明之后、根元素之前。其基本语法如下:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root-element [ <!-- DTD定义内容放在这里 --> ]> <root-element> <!-- XML文档内容 --> </root-element> 

外部DTD声明

外部DTD存储在单独的文件中(通常以.dtd为扩展名),XML文档通过SYSTEM或PUBLIC标识符引用它:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root-element SYSTEM "bookstore.dtd"> <root-element> <!-- XML文档内容 --> </root-element> 

DTD中的关键语法元素

DTD使用特定的声明语法,主要包含以下几种声明类型:

  1. ELEMENT声明:定义元素及其内容模型
  2. ATTLIST声明:定义元素的属性列表
  3. ENTITY声明:定义实体(文本替换)
  4. NOTATION声明:定义不可解析数据的格式
  5. COMMENT:注释(与XML注释语法相同)
  6. PROCESSING INSTRUCTION:处理指令(与XML处理指令语法相同)

所有DTD声明都以<!开头,以>结束。声明中的关键字(如ELEMENT、ATTLIST等)不区分大小写,但按照惯例通常使用大写。

元素声明(ELEMENT)详解

元素声明是DTD的核心,用于定义XML文档中允许使用的元素及其内容模型。元素声明的基本语法为:

<!ELEMENT element-name content-model> 

内容模型类型

元素的内容模型决定了该元素可以包含什么内容,主要分为以下几类:

1. 空元素(EMPTY)

空元素不能包含任何内容,通常用于表示标记或占位符。例如:

<!ELEMENT br EMPTY> <!ELEMENT img EMPTY> 

对应的XML使用:

<br/> <img src="logo.png" alt="Logo"/> 

2. 任意内容(ANY)

ANY表示元素可以包含任何可解析的内容(文本或其他元素)。这通常用于灵活性要求高的场景,但会削弱DTD的约束力:

<!ELEMENT note ANY> 

3. 混合内容(Mixed Content)

混合内容允许元素同时包含文本和子元素,使用#PCDATA(Parsed Character Data)表示可解析文本:

<!ELEMENT paragraph (#PCDATA | em | strong | a)*> 

这个声明表示<paragraph>元素可以包含文本、<em><strong><a>元素的任意组合(零次或多次)。

4. 元素内容(Element Content)

只包含子元素的元素,使用子元素名称和连接符定义结构:

<!ELEMENT book (title, author+, publisher?, chapters*)> 

这个声明使用了以下连接符:

  • 逗号(,):表示顺序出现(title必须在author之前)
  • 加号(+):表示一次或多次(author至少出现一次)
  • 问号(?):表示零次或一次(publisher可选)
  • 星号(*):表示零次或多次(chapters可选)

元素分组和选择

使用圆括号可以对元素进行分组,创建更复杂的内容模型:

<!ELEMENT section (title, (para | note | warning)+, (subsection*)?)> 

这里使用竖线|表示”或”关系,表示<section>必须包含一个<title>,然后是一个或多个paranotewarning的混合,最后可选一个或多个subsection

实际示例:书籍目录系统

让我们创建一个完整的书籍目录DTD示例:

<!ELEMENT bookstore (book+)> <!ELEMENT book (title, author+, publisher?, isbn?, chapters)> <!ELEMENT title (#PCDATA)> <!ELEMENT author (first-name, last-name)> <!ELEMENT first-name (#PCDATA)> <!ELEMENT last-name (#PCDATA)> <!ELEMENT publisher (#PCDATA)> <!ELEMENT isbn (#PCDATA)> <!ELEMENT chapters (chapter+)> <!ELEMENT chapter (title, section*)> <!ELEMENT section (title, para+)> <!ELEMENT para (#PCDATA | emphasis | link)*> <!ELEMENT emphasis (#PCDATA)> <!ELEMENT link EMPTY> 

对应的XML文档:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE bookstore SYSTEM "bookstore.dtd"> <bookstore> <book> <title>XML权威指南</title> <author> <first-name>Elliotte</first-name> <last-name>Rusty Harold</last-name> </author> <publisher>O'Reilly Media</publisher> <isbn>978-0596007645</isbn> <chapters> <chapter> <title>XML基础</title> <section> <title>什么是XML</title> <para>XML是<emphasis>可扩展标记语言</emphasis>的缩写。</para> </section> </chapter> </chapters> </book> </bookstore> 

属性声明(ATTLIST)详解

属性声明用于定义元素可以拥有的属性、属性类型以及默认值。属性声明的基本语法为:

<!ATTLIST element-name attribute-name attribute-type default-value > 

属性类型

DTD定义了多种属性类型,用于约束属性值的格式和内容:

1. 字符串类型(CDATA)

最通用的类型,表示属性值可以是任意字符串:

<!ATTLIST img src CDATA #REQUIRED alt CDATA #IMPLIED > 

2. 枚举类型(枚举值列表)

使用圆括号列出所有允许的值,值之间用竖线分隔:

<!ATTLIST book category (小说|技术|历史|科学) #REQUIRED language (中文|英文|日文|法文) "中文" > 

3. ID、IDREF和IDREFS类型

  • ID:唯一标识符,文档中必须唯一
  • IDREF:引用其他元素的ID
  • IDREFS:引用多个ID,用空格分隔
<!ATTLIST person id ID #REQUIRED > <!ATTLIST relation person1 IDREF #REQUIRED person2 IDREF #REQUIRED type (朋友|同事|家人) #REQUIRED > 

4. NMTOKEN和NMTOKENS类型

  • NMTOKEN:名称标记,由字母、数字、点、连字符、下划线组成
  • NMTOKENS:多个名称标记,用空格分隔
<!ATTLIST tag keywords NMTOKENS #IMPLIED > 

5. NOTATION类型

引用之前声明的NOTATION,用于标识外部数据的格式:

<!NOTATION gif SYSTEM "image/gif"> <!NOTATION png SYSTEM "image/png"> <!ATTLIST image format NOTATION (gif|png) #REQUIRED > 

默认值声明

属性声明的最后部分指定默认值行为:

  • #REQUIRED:属性必须提供
  • #IMPLIED:属性可选
  • #FIXED value:属性值固定,即使提供也会被忽略
  • “default-value”:提供默认值,如果XML中未指定则使用默认值
<!ATTLIST book id ID #REQUIRED status (available|checked-out) "available" edition CDATA #FIXED "1st" notes CDATA #IMPLIED > 

实际示例:增强书籍目录DTD

让我们为之前的书籍目录添加属性:

<!ATTLIST bookstore xmlns CDATA #FIXED "http://www.example.com/bookstore" > <!ATTLIST book id ID #REQUIRED category (小说|技术|历史|科学) #REQUIRED status (available|reserved) "available" language (中文|英文|日文) "中文" > <!ATTLIST author id ID #REQUIRED country CDATA "China" > <!ATTLIST link href CDATA #REQUIRED type (internal|external) "external" > 

对应的XML:

<bookstore xmlns="http://www.example.com/bookstore"> <book id="b001" category="技术" language="英文"> <author id="a001" country="USA">Elliotte Rusty Harold</author> <!-- ... --> </book> </bookstore> 

实体声明(ENTITY)详解

实体是DTD中的文本替换机制,可以简化文档编写、重用内容或引用外部资源。实体声明的基本语法为:

<!ENTITY entity-name "entity-value"> 

实体类型

1. 通用实体(General Entities)

通用实体用于在文档内容中进行替换,以&entity-name;形式引用:

<!ENTITY company "Acme Corporation"> <!ENTITY copyright "© 2024 &company;"> 

XML中使用:

<company>&company;</company> <copyright>&copyright;</copyright> 

2. 参数实体(Parameter Entities)

参数实体只能在DTD内部使用,以%entity-name;形式引用:

<!ENTITY % common.attributes "id ID #IMPLIED created CDATA #IMPLIED"> <!ATTLIST book %common.attributes;> <!ATTLIST author %common.attributes;> 

3. 外部实体(External Entities)

外部实体引用外部文件的内容:

<!ENTITY header SYSTEM "header.xml"> <!ENTITY footer SYSTEM "footer.xml" NDATA xml> 

4. 实体与符号(Notation)

实体可以与NOTATION结合,引用非XML格式的外部数据:

<!NOTATION gif SYSTEM "image/gif"> <!ENTITY logo SYSTEM "logo.gif" NDATA gif> 

预定义实体

XML预定义了五个实体,无需声明即可使用:

  • &lt;<
  • &gt;>
  • &amp;&
  • &apos;'
  • &quot; "

实际示例:使用实体的书籍目录

<!ENTITY % book.content "title, author+, publisher?, isbn?, chapters"> <!ENTITY % inline.formatting "em|strong|code"> <!ENTITY company "TechBooks Publishing"> <!ENTITY copyright "© 2024 &company;"> <!ELEMENT bookstore (book+)> <!ELEMENT book (%book.content;)> <!ELEMENT title (#PCDATA)> <!ELEMENT author (first-name, last-name)> <!ELEMENT first-name (#PCDATA)> <!ELEMENT last-name (#PCDATA)> <!ELEMENT publisher (#PCDATA | &company;)*> <!ELEMENT isbn (#PCDATA)> <!ELEMENT chapters (chapter+)> <!ELEMENT chapter (title, section*)> <!ELEMENT section (title, para+)> <!ELEMENT para (#PCDATA | %inline.formatting; | link)*> <!ELEMENT emphasis (#PCDATA)> <!ELEMENT link EMPTY> <!ATTLIST link href CDATA #REQUIRED> 

NOTATION声明

NOTATION用于声明不可解析数据的格式,通常与实体结合使用,标识外部资源的类型:

<!NOTATION notation-name SYSTEM "public-identifier"|"system-literal"> 

示例:

<!NOTATION gif SYSTEM "image/gif"> <!NOTATION png SYSTEM "2000/svg+xml"> <!NOTATION pdf SYSTEM "application/pdf"> <!ENTITY company-logo SYSTEM "logo.gif" NDATA gif> <!ATTLIST image src ENTITY #REQUIRED format NOTATION (gif|png) #REQUIRED > 

DTD验证实践

使用XML解析器进行验证

大多数编程语言都提供XML解析器支持DTD验证:

Python示例(使用lxml库)

from lxml import etree # 定义DTD字符串 dtd_content = ''' <!ELEMENT bookstore (book+)> <!ELEMENT book (title, author+, publisher?, isbn?, chapters)> <!ELEMENT title (#PCDATA)> <!ELEMENT author (first-name, last-name)> <!ELEMENT first-name (#PCDATA)> <!ELEMENT last-name (#PCDATA)> <!ELEMENT publisher (#PCDATA)> <!ELEMENT isbn (#PCDATA)> <!ELEMENT chapters (chapter+)> <!ELEMENT chapter (title, section*)> <!ELEMENT section (title, para+)> <!ELEMENT para (#PCDATA | emphasis | link)*> <!ELEMENT emphasis (#PCDATA)> <!ELEMENT link EMPTY> <!ATTLIST link href CDATA #REQUIRED> ''' # 创建DTD对象 dtd = etree.DTD(dtd_content) # 待验证的XML xml_content = ''' <bookstore> <book> <title>XML权威指南</title> <author> <first-name>Elliotte</first-name> <last-name>Rusty Harold</last-name> </author> <publisher>O'Reilly Media</publisher> <isbn>978-0596007645</isbn> <chapters> <chapter> <title>XML基础</title> <section> <title>什么是XML</title> <para>XML是<emphasis>可扩展标记语言</emphasis>的缩写。</para> </section> </chapter> </chapters> </book> </bookstore> ''' # 解析XML xml_doc = etree.fromstring(xml_content) # 验证 if dtd.validate(xml_doc): print("XML验证通过!") else: print("XML验证失败:") print(dtd.error_log) 

Java示例(使用javax.xml.validation)

import javax.xml.XMLConstants; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; import org.xml.sax.SAXException; import java.io.StringReader; import java.io.File; public class DTDValidator { public static void main(String[] args) { try { // 创建SchemaFactory并禁用外部实体(安全考虑) SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.XML_DTD_NS_URI); // 创建DTD源 StreamSource dtdSource = new StreamSource(new File("bookstore.dtd")); // 创建Schema Schema schema = factory.newSchema(dtdSource); // 创建Validator Validator validator = schema.newValidator(); // 待验证的XML String xmlContent = """ <bookstore> <book> <title>XML权威指南</title> <author> <first-name>Elliotte</first-name> <last-name>Rusty Harold</last-name> </author> <publisher>O'Reilly Media</publisher> <isbn>978-0596007645</isbn> <chapters> <chapter> <title>XML基础</title> <section> <title>什么是XML</title> <para>XML是<emphasis>可扩展标记语言</emphasis>的缩写。</para> </section> </chapter> </chapters> </book> </bookstore> """; // 验证 validator.validate(new StreamSource(new StringReader(xmlContent))); System.out.println("XML验证通过!"); } catch (SAXException e) { System.err.println("XML验证失败:" + e.getMessage()); } catch (Exception e) { e.printStackTrace(); } } } 

JavaScript/Node.js示例(使用libxmljs)

const libxmljs = require('libxmljs'); // DTD定义 const dtdContent = ` <!ELEMENT bookstore (book+)> <!ELEMENT book (title, author+, publisher?, isbn?, chapters)> <!ELEMENT title (#PCDATA)> <!ELEMENT author (first-name, last-name)> <!ELEMENT first-name (#PCDATA)> <!ELEMENT last-name (#PCDATA)> <!ELEMENT publisher (#PCDATA)> <!ELEMENT isbn (#PCDATA)> <!ELEMENT chapters (chapter+)> <!ELEMENT chapter (title, section*)> <!ELEMENT section (title, para+)> <!ELEMENT para (#PCDATA | emphasis | link)*> <!ELEMENT emphasis (#PCDATA)> <!ELEMENT link EMPTY> <!ATTLIST link href CDATA #REQUIRED> `; // 待验证的XML const xmlContent = ` <bookstore> <book> <title>XML权威指南</title> <author> <first-name>Elliotte</first-name> <last-name>Rusty Harold</last-name> </author> <publisher>O'Reilly Media</publisher> <isbn>978-0596007645</isbn> <chapters> <chapter> <title>XML基础</title> <section> <title>什么是XML</title> <para>XML是<emphasis>可扩展标记语言</emphasis>的缩写。</para> </section> </chapter> </chapters> </book> </bookstore> `; // 创建DTD文档 const dtdDoc = libxmljs.parseXml(dtdContent, { dtd: true }); // 创建XML文档 const xmlDoc = libxmljs.parseXml(xmlContent); // 验证 const isValid = xmlDoc.validate(dtdDoc); if (isValid) { console.log("XML验证通过!"); } else { console.log("XML验证失败:"); console.log(xmlDoc.validationErrors); } 

常见验证错误及解决方案

错误1:元素内容不符合声明

错误信息Element book content does not follow the DTD, expected (title, author+, publisher?, isbn?, chapters)

原因:元素子元素顺序、数量或类型错误

解决方案:检查XML中元素的子元素是否符合DTD声明的顺序和数量要求

错误2:属性缺失或类型错误

错误信息Attribute "id" is required for element "book"

原因:声明为#REQUIRED的属性未提供,或属性值不符合类型约束

解决方案:确保所有#REQUIRED属性都已提供,且值符合类型要求

错误3:未声明的元素或属性

错误信息Element "unknown" is not declared

原因:XML中使用了DTD中未声明的元素或属性

解决方案:在DTD中添加相应声明,或修改XML移除未声明的内容

DTD最佳实践和注意事项

1. 使用外部DTD提高可维护性

将DTD放在独立文件中,便于多文档共享和版本控制:

<!DOCTYPE book SYSTEM "book.dtd"> 

2. 合理使用参数实体

参数实体可以减少重复代码,提高DTD的可维护性:

<!ENTITY % common.attributes "id ID #IMPLIED created CDATA #IMPLIED updated CDATA #IMPLIED"> <!ENTITY % metadata "meta*"> <!ATTLIST book %common.attributes;> <!ATTLIST author %common.attributes;> 

3. 避免过度复杂的嵌套

保持内容模型简单清晰,避免过深的嵌套和复杂的组合:

<!-- 避免 --> <!ELEMENT complex ((a,b,c)|(d,e,f)|(g,h,i))> <!-- 推荐 --> <!ELEMENT complex (choice1 | choice2 | choice3)> <!ELEMENT choice1 (a,b,c)> <!ELEMENT choice2 (d,e,f)> <!ELEMENT choice3 (g,h,i)> 

4. 安全考虑

禁用外部实体解析以防止XXE(XML External Entity)攻击:

# Python中禁用外部实体 parser = etree.XMLParser(resolve_entities=False) 

5. 与XML Schema的对比

特性DTDXML Schema
数据类型有限(CDATA、ID等)丰富(string、integer、date等)
命名空间支持有限完整支持
复杂度简单复杂
可扩展性有限强大
学习曲线平缓陡峭

总结

DTD作为XML结构验证的基础机制,虽然功能相对简单,但在许多场景下仍然非常有用。掌握DTD语法对于理解XML文档结构、维护遗留系统以及处理特定领域(如HTML、SGML兼容)的文档至关重要。

通过本文的学习,您应该能够:

  1. 理解DTD的基本概念和作用
  2. 编写元素声明、属性声明和实体声明
  3. 创建完整的DTD文档
  4. 使用编程语言进行DTD验证
  5. 遵循DTD最佳实践

尽管现代XML模式语言提供了更强大的功能,但DTD的简洁性和广泛支持使其在XML生态系统中仍占有一席之地。在实际项目中,根据具体需求选择合适的验证机制(DTD、XSD或Relax NG)是确保数据质量和互操作性的关键。