零宽字符与隐藏字符:怎么查出来又怎么清干净
粘贴代码报莫名其妙的错,文本里藏了看不见的零宽空格 U+200B。这篇讲清零宽字符是什么、藏在哪、危害在哪,以及怎么在浏览器本地查出位置并清理干净。
零宽字符与隐藏字符:怎么查出来又怎么清干净
有一类 bug 特别折磨人:两段文本看起来一模一样,像素级一致,可程序就是认定它们不相等。你盯着屏幕反复核对每个字母,找不出任何区别。问题不在你眼睛,而在字节里藏了看不见的东西,最常见的就是零宽字符。
零宽字符到底是什么
零宽字符是真实存在的 Unicode 码点,特点是不占任何视觉宽度。它确实在字节里,确实参与字符串长度计算、相等判断、正则匹配,可你在屏幕上一点痕迹都看不到。
最典型的是 U+200B 零宽空格。它本来的用途是给长单词提供一个软换行点,让排版能在合适的地方折行。但被滥用或误带进文本后,它就成了纯粹的隐患。同一家族还有 U+200C 零宽非连接符、U+200D 零宽连接符、U+FEFF 字节序标记(BOM),以及一批看着像空格其实不是的家伙,比如 U+00A0 不间断空格、U+3000 表意空格。
这里有个值得记住的具体点:U+200B 这样的字符虽然渲染出来什么都没有,却实实在在占字节、占字符串长度。一个看起来是 7 个字的句子,真实长度可能是 9,多出来的两个就是隐形的。
它们从哪儿冒出来
这些字符很少是你手敲的,几乎都是复制粘贴时搭便车混进来的。从聊天软件复制一段话、从 PDF 抠一行字、在 Word 或 CMS 富文本框里选中内容、粘贴 AI 模型的输出,都可能把零宽字符一并带走。
最常见的翻车现场是从带样式的文档里复制函数名。你把 getUserById 从 API 文档拷到编辑器,运行却报 undefined,因为 get 和 User 之间被塞了个看不见的 U+200B,标识符已经对不上原来的符号了。
危害在哪:三种典型场景
第一是粘贴代码报莫名其妙的错。JSON 文件开头若有个 U+FEFF,JSON.parse 直接抛 "Unexpected token",哪怕文件你看一万遍都觉得没毛病。该是普通空格的地方混了个 U+00A0,"a b" 就不等于 "a b",字符串比较和 split(' ') 全部悄悄失效。
第二是文本盲水印。有些系统会在可见字母之间藏一串 U+200B / U+200C / U+200D,给生成文本打指纹。表面是普通一段话,底下埋着一条只有特定工具能读出来的隐蔽编码,用来追踪内容来源。
第三跟 AI 检测相关。模型输出有时携带零宽模式,从带追踪的文档里扒出来的内容同理。不清理就转发,等于把别人埋的指纹也一起带走了。
一个真实的检测例子
拿一段带零宽字符的文本试试。假设你粘进去的内容是 HelloWorld,看上去就是两个单词连在一起,没有空格。
检测器扫完会告诉你:在第 5 个字符之后存在一个 U+200B ZERO WIDTH SPACE,出现 1 次,预览区里这个位置被标成红色。也就是说真实内容其实是 Hello + 看不见的零宽空格 + World,字符串长度是 11 而不是 10。光看屏幕你永远发现不了这一位的偏差,但码点表把它的位置、名称、次数全摊开了。
清理:全删还是规范化
清理有两种思路,区别要分清。
全删模式把所有隐形字符直接删掉,适合零宽水印、混进标识符的 U+200B 这类纯垃圾。规范化模式则把那些隐形空格换成一个普通 ASCII 空格,同时删掉零宽那一族,适合 "价格: 9" 这种情况,处理后变成 "价格: 9",词间空格保留,而不是挤成 "价格:9"。
有两个坑要绕开。一是含真 emoji 的文本别无脑全删,U+200D 零宽连接符正是把 👨👩👧 这类多段 emoji 粘在一起的胶水,删掉它一家三口就裂成三个独立 emoji。二是开头的 BOM 别随手删,它会让 JSON 解析器报错,但有些老的 CSV 导入器靠它判断 UTF-8,删之前先搞清谁来读这文件。
为什么坚持本地处理
我自己处理这类文本时最在意一件事:粘进去的内容往往很敏感。可能是泄露排查中的密钥、私密文档片段、待审的 AI 输出。这些东西绝不该上传到任何服务器。所以检测、清理、预览全是浏览器标签页里运行的纯 JavaScript,文本不离开页面、不上传、不记录,关掉标签页什么都不留下。
想动手查一段文本,用 零宽字符检测器 直接粘进去看码点表。如果你想进一步逐字符拆解某段文本里每个 Unicode 码点的名称和分类,Unicode 字符检查器 能给出更细的逐字视图。
看不见不代表不存在。文本相等判断挂在看着没问题的字符串上时,第一反应应该是去查字节,而不是怀疑自己的眼睛。
Made by Toolora · Updated 2026-06-13