上下文无关文法的应用——DTD 文件

在讲 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 的内部
  • pspanscript 等标签也应该放在 headbody 内部

这一系列的规则就是 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 

参考文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注