在XML技术规范中,文档类型定义(DTD)用来约束XML的结构与合法元素。当一份XML携带DTD时,解析器不仅要做基础的标签匹配,还可能进行合法性校验、实体展开以及外部资源获取。不同语言、不同解析库对带DTD的XML处理策略并不一致,若配置不当,轻则解析失败,重则引发外部实体注入或网络阻塞。本文围绕常见开发场景,说明解析带DTD的XML的具体操作方法与注意事项。

一、DTD的基本形式与解析器行为
DTD可以写在XML文件内部,也可以引用外部文件。内部DTD位于文档开头的<!DOCTYPE>声明中,外部DTD则通过SYSTEM或PUBLIC标识符指向某个URL或本地路径。多数解析器在遇见外部DTD时,默认会尝试读取对应资源,这意味着如果XML来自不可信环境,解析动作可能发起 outward 请求。
以Java标准库为例,SAXParserFactory创建的解析器在默认配置下会处理DTD并执行校验。如果网络不通或资源不存在,就会抛出IOException或SAXParseException。因此在实际工程中,我们往往需要根据业务诉求决定:是严格按DTD校验,还是仅解析数据而忽略DTD约束。
1.1 内部DTD与外部DTD的差异
内部DTD随文件一起分发,不存在额外的资源获取问题,但依然会参与结构校验。外部DTD便于多文件复用同一套规则,却引入了依赖管理与安全风险。在配置文件或报文交换场景中,很多系统采用PUBLIC标识配合本地Catalog映射,避免运行时访问外网。
理解这两种形态,有助于我们选择正确的解析参数。例如只想读取数据而不关心校验,就应关闭验证;若必须确保报文合规,则要保留DTD处理并准备好本地副本。
二、Java中使用SAXParser解析带DTD的XML
Java自带的SAXParser是事件驱动型解析器,适合处理大文件。通过SAXParserFactory设置Feature,可以精细控制DTD相关行为。以下示例展示如何关闭外部DTD加载,同时不抛出因缺少外部文件导致的异常。
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
import java.io.StringReader;
public class DtdParseDemo {
public static void main(String[] args) throws Exception {
String xml = "<?xml version='1.0'?>"
+ "<!DOCTYPE note SYSTEM 'http://ipipp.com/note.dtd'>"
+ "<note><to>A</to></note>";
SAXParserFactory factory = SAXParserFactory.newInstance();
// 关闭DTD校验,避免外部请求
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setValidating(false);
SAXParser parser = factory.newSAXParser();
DefaultHandler handler = new DefaultHandler();
parser.parse(new InputSource(new StringReader(xml)), handler);
System.out.println("解析完成,未加载外部DTD");
}
}
上述代码将load-external-dtd特性设为false,解析器不会去ipipp.com获取note.dtd,仅按XML结构读取内容。这种方式适用于只提取数据的场景,但无法发现不符合DTD定义的非法节点。
如果业务要求必须校验,可将setValidating设为true,并通过Catalog或本地副本提供DTD文件。此时解析器会对照DTD检查元素与属性,遇到不合规内容即报错,保障数据质量。
2.1 使用Catalog避免外部请求
在需要校验但又不想访问外网时,可使用XML Catalog将PUBLIC或SYSTEM标识映射到本地文件。Java中借助xml-resolver等库,在解析前设置EntityResolver,让解析器读取本地DTD。
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import java.io.FileInputStream;
EntityResolver resolver = new EntityResolver() {
public InputSource resolveEntity(String publicId, String systemId) {
if (systemId != null && systemId.endsWith("note.dtd")) {
try {
return new InputSource(new FileInputStream("/local/note.dtd"));
} catch (Exception e) {
return null;
}
}
return null;
}
};
// 将resolver设置到parser或factory对应方法中
这种方案兼顾了合规性与安全性,常用于金融报文、电子票据等强校验领域。缺点是需维护本地DTD版本,与上游规范保持同步。
三、Python中解析带DTD的XML操作方法
Python标准库的xml.etree.ElementTree默认不处理DTD,也不会展开外部实体,适合快速提取数据。但如果需要按DTD验证,应选用lxml库,它基于libxml2,支持DTD校验开关。
from lxml import etree
xml_text = '''<?xml version="1.0"?>
<!DOCTYPE note SYSTEM "http://ipipp.com/note.dtd">
<note><to>A</to></note>'''
parser = etree.XMLParser(dtd_validation=True, no_network=True)
try:
root = etree.fromstring(xml_text, parser)
print("校验通过", root.tag)
except etree.XMLSyntaxError as e:
print("DTD校验失败", e)
上例中dtd_validation=True开启校验,no_network=True禁止网络访问,防止解析器去ipipp.com拉取DTD。若本地无对应文件,将直接报错,因此需提前准备好DTD副本。
若仅想解析而不校验,使用标准库即可:etree.fromstring默认不加载DTD,不会因外部资源缺失而失败。这种轻量方式在爬虫或日志处理中十分常见。
3.1 防范外部实体注入
当XML包含恶意DTD定义时,可能通过SYSTEM实体读取本地文件或发起请求。Python的lxml可通过禁止实体解析来防护:设置resolve_entities=False,或采用defusedxml专用库。Java侧则应关闭外部通用实体特性,避免XXE漏洞。
from lxml import etree safe_parser = etree.XMLParser(resolve_entities=False, no_network=True) data = etree.fromstring(xml_text, safe_parser)
安全配置应作为解析带DTD的XML的基线要求,尤其当XML来自第三方系统时,任何默认开启的外部资源加载都可能成为攻击入口。
四、不同解析方式的选型建议
面对带DTD的XML,选型应基于业务对校验严格度、性能及安全的要求。下面用一张简表对比常见方案:
| 语言/库 | 默认DTD行为 | 是否支持校验 | 适用场景 |
|---|---|---|---|
| Java SAXParser | 可配置,默认可能加载外部 | 是 | 大文件、强校验 |
| Python xml.etree | 忽略DTD | 否 | 快速抽取数据 |
| Python lxml | 可配置校验与网络 | 是 | 需校验且控风险 |
若系统仅做数据互通而不关心结构约束,关闭DTD处理最为高效;若处于监管或强一致场景,则应保留校验并用本地Catalog支撑。无论哪种方式,禁止不可信外部网络请求都是必要前提。
综上,解析带DTD的XML并非单纯调用解析函数,而是要结合DTD形态、解析库特性与安全策略综合配置。掌握SAXParser与lxml的关键参数,便能在实际项目中稳妥处理各类XML报文。