XML实体是文档类型定义中一种常用的替换机制。声明实体之后,文档内容只需使用实体引用,解析器便会把引用替换成声明中对应的值。这种方式可以显著减少重复内容,也方便统一维护某些文本片段或外部资源。实体声明并不复杂,难点在于区分普通实体与参数实体、内部实体与外部实体,以及了解不同解析器对安全策略的差异。

一、实体声明的语法与位置
XML 实体必须在文档类型定义中声明。文档类型定义可以写在 XML 文档内部的方括号中,称为内部子集;也可以放在独立的外部 DTD 文件中,通过 SYSTEM 或 PUBLIC 关键字引用。内部子集的基本结构是 <!DOCTYPE> 后接根元素名称,再使用方括号包含一个或多个实体声明。实体声明使用 <!ENTITY> 关键字,后面依次是实体名称和实体值。
实体名称需要符合 XML 名称规范,通常以字母或下划线开头,不能包含空格和部分标点。实体值如果是内部文本,必须放在一对双引号中;如果文本里包含双引号,可以使用单引号包裹值,或者使用字符引用。实体声明后,在 XML 正文或其他声明中通过 &实体名; 的形式引用。
下面是一个包含内部子集的简单 XML 文档,声明了两个普通实体,并在元素内容中引用它们。实际解析时,&company; 会被替换成实体值,&email; 也会被替换为对应的联系地址,从而减少重复书写。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root [ <!ENTITY company "IPipp"> <!ENTITY email "support@ipipp.com"> ]> <root> <company>&company;</company> <contact>&email;</contact> </root>
二、普通实体的声明与引用
普通实体主要分为内部实体和外部实体。内部实体把具体内容直接写在声明中,适合替换固定文本片段。声明格式为 <!ENTITY 名称 "值">,例如把公司名称声明为实体后,文档中多处使用 &company;,将来修改名称时只需要调整声明值,不需要逐处修改。
内部实体不仅能包含纯文本,也可以包含标记,只要替换后仍能保持文档结构合法。例如某个实体可以声明为 <!ENTITY address "<address>北京市海淀区</address>">。需要注意的是,实体值中的尖括号在 XML 解析时会被识别为标记的一部分,如果希望它作为文本显示,则应使用预定义实体或字符引用。内部实体的优势是可读性好、无需额外资源,缺点是这些内容只能存在于当前 DTD 中,无法跨文档复用。
外部实体则把内容指向一个外部资源,例如本地文件或远程 URL。声明格式为 <!ENTITY chapter1 SYSTEM "chapter1.xml">,如果使用公共标识,可以写 PUBLIC 后接公共 ID 和后备 URI。外部实体适合把大段内容拆分到独立文件,但解析行为受解析器安全配置影响,默认开启外部实体读取的解析器可能存在安全风险。
三、参数实体与DTD模块化
参数实体与普通实体的最大区别在于使用范围。参数实体只在 DTD 内部有效,不能在 XML 正文中引用。声明时在 ENTITY 关键字和名称之间增加百分号,例如 <!ENTITY % commonAttrs "id ID #IMPLIED">。引用时使用 %commonAttrs;,而不是普通实体所用的与号和分号格式。
参数实体最常见的用途是复用 DTD 片段。一个较大的 DTD 可能包含多个元素和属性列表声明,这些声明中经常出现重复的属性集合。通过参数实体把公共属性提取出来,后续多个 <!ATTLIST> 声明可以直接引用。参数实体还可以指向外部 DTD 文件,实现跨文档的 DTD 模块化管理。
<!DOCTYPE root [
<!ENTITY % commonAttrs "id ID #IMPLIED">
<!ELEMENT root (item*)>
<!ELEMENT item (#PCDATA)>
<!ATTLIST item
%commonAttrs;
>
]>
<root>
<item id="a1">content</item>
</root>
外部参数实体的声明和引用也遵循类似规则。一个典型做法是把所有元素声明放在 common.dtd 文件中,再在主 DTD 中通过 <!ENTITY % ext SYSTEM "common.dtd"> 声明,紧跟着写上 %ext; 完成加载。这种方式能提高 DTD 的可维护性,但也意味着解析器需要访问外部文件,因此部署时要保证路径正确和权限可控。
四、预定义实体、字符引用与安全风险
XML 规范预定义了五个实体,分别是表示小于号的 <、大于号的 >、与号的 &、单引号的 ' 和双引号的 "。这些实体不需要在 DTD 中声明,XML 解析器会直接识别。除此之外,XML 还支持使用字符引用直接表示任意 Unicode 字符,十进制格式为 A,十六进制格式为 A,两者都表示字符 A。
字符引用与实体引用容易混淆。字符引用不需要声明,它通过数字直接定位字符;而实体引用需要事先在 DTD 中声明,或者使用 XML 预定义实体。需要特别注意的是,如果文档里出现裸的与号,解析器会尝试把它识别为实体引用开头,导致解析失败。因此文本中要表示与号本身时,必须写成 & 或使用字符引用。
外部实体的安全风险主要集中在 XXE 漏洞。攻击者可以在实体声明中把资源指向本地敏感文件,例如 <!ENTITY xxe SYSTEM "file:///etc/passwd">,随后在文档中引用该实体,解析器如果未禁用外部实体解析,就可能把文件内容拼进结果。现代 XML 解析库通常默认禁止或提供开关关闭外部实体和参数实体解析,在处理来自不可信来源的 XML 时,应显式配置这些选项。
<!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <root>&xxe;</root>
总体来看,XML 实体声明方法围绕 DTD 展开,区分普通实体、参数实体、内部实体和外部实体是理解的关键。日常使用中优先用内部实体处理固定文本,用参数实体复用 DTD 声明,同时严格控制外部实体的解析行为。掌握这些点后,可以在内容维护和文档结构设计上更得心应手,也能有效规避解析错误与安全风险。