非法XML实体指未声明的命名实体(如&unknown;)或格式错误/超范围的字符引用(如?),直接用正则删除易误伤URL、注释等合法内容;应先用pugixml等解析器定位非法实体起始位置,再精准替换,避免解析器报错。

什么是非法XML实体,为什么不能直接用正则删
XML只允许五种预定义实体:&、<、>、"、'。其他形如 、&unknown;、 的都属于非法实体——它们要么是未声明的命名实体,要么是超出Unicode范围或格式错误的字符引用。
别急着上 std::regex_replace 匹配 &[^;]+;,这会误杀合法内容:比如 URL 中的 &id=123、注释里的 (即使没声明,人眼也认得),甚至带分号的普通文本 abc&def;。更糟的是,XML解析器在遇到非法实体时通常直接报错(如 libxml2 抛 XML_ERR_INVALID_CHAR),根本不会等你后期清理。
真正安全的做法是:先做一次轻量级 XML 解析校验,再针对性替换。不依赖完整 DOM,只用 SAX 或事件式扫描即可。
用 pugixml 快速检测并清理非法实体
pugixml 是头文件库,轻量且默认启用实体解析校验。它会在加载时自动拒绝非法实体——但关键是,你可以捕获这个过程,把“失败点”前的内容保留下来,再手动修复。
立即学习“C++免费学习笔记(深入)”;
- 启用
pugi::parse_escapes和pugi::parse_eol,禁用pugi::parse_wconv_attribute(避免宽字符干扰) - 用
pugi::xml_parse_result检查result.status == pugi::status_bad_entity,此时result.offset指向非法实体起始位置 - 从该 offset 往前找最近的
&,往后找最近的;,提取疑似非法片段,用空字符串替换 - 重复直到解析成功(最多 3 轮,防死循环)
std::string clean_xml_entities(std::string s) {
pugi::xml_document doc;
for (int i = 0; i < 3; ++i) {
pugi::xml_parse_result res = doc.load_string(s.c_str());
if (res.status == pugi::status_ok) return s;
if (res.status != pugi::status_bad_entity) break;
size_t off = res.offset;
size_t start = s.rfind('&', off);
size_t end = s.find(';', off);
if (start == std::string::npos || end == std::string::npos || end < start) break;
s.replace(start, end - start + 1, "");
}
return s;
}
纯手工扫描:绕过解析器的底线方案
如果你不能引入任何第三方(比如嵌入式环境),就得自己写状态机扫描。核心逻辑只有三步:
- 状态
normal:逐字复制,遇&进入in_entity - 状态
in_entity:收集字符直到;;若中间出现非字母/数字/#/x,或长度超 10,判定为非法,跳过整段 - 合法情况仅接受:
&、<、>、"、',或{、J(需验证码点 ≤ 0x10FFFF 且非代理对、控制字符)
注意: 是空字符,XML 禁止; 是高位代理,非法;(0x110000)超 Unicode 上限,也得剔除。这些检查不难,但漏掉任一条件都会导致后续解析崩溃。
libxml2 用户:别调 xmlParseDoc 做清理
xmlParseDoc 遇到非法实体直接返回 NULL,不提供修复接口。想“边解析边修”,必须用 xmlSAXHandler 自定义 handler,并在 characters 回调里检查缓冲区是否含未闭合 & 或孤立 ;。但这容易和 CDATA、注释混淆——比如 <![CDATA[¬]]> 里的 ¬ 是字面量,不该动。
更稳妥的做法是:先用 xmlReadMemory 加 XML_PARSE_RECOVER 标志强制继续解析(它会把非法实体转成 U+FFFD 替换符),再遍历节点树,对所有 xmlNode->content 手动还原——把 U+FFFD 换成空,同时保留原始文本中本就存在的 U+FFFD。
实际中最容易被忽略的,是混合编码场景:输入字符串是 UTF-8,但实体里 ... 的十六进制值按 Unicode 码点解释,不是按字节。替换时若没做 UTF-8 编码/解码,会导致多字节字符被截断。


















