从零构建文本隐形水印:一种基于Unicode零宽度字符的隐写实践

📅 2026/7/15 19:44:39 👁️ 阅读次数 📝 编程学习
从零构建文本隐形水印:一种基于Unicode零宽度字符的隐写实践

1. 什么是文本隐形水印?

你可能见过图片上的半透明logo,但文本也能藏水印——而且完全看不见。文本隐形水印是一种通过修改文本的底层编码结构,嵌入隐藏信息的技术。就像用隐形墨水在纸上写字,平时看不出痕迹,但用特殊方法就能显现。

这种技术主要依赖Unicode中的零宽度字符——这些字符在显示时不会占据任何视觉空间。比如:

  • U+200B(零宽度空格)
  • U+200C(零宽度非连接符)
  • U+200D(零宽度连接符)

实际场景中,你可以在合同、小说甚至代码里嵌入作者信息或版权标识。即使文本被复制粘贴多次,水印依然存在。去年有个案例:某科技公司发现内部代码泄露,通过提取文本中的零宽度字符水印,最终锁定了泄露员工。

2. 零宽度字符的工作原理

2.1 Unicode的隐藏彩蛋

Unicode标准中有一组特殊字符,它们没有宽度、不可见,但能被计算机识别。比如在字符串"Hello"中插入U+200B,显示仍是"Hello",但实际存储的是"H‌e‌l‌l‌o"(这里用‌模拟零宽度字符)。

这些字符的二进制表示如下:

字符名称Unicode编码二进制表示
零宽度空格U+200B11100000 10000000 10001011
零宽度非连接符U+200C11100000 10000000 10001100
零宽度连接符U+200D11100000 10000000 10001101

2.2 编码规则设计

假设我们要嵌入二进制水印"1011":

  1. 将原文每个字符后插入一个零宽度字符
  2. U+200B代表"0",U+200D代表"1"
  3. 按水印位循环插入:H[1]e[0]l[1]l[1]o

提取时只需扫描零宽度字符序列,还原二进制流。我实测发现,普通文本编辑器(如VS Code)会保留这些字符,但微信、钉钉等IM工具可能会过滤掉它们——这是需要注意的兼容性问题。

3. 实战:Python实现水印嵌入

3.1 基础版本代码

def embed_watermark(text, watermark): zero_width_map = {'0': '\u200b', '1': '\u200d'} watermarked = [] for i, char in enumerate(text): watermark_bit = watermark[i % len(watermark)] watermarked.append(char + zero_width_map[watermark_bit]) return ''.join(watermarked) # 使用示例 original = "重要机密文件" watermark = "110101" # 版权标识的二进制形式 hidden_text = embed_watermark(original, watermark) print(f"看似普通的文本:{hidden_text}") print(f"实际长度:{len(hidden_text)} vs 原始长度:{len(original)}")

这段代码会在每个字符后插入水印位。用hexdump查看输出,能看到交替出现的E2 80 8B和E2 80 8D(零宽度字符的UTF-8编码)。

3.2 增强鲁棒性

单纯插入字符容易被批量删除,我们可以:

  1. 随机化插入位置:不是每个字符后都插入
  2. 添加校验码:在水印末尾加CRC校验
  3. 加密水印:先用AES加密再嵌入

改进后的嵌入逻辑:

import random from Crypto.Cipher import AES def robust_embed(text, watermark, key): # AES加密水印 cipher = AES.new(key, AES.MODE_ECB) encrypted = cipher.encrypt(watermark.ljust(16).encode()) # 转换为二进制串 binary_wm = ''.join(f'{byte:08b}' for byte in encrypted) # 随机选择插入位置 positions = sorted(random.sample(range(len(text)), len(binary_wm))) # 插入水印 result = list(text) for pos, bit in zip(positions, binary_wm): result.insert(pos + 1, '\u200b' if bit == '0' else '\u200d') return ''.join(result)

4. 水印提取与检测

4.1 基础提取器

def extract_watermark(watermarked_text): bits = [] for char in watermarked_text: if char == '\u200b': bits.append('0') elif char == '\u200d': bits.append('1') return ''.join(bits) # 高级版:处理加密水印 def decrypt_watermark(watermarked_text, key): binary_str = extract_watermark(watermarked_text) bytes_data = [int(binary_str[i:i+8], 2) for i in range(0, len(binary_str), 8)] cipher = AES.new(key, AES.MODE_ECB) return cipher.decrypt(bytes(bytes_data)).decode().strip()

4.2 对抗文本编辑

文本可能被修改,导致水印损坏。我们可以:

  1. 冗余编码:重复嵌入3次水印,取多数表决
  2. 位置散列:用HMAC算法确定插入位置
  3. 错误校正:添加Reed-Solomon编码
from hashlib import sha256 def hmac_position(text, watermark, secret): hmac = sha256((text + secret).encode()).hexdigest() positions = [] for i in range(len(watermark)): pos = int(hmac[i*2:i*2+2], 16) % len(text) positions.append(pos) return sorted(positions)

5. 应用场景与局限性

5.1 典型使用案例

  • 代码版权保护:在开源代码中嵌入开发者信息
  • 文档溯源:政府公文添加部门标识
  • 聊天审计:IM自动在消息中插入发送者ID
  • 防抄袭检测:论文预印本嵌入指纹

去年某开源项目发现有人去除License后二次销售,通过提取代码中的零宽度字符水印,成功证明了原始著作权。

5.2 技术限制

  1. 编辑器兼容性

    • 保留水印:VS Code、Sublime Text
    • 可能丢失:Word、WPS(粘贴时)
    • 完全过滤:大多数网页文本框
  2. 攻击手段

    • 重新输入文本(OCR无法提取)
    • 转PDF再转文本
    • 使用strings命令提取可见字符
  3. 容量限制:每100字符约可嵌入20-30位水印信息

我在实际项目中发现,结合自然语言水印(同义词替换)和零宽度字符,能显著提高抗攻击能力。例如同时用"快速"和"迅速"来编码二进制位,即使零宽度字符被过滤,仍有部分信息保留。