不可见 Unicode 字符:它们是什么,又会破坏什么

一份实用参考,介绍那些存在于文本中却不出现在屏幕上的字符。

不可见 Unicode 字符不占用任何可见宽度,但仍是字符串的一部分。常见的有零宽空格、软连字符、字节序标记、不换行空格和方向控制标记。每一种都有其合法的排版用途,但当它们在复制粘贴中幸存下来时,每一种都会破坏搜索、校验、解析和去重。

你真正会遇到的字符

字符 码位 用途 会破坏什么
零宽空格 U+200B 提示可换行位置 搜索、字符串比较
零宽不连字符 U+200C 阻止字母连写 连字、阿拉伯文和印度文文字
零宽连字符 U+200D 强制字母连写 表情符号序列、文本长度
软连字符 U+00AD 仅在换行时显示的连字符 搜索、复制的文本
不换行空格 U+00A0 永不断行的空格 去除首尾空白、CSV 解析、代码
窄不换行空格 U+202F 标点前的窄空格 同上
字节序标记 U+FEFF 在文件开头标记编码 CSV 首列、JSON 解析
字词连接符 U+2060 阻止断行,无宽度 搜索
从左到右标记 U+200E 设定文字方向 显示顺序、去除首尾空白
从右到左标记 U+200F 设定文字方向 显示顺序
RTL 覆盖符 U+202E 强制从右到左显示 文件名伪装
行分隔符 U+2028 Unicode 换行 JavaScript 字符串字面量
段落分隔符 U+2029 Unicode 段落分隔 旧版解析器中的 JSON

这些字符每一个都有其真正的作用。问题从来不在于它们的存在——而在于它们会在不同处理方式各异的系统之间悄无声息地传播。

它们从哪里来

  • 从网页复制。 网站会插入零宽空格来控制长字符串的换行位置,插入软连字符来排版两端对齐的文本。复制时两者都会被一并带走。
  • 文字处理软件。 会自动插入不换行空格,让数字和单位、或姓名保持在同一行。
  • PDF 提取。 从 PDF 复制时,排版时跨行断词的位置常常会产生软连字符,两端对齐的间距还会带来不换行空格。
  • AI 聊天界面。 输出以 HTML 渲染,因此复制时会带上排版所用的各种空格字符——通常是 U+00A0 和 U+202F。
  • 编码转换。 文件以带 BOM 的 UTF-8 保存时会出现字节序标记,这在 Windows 上很常见。
  • 故意插入。 零宽字符曾被用于给文档加水印指纹以追踪泄露源头,也曾被用来让文本绕过关键词过滤。

规律始终如一:当文本从展示场景进入数据场景时,它们就会混进来。

它们如何造成破坏

搜索无声无息地失败。 单词内部的一个零宽空格意味着搜索它时一无所获。没有任何提示说明原因——那个单词明明就在眼前。

校验拒绝正确的输入。 电子邮箱地址尾部带一个不换行空格就会格式校验失败。用户看到的是正确的地址和一条莫名其妙的错误信息。

比较失败。 "Ada" === "Ada\u200B" 的结果是 false。去重漏掉明显的重复项;查找一无所获;关联查询丢行。

去除首尾空白也无济于事。 大多数 trim() 实现只移除 ASCII 空白字符。在很多语言中,不换行空格能在 .trim() 之后幸存,这就是为什么“我明明已经 trim 过了”是如此常见的死胡同。

代码无法编译。 本该是普通空格的地方出现一个不换行空格,就会在一行看起来完美无缺的代码上抛出语法错误。

CSV 导入出错。 字节序标记会附着到第一个表头上,于是 id 变成了 \uFEFFid,第一列无声地映射失败。

长度不对。 表情符号序列中的零宽连字符意味着一个可见的表情符号可能由多个码位组成,于是字符数上限会拒绝看起来远未超限的输入。

文件名可以骗人。 U+202E 会反转显示的文字,因此 report\u202Egnp.exe 可能显示为 reportexe.png。这是真正的安全隐患,而非猎奇谈资。

查找并删除它们

先扫描。 把文本粘贴到不可见字符扫描工具中。它会报告存在哪些类型、各有多少个,然后按需删除。先看到数量,能告诉你面对的是一两个散落的字符,还是系统性的污染。

在编辑器中。 VS Code 默认通过 unicode-highlight 设置高亮大多数不可见字符。对于支持正则的查找替换,下面这个模式可以覆盖常见的字符集合:

[\u200B-\u200F\u00AD\uFEFF\u2060\u202A-\u202E\u2028\u2029]

在代码中,应在边界处做规范化——在数据到达时处理,而不是在使用时:

const clean = input
  .replace(/[\u200B-\u200F\u2060\uFEFF\u00AD\u202A-\u202E]/g, "")
  .replace(/[\u00A0\u202F]/g, " ")
  .normalize("NFC")
  .trim();

注意这里分两步处理:零宽字符被删除,但不换行空格被转换为普通空格——直接删除会把单词粘在一起。

不要盲目清除。 零宽连字符在表情符号序列中起结构作用,在阿拉伯文、波斯文和许多印度文文字中也是如此,删除它们会改变单词的渲染方式。在标识符、键名和代码中可以激进地清除;在面向用户的内容中则应保守处理。

关于 AI 工具是否会故意插入这些字符这一具体问题,请参阅 ChatGPT 会隐藏不可见字符吗。同样具有破坏性、但肉眼可见的同类问题,请参阅为什么弯引号会破坏代码

常见问题

什么是零宽空格?

即 U+200B,一个不占用可见宽度、但仍是字符串一部分的字符。网站用它提示长单词可以换行的位置。它会破坏搜索和字符串比较,因为文本看起来一模一样,底层字节却不同。

为什么明明就在那里的文本却搜索不到?

单词内部几乎肯定藏着一个不可见字符——通常是从网页或 PDF 复制时带入的零宽空格或软连字符。渲染出的文本与你输入的内容一致,但底层字符串并不一致。

trim() 能删除不换行空格吗?

通常不能。大多数 trim 实现只移除 ASCII 空白字符,因此不换行空格(U+00A0)会幸存下来。这就是为什么看起来首尾没有空格的输入在 trim 之后仍然校验失败。

如何删除文本中的不可见字符?

把文本粘贴到能列出并清除它们的扫描工具中,或使用覆盖 U+200B–U+200F、U+00AD、U+FEFF、U+2060 和 U+202A–U+202E 的正则表达式。零宽字符应直接删除,但不换行空格应转换为普通空格而非删除,否则单词会粘连在一起。

不可见字符有用吗?

有。软连字符控制两端对齐文本的断词位置,不换行空格让数值和单位保持在一起,零宽连字符对表情符号序列以及阿拉伯文、波斯文和印度文文字必不可少。在代码和标识符中可以激进清除,在正文中则应保守处理。

本指南提到的工具

相关指南