不可见字符
无可见字形的字符:空白、零宽字符、控制字符和格式字符,可能引发欺骗和文本隐写等安全问题。
What Are Invisible Characters?
Invisible characters are Unicode code points that have no visible glyph — they render as nothing in normal circumstances, yet they occupy space in a string and can affect text layout, rendering, and processing. They include format characters, zero-width characters, and various control or separator code points.
Invisible characters are legitimate and useful in proper typography and internationalization, but they are also exploited for obfuscation, invisible text, and bypassing filters.
Categories of Invisible Characters
Zero-Width Characters
These have no advance width in text layout:
| Code Point | Name | Abbreviation | Purpose |
|---|---|---|---|
| U+200B | Zero Width Space | ZWSP | Allows line break without visible space |
| U+200C | Zero Width Non-Joiner | ZWNJ | Prevents ligature/cursive joining |
| U+200D | Zero Width Joiner | ZWJ | Joins emoji; enables cursive joining |
| U+2060 | Word Joiner | WJ | Prevents line break, zero width |
| U+FEFF | Zero Width No-Break Space | BOM | Historical no-break; now mainly a BOM |
Format Characters (Cf)
| Code Point | Name | Effect |
|---|---|---|
| U+00AD | Soft Hyphen | Suggested break point; only visible when line breaks |
| U+2028 | Line Separator | Forces line break |
| U+2029 | Paragraph Separator | Forces paragraph break |
| U+200E | Left-to-Right Mark | LRM: influences bidi algorithm |
| U+200F | Right-to-Left Mark | RLM: influences bidi algorithm |
| U+202A–202E | Bidi embedding/override chars | Control text direction |
| U+2061–2064 | Mathematical operators | Invisible function application, etc. |
Non-Printing Control Characters
U+0000–U+001F (C0 controls) and U+007F–U+009F (C1 controls) are mostly invisible and have no standard rendering.
Detecting Invisible Characters
import unicodedata
def is_invisible(char: str) -> bool:
cat = unicodedata.category(char)
# Cf = Format, Cc = Control, Cs = Surrogate
return cat in ("Cf", "Cc") or unicodedata.combining(char) != 0
def find_invisible(text: str) -> list[tuple[int, str, str]]:
return [
(i, hex(ord(c)), unicodedata.name(c, "UNKNOWN"))
for i, c in enumerate(text)
if is_invisible(c)
]
text = "Hello\u200BWorld\u200D!"
find_invisible(text)
# [(5, "0x200b", "ZERO WIDTH SPACE"),
# (11, "0x200d", "ZERO WIDTH JOINER")]
# Stripping all invisible characters
import regex # pip install regex
def strip_invisible(text: str) -> str:
return regex.sub(r"\p{Cf}", "", text)
strip_invisible("Hello\u200BWorld") # "HelloWorld"
JavaScript Detection
// Detect zero-width and format characters
function findInvisible(text) {
const results = [];
for (const [i, char] of [...text].entries()) {
const cp = char.codePointAt(0);
if (
(cp >= 0x200B && cp <= 0x200F) || // ZW space, joiners, marks
(cp >= 0x202A && cp <= 0x202E) || // bidi controls
cp === 0x2060 || cp === 0xFEFF ||
(cp >= 0x2061 && cp <= 0x2064)
) {
results.push({ index: i, codePoint: cp.toString(16), char });
}
}
return results;
}
// Strip format characters using Unicode property
const stripped = text.replace(/\p{Cf}/gu, "");
Legitimate Uses
# ZWJ in emoji sequences (family emoji)
family = "👨\u200D👩\u200D👧" # 👨👩👧 one grapheme
# ZWNJ for Persian/Arabic (prevent unwanted ligature)
correct = "می\u200Cکنم" # correct word separation in Persian
# LRM/RLM for bidi text
mixed = "Hello \u200Eمرحبا" # force LTR context around Arabic
Security Considerations
Invisible characters are used for:
- Text fingerprinting/watermarking: Embedding hidden patterns to track document leaks.
- Bypassing content filters:
c\u200Ba\u200Btto write "cat" while evading text matching. - Homograph attacks: Hidden bidi overrides can reverse text direction in filenames or URLs.
- Obfuscating malicious strings: Zero-width characters interspersed in code.
# Security: normalize input by stripping Cf characters
import unicodedata
def sanitize(text: str) -> str:
# Remove format characters
cleaned = "".join(c for c in text if unicodedata.category(c) != "Cf")
# NFC normalize
return unicodedata.normalize("NFC", cleaned)
Quick Facts
| Property | Value |
|---|---|
| Most common invisible chars | U+200B, U+200C, U+200D, U+2060, U+FEFF |
| Unicode category | Cf (Format), Cc (Control) |
| Emoji ZWJ | U+200D — joins emoji into multi-person sequences |
| Python detection | unicodedata.category(c) == "Cf" |
| JS regex removal | text.replace(/\p{Cf}/gu, "") |
| Security risk | Homograph attacks, filter bypass, text fingerprinting |
| Legitimate uses | Bidi control, emoji sequences, typography, cursive joining |
相关术语
编程与开发 中的更多内容
Java strings use UTF-16 internally. char is 16-bit (only BMP). For supplementary …
Python 3 uses Unicode strings by default (str = UTF-8 internally via …
Rust strings (str/String) are guaranteed valid UTF-8. char type represents a Unicode …
使用Unicode属性的正则表达式模式:\p{L}(任意字母)、\p{Script=Greek}(希腊文字)、\p{Emoji},各语言和正则引擎的支持程度不同。
在源代码中表示Unicode字符的语法,各语言不同:\u2713(Python/Java/JS)、\u{2713}(JS/Ruby/Rust)、\U00012345(Python/C)。
用错误编码解码字节时产生的乱码文本,来自日语词“文字化け”。例如:'café'以UTF-8存储但用Latin-1读取,显示为'café'。
在UTF-16中一起编码补充字符的两个16位码元(高代理U+D800–U+DBFF + 低代理U+DC00–U+DFFF),😀 = D83D DE00。
编程语言中的字符序列,内部表示各异:UTF-8(Go、Rust、新版Python)、UTF-16(Java、JavaScript、C#)或UTF-32(Python)。
Unicode字符串的“长度”取决于计量单位:码元(JavaScript .length)、码位(Python len())或字素簇。👨👩👧👦 = 7个码位,1个字素。
U+FFFD(�),解码器遇到无效字节序列时显示的字符——“解码出错”的通用符号。