在讲 DTD 文件之前,我们还得先谈谈 XML 文档。
XML 文档
对于大部分程序员来说,经常需要与 XML 文档打交道。什么时候会用到 XML 文档?我想最常见的场景当属互联网中数据的传输,以及各种开发配置文件了。
例如,我们要在互联网中传输商品信息,可以直接使用 XML 文档来存储:
item.xml
<?xml version="1.0" encoding="utf-8"?>
<商品>
<名称>XXX笔记本电脑</名称>
<价格>20000</价格>
<库存>300</库存>
<配置>
<处理器>i7</处理器>
<内存>8G</内存>
<硬盘>
<固态硬盘>256G</固态硬盘>
</硬盘>
<硬盘>
<机械硬盘>500G</机械硬盘>
</硬盘>
</配置>
</商品>
这是一个没有 DTD 约束的普通 XML 文档,该文档很清晰地描述了该商品的各种信息,解析起该文件也没什么问题。但如果我们要传输的是一件衣服的信息呢?那么包含的属性就不一样了,因为对于一件衣服而言,并不需要 配置
这个属性。
如何知道那些属性是合法的,那些属性是非法的?也就是说,在语法没有错误的基础上,我们什么时候应该使用哪些标签才是正确的?
像上面的例子中,如果我们在 <商品>
内部添加一个 身高
,语法上并没有错误,但语义上就存在问题了。
但是 XML 本身只能检测语法是否有错误,不能检查语义是否出错。
为了能够进一步约束 XML,于是就有了 DTD 文件。
DTD 文件
DTD 文件(全称 Document Type Definition,即文档类型定义)存放的是一组约束条件,这些约束条件采用的是上下文无关文法(Context-Free Grammar, CFG),对于标签之间的派生关系,以及标签可以出现的位置和数量进行了定义。
类比 HTML 约束条件
什么是约束条件?html 文档与 XML 非常类似,可以视为 XML 的特例。我们看看 html 文档都有哪些约束:
- 文档的根元素为
html
- 头部为
head
- 主体部分为
body
title
标签应该放在head
的内部p
、span
、script
等标签也应该放在head
或body
内部
这一系列的规则就是 html 文档的约束条件。而作为一个比 html 更为通用的标记语言,xml 本身并没有对标签进行层次结构上的约束,只要语法正确,你可以编写任意的标签,进行任意的嵌套。如果要进行约束,就要声明其使用的 DTD 文件。
DTD 语法
针对最开始给出的商品信息的示例,我们想要进行如下的约束:XML 文档的根元素为 商品
,商品
的属性只能是 名称
、配置
、价格
、库存
,其中 配置
是可选的,且 配置
中的 硬盘
可以有多个。
根据上述约束条件,我们可以编写相应的 DTD 文件:
item.dtd
<?xml version="1.0" encoding="utf-8"?>
<!ELEMENT 商品 (名称, 价格, 库存, 配置?)>
<!ELEMENT 配置 (处理器, 内存, 硬盘+)>
<!ELEMENT 硬盘 (固态硬盘 | 机械硬盘)>
<!ELEMENT 名称 (#PCDATA)>
<!ELEMENT 价格 (#PCDATA)>
<!ELEMENT 库存 (#PCDATA)>
<!ELEMENT 处理器 (#PCDATA)>
<!ELEMENT 内存 (#PCDATA)>
<!ELEMENT 固态硬盘 (#PCDATA)>
<!ELEMENT 机械硬盘 (#PCDATA)>
回顾一下 CFG,终结符 表示的是不能继续派生的符号,即常量值;变量 表示的是可以继续派生出其它变量或终结符的符号。
再来看这个 DTD 文件中所用到的语法:
<!ELEMENT NAME (CONTENT)>
定义变量,括号中为该变量派生的内容,可以是变量也可以是终结符?
表示变量可出现一次或不出现+
表示变量出现一次或一次以上|
表示可派生左边的变量,也可派生右边的变量#PCDATA
表示常量值,对应 CFG 中的终结符,即 XML 中标签内部的存储内容
上述约束条件用 CFG 产生式 可描述为:
商品 -> 名称 价格 库存 | 名称 配置 价格 库存
配置 -> 处理器 内存 硬盘 | 配置 硬盘
硬盘 -> 固态硬盘 | 机械硬盘
名称 -> #PCDATA
价格 -> #PCDATA
库存 -> #PCDATA
处理器 -> #PCDATA
内存 -> #PCDATA
固态硬盘 -> #PCDATA
机械硬盘 -> #PCDATA
注意:在 DTD 文件中,一个变量要么只派生变量,要么只派生终结符,不能同时派生出变量和终结符。
派生出的变量之间是有顺序关系的,例如商品可以派生“名称, 价格, 库存”,那么在 XML 文件中,这三个标签必须按照这个先后顺序编写,否则将违反该约束条件。
在 XML 中引入 DTD 文件
<?xml version="1.0" encoding="utf-8"?>
<!-- 引入 DTD 文件 -->
<!DOCTYPE 商品 SYSTEM "item.dtd">
<商品>
<名称>XXX笔记本电脑</名称>
<价格>20000</价格>
<库存>300</库存>
<配置>
<处理器>i7</处理器>
<内存>8G</内存>
<硬盘>
<固态硬盘>256G</固态硬盘>
</硬盘>
<硬盘>
<机械硬盘>500G</机械硬盘>
</硬盘>
</配置>
</商品>
特别注意:包含中文等非 ASCII 字符时,需要确保文件中注明的编码与实际文件编码一致,否则在校验 XML 有效性时可能因无法解析文件而出错。
<!DOCTYPE
用于声明文档约束,其格式如下:
<!DOCTYPE 根元素>
<!DOCTYPE 根元素 SYSTEM "本地DTD文件路径">
<!DOCTYPE 根元素 PUBLIC "DTD标识符" "DTD文件URL">
第一种只标识文档的根元素,示例:
<!DOCTYPE html>
第二种引用本地 DTD 文件,示例:
<!DOCTYPE 商品 SYSTEM "item.dtd">
第三种引用网络 DTD 文件,示例:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems,Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
利用 IE 校验 XML
下面是一个基于 js 的简单校验工具,利用 IE 中 XML 对象的解析功能对 XML 文件进行校验。
由于该工具利用的是 IE 的 XML 对象,需要用 IE 浏览器打开该文件才能生效。
checkXml.html
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script language="javascript">
// 创建xml文档解析器对象
var xmldoc = new ActiveXObject("Microsoft.XMLDOM");
// 开启xml校验
xmldoc.validateOnParse = "true";
// 装载xml文档,即指定校验哪个XML文件
xmldoc.load("item.xml");
document.writeln("错误信息:" + xmldoc.parseError.reason + "<br>");
document.writeln("错误行号:" + xmldoc.parseError.line);
</script>
</head>
<body>
</body>
</html>
校验成功时,显示内容如下:
错误信息:
错误行号:0
当删除了 item.xml 中的 <库存>
标签时,校验失败,提示出错信息:
错误信息:根据 DTD/Schema,元素内容无效。 预期: 库存。
错误行号:8
参考文章
作者:Wray Zheng
原文:http://www.codebelief.com/article/2018/01/application-of-context-free-grammar-dtd-file/