# msa-watermark **Repository Path**: hmsay/msa-watermark ## Basic Information - **Project Name**: msa-watermark - **Description**: No description available - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-08-01 - **Last Updated**: 2025-08-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 中文文本水印系统 > 通过在中文文本中嵌入水印来实现文本的版权保护 将64位二进制水印嵌入到约1000条中文字符串中。方案的核心思路是将水印信息分布式嵌入到多个字符串中,通过错误校正码(ECC) 和基于内容的哈希索引来实现鲁棒性,即使字符串顺序打乱或只使用部分字符串(例如,仅60%的字符串),也能提取出水印。水印嵌入采用不可见字符修改,确保不易被察觉(不影响文本的可见内容和语义)。 ## 方案概述 **水印预处理**:使用Reed-Solomon错误校正码(ECC)将64位水印扩展为128位,增加冗余。这允许容忍部分字符串缺失(最多可缺失50%)或提取错误。 **嵌入机制**:每个非空字符串嵌入1位水印信息(0或1),通过添加Unicode零宽度字符实现。这些字符在显示时不可见,但可以在提取时检测。 **索引分配**:基于字符串内容的哈希值(如SHA-256)计算索引,将水印位分配到特定字符串。索引不依赖顺序,因此字符串打乱不影响提取。 **提取过程**:收集可用字符串(无论顺序),提取每个字符串的嵌入位,并通过多数投票和ECC解码恢复水印。 **鲁棒性**:通过ECC和分布式设计,只要可用字符串覆盖大部分索引(例如,至少50%的字符串),就能正确提取水印。测试表明,在字符串缺失率≤40%时,成功率>99%。 ## 详细步骤 ### 1. 水印预处理(增加冗余) 将64位水印(例如0101...10)使用Reed-Solomon(64, 128)编码为128位。这意味: - **原始水印**:64位。 - **ECC输出**:128位(64位数据 + 64位冗余)。 为什么? Reed-Solomon码可以纠正擦除(缺失)和错误。这里n=128, k=64,最多可容忍64位缺失或错误。 将128位水印视为一个位序列:索引0到127,每个索引对应1位值(0或1)。 ### 2. 字符串筛选与索引分配 **忽略空字符串**:长度小于1的字符串无法嵌入信息,直接跳过。假设您有约1000条字符串,忽略空字符串后,实际用于嵌入的字符串数为N(e.g., N≈950-1000)。 **为每个字符串分配索引**: 使用固定哈希函数(如SHA-256)计算字符串内容的哈希值。例如:hash = SHA256(string_content)。 **计算索引**:index = hash % 128(取模128,范围0-127)。这确保每个索引对应水印的1位。 **索引冲突处理**:多个字符串可能映射到同一索引(概率低,N=1000时冲突率<5%)。所有映射到同一索引的字符串嵌入相同的位值(增强鲁棒性)。 **输出**:得到一个映射表(字符串 → 索引 → 水印位值)。此表在嵌入和提取时使用,但不存储,而是动态计算。 ### 3. 嵌入水印到字符串(不易察觉修改) **嵌入方法**:在字符串末尾添加Unicode零宽度字符,表示位值: 如果水印位值=0,添加 U+200B(零宽度空格)。 如果水印位值=1,添加 U+200C(零宽度非连接符)。 **示例**: 原始字符串:"数据中台"。 嵌入后(位值=0):"数据中台\U+200B​"(末尾有U+200B,显示时不可见)。 嵌入后(位值=1):"数据中台‍\U+200C"(末尾有U+200C,显示时不可见)。 > 为什么选择零宽度字符? **不易察觉**:在大多数显示环境中,这些字符不占用空间或改变文本外观。 **兼容性**:支持Unicode的系统和工具(如数据库、API)能正确处理,且不影响文本分析。 **长度影响**:字符串长度增加1(但零宽度,实际显示长度不变)。对于短字符串(长度0-20),这很安全。 **特殊处理**: - 如果字符串已包含零宽度字符,优先覆盖或追加(但概率低)。 - 避免修改文本内容(如字符替换),以保持语义不变。 ### 4. 提取水印(支持乱序和部分字符串) **输入**:任意子集的字符串(可能乱序),例如仅500条字符串。 **步骤**: 提取每个字符串的位值和索引: - **检查字符串末尾**:如果检测到U+200B,提取位值=0;如果U+200C,位值=1;如果无或无效,跳过该字符串。 - **重新计算索引**:index = SHA256(original_string) % 128,其中original_string是移除零宽度字符后的原始内容(如果添加了字符)。 - **多数投票处理索引冲突**: 对于每个索引i (0-127),收集所有字符串报告的位值。 取多数投票(majority vote)得到该索引的最终位值。例如,索引5有3个字符串报告0,2个报告1,则位值=0。 如果某索引无字符串覆盖,标记为"擦除"(缺失)。 - **ECC解码**: 得到128位序列(可能含擦除)。 使用Reed-Solomon解码恢复原始64位水印,自动纠正擦除和错误(最多容忍64位缺失)。 - **鲁棒性分析** - **字符串缺失容忍**:理论最大缺失50%(ECC可纠正)。在N=1000、索引128个的场景: 每个索引平均覆盖 ~7.8个字符串(1000/128)。 即使仅50%字符串可用(500条),大多数索引仍有覆盖(平均~3.9个字符串/索引),多数投票有效。 - **测试**:随机缺失60%字符串时,提取成功率>95%;缺失40%时,>99%。 - **乱序处理**:索引基于内容哈希,不依赖顺序。 - **短字符串适应性**:长度≥1的字符串均可嵌入(零宽度字符添加简单)。 - **安全考虑**:哈希函数(SHA-256)确保索引分配不可预测,防止攻击者篡改。 ## 使用示例 ### 文本转水印 ```python watermarker = ChineseTextWatermarker() watermark = watermarker._string_to_watermark("lamer") ``` ### 嵌入水印 ```python strings = ["数据中台", "数据仓库", "数据湖", "数据挖掘", ....] watermarked_strings = embed_watermark(strings, watermark) ``` ### 提取水印 ```python extracted_watermark = extract_watermark(watermarked_strings) ``` ### 水印转文本 ```python extracted_string = watermarker.watermark_to_string(extracted_watermark) ``` ## 测试结果 使用1000条字符串,嵌入水印"12345678"。从字符串取部分数据提取水印,测试结果如下图: ![result.png](result.png) ## 结论 要注入一个8位长度的字符串,理论上需要最少 8*8 位水印,加上ecc编码,最需要128位水印。根据测试结果,需要数据完整性达到60%,才能获得99%的提取成功率。因此至少需要213条数据,才能完整嵌入水印。 如果数据库批量查询,每次查询1000条数据,如果要一次嵌入水印,1000/(128 /0.6) * 8 = 37.5个字符串。 即水印的长度不要超过37.5个字符。`watermark_bits`的最大值不得超过 300;`ecc_redundancy` 也不要超过300。