控制字符
不在屏幕上显示,但用于指示文本处理方式的特殊字符。包括换行 (LF)、制表符 (HT)、回车 (CR)、空字符 (NUL) 等。
控制字符 (control character) 不是作为文字来显示,而是为了控制文本的处理和显示而使用的特殊字符。ASCII 的前 32 个字符 (U+0000〜U+001F) 和 DEL (U+007F) 被归类为控制字符。Unicode 中还在 U+0080〜U+009F 的范围内定义了控制字符。
日常使用的控制字符是有限的。LF (Line Feed,U+000A) 表示换行,是 UNIX 系操作系统的标准换行符。CR (Carriage Return,U+000D) 是把光标移回行首的字符,Windows 中用 CR+LF 两个字符表示换行。HT (Horizontal Tab,U+0009) 是制表符,用于文本的列对齐。NUL (U+0000) 是空字符,在 C 语言中表示字符串的结尾。
在字符计数中,控制字符的处理会成为混乱的根源。在文本编辑器里输入"换行",看上去只是换了一行,但在内部插入的是 LF 这 1 个字符,或者 CR+LF 这 2 个字符份。数一数在 Windows 上创建的文本文件的字符数,每 1 个换行会被计为 2 个字符,比在 UNIX 上创建的同一内容的文件字符数更多。明明是同一篇正文却数不一致,这类矛盾大多源于此。在数之前先把换行符统一到其中一种,那么跨环境也会得到相同的数。另外,把输入框里写的文章通过表单发送时,浏览器会在发送时把换行统一为 CR+LF 的成对形式 (无论 wrap 属性取什么值,这是表单发送时序列化规则的行为)。在画面上数出的值与在服务器端收到之后数出的值会出现偏差,就是因为中间夹了这道转换。
在 Web 的语境中,连续的空白字符 (空格、制表符、换行) 会被归并为一个空格来显示。这并不是 HTML 语法上如此规定的,而是 CSS 的 white-space 属性的默认值 (normal) 带来的显示上的处理。因此,HTML 源代码上的字符数与浏览器上显示的文本的字符数并不一致。在 <pre> 元素或指定了 white-space: pre 的元素中,制表符和换行会原样显示。要数的对象是"源码上的字符"还是"画面上看得见的字符",答案会随之改变,所以需要先决定自己想数哪一个。
从安全的角度看,控制字符可能成为注入攻击的手段。在 HTTP 头部插入 CR+LF 来伪造另一个头部的"HTTP 头部注入",以及在日志文件中插入换行来制造假的日志行的"日志注入"都是已知的。关于前者,截至 2026 年的主流服务器实现在头部的值中含有 CR 或 LF 时会拒绝发送本身 (Node.js 中会抛出异常),但对于像日志那样自己拼装字符串再写出的输出,并没有准备好同样的防壁。作为对策去除控制字符时,范围的指定需要注意。把 U+0000〜U+001F 一并删除,换行和制表符也会一起消失,正文会被压成一行。保留有意义的换行 (LF、CR) 和制表符,把其余的丢掉,按这个粒度来区分才是现实的落点。
Unicode 中除了控制字符之外,还有"格式字符"这一类别。零宽空格 (U+200B)、零宽连接符 (U+200D)、双向控制字符 (U+200E、U+200F) 等属于此类。它们与控制字符一样不可见,但在 Unicode 的分类上属于另外的类别 (控制字符为 Cc,格式字符为 Cf)。这个区别也会体现在实务上。JSON 的字符串字面量中不能写入原始的控制字符,因此制表符或换行原样混入的数据在读取的时点就会成为语法错误,而零宽空格是格式字符,会毫无警告地穿过去。也就是说,有些字符会被传递用的格式本身挡掉,有些字符则会一直残留到最后。
处理控制字符时的判断,可以整理为 3 条。首先,换行和制表符是承担文章结构的信息,并不是不需要的垃圾,所以不要一律删除。其次,像 NUL 这样可能让处理系统误动作的字符,以及像 U+0080〜U+009F 这样没有理由作为文本出现的字符,要在保存之前的输入检查中挡掉。最后,在数字符数的场合,要先决定好把换行数作 1 个字符还是 2 个字符,或者根本不计入数中。把这 3 条定下来,跨环境、跨工具时的矛盾大半都能防住。