ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

文本隐写术实战:五种Python方法实现内容防泄露与溯源

文本隐写术实战:五种Python方法实现内容防泄露与溯源 就我这些年折腾内容保护和防泄露的经验来看很多团队到现在还在用“给图片打水印”这种老办法。费劲不说水印位置固定、容易被裁剪抹除而且一旦文字被复制出来图片上的心血就全部归零了。更尴尬的是有些场景根本不方便甩图片出去比如合同审批、群聊确认、文章防篡改核对。这时候文本隐写术反而是更聪明的解法肉眼看到的文字一切正常但里面悄悄嵌入了你自己才知道的暗号。今天这篇就系统拆一下我实测过的5种文本隐写术从原理、代码到坑位一次性讲清楚。这条路适合谁走如果你是运营、开发、法务、编辑或者任何需要“追踪文本流向”的人这篇文章能帮你解决一个很现实的问题别人复制了你的内容你怎么证明这是你的、以及它是从哪个渠道流出去的。不需要你会复杂的密码学只要有点Python基础就能跟上。我会把每种方法的原理讲明白再把可以直接抄的代码贴出来最后聊一聊哪些场景踩坑最惨。1. 为什么我劝你先别碰图片水印1.1 图片水印在真实场景里的4个硬伤图片水印看起来直观但实际操作中头疼的事特别多。第一水印会破坏内容的可读性。为了防止别人盗图不少人喜欢在图片正中间铺一堆半透明logo客户看个报价单还要眯着眼找数字体验极差。第二水印能被针对性抹除。现在的修图工具越来越聪明稍微懂点克隆填充的人几秒钟就能把角落里的水印抹干净。第三图片水印保存不了文字上下文。你明明要保护的是“一段话”最后却要截图处理接收方复制内容时直接变成图片里的OCR文本格式全乱。第四也是最要命的图片水印只能在图片这个载体里生效一旦内容被写成文档、粘贴到聊天框、发布到网页水印就完完全全消失了。所以我的结论是图片水印适合保护“图片视觉版权”但根本保护不了“文本内容的流向”。你想要追踪的是文字本身那就得把隐写信息焊死在文字里让文字走到哪儿标记就跟到哪儿这才是文本隐写术的核心价值。1.2 文本隐写术到底隐藏的是什么文本隐写术做的事情本质上是在“不改肉眼可见文字内容”的前提下利用人类阅读时的视觉盲区和计算机解析字符时的编码差异把一段额外信息塞进正常的文本流里。打个比方图片水印像是给快递盒外面贴了一张写着收件人名字的胶带谁都能看见文本隐写术则是在快递盒的瓦楞纸夹层里塞了一张纸条外面看着就是个普普通通的盒子只有知道夹层存在的人才找得到纸条。这说明了两件事第一文本隐写术的重点是“隐蔽性”而不是“加密强度”它防的不是专业的取证工程师而是让普通读者、平台审核、甚至是对方公司的技术同事都察觉不到异常第二隐写和加密可以组合使用。通常我会先对要隐藏的内容做一次加密或哈希再嵌入文本这样就算别人发现了隐藏内容也读不出原始信息。还有个容易被忽略的点文本隐写术的“隐藏”其实是分层级的。有的方案隐藏的是内容本身有的方案隐藏的是“这段文本的来源身份”还有的方案隐藏的是“这段文本有没有被篡改过”。你在选型时要先想清楚自己想解决哪一种问题后面讲五种方案时我会对应说明。2. 先搞懂文本隐写术的核心思想2.1 利用的是“视觉盲区”不是加密算法很多人第一次听说文本隐写术时会觉得高深其实拆开看核心就一句话人类的视觉系统会在阅读时自动“脑补”和“忽略”某些不寻常但合法的字符。我们的眼睛看熟悉的中英文时是按“整词”“整行”去识别的不会逐字母比对。这就给隐写留下了巨大的操作空间。比如英文单词里的某些字母如果被替换成长得一模一样的西里尔字母绝大多数人根本发现不了比如文本里如果被插入几个宽度为零的不可见字符阅读时完全感受不到再比如中文字符串里把某些逗号从中文标点换成英文标点你不拿编辑器逐字看根本看不出区别。从计算机的角度讲这些字符在Unicode标准里有自己独立的码位程序可以精准地区分它们。所以只要编码方案约定好我们就能用这些“视觉上无感、编码上可区分”的字符作为0和1把二进制数据嵌入文本。这其实和无线电通信里的“隐写信道”是同一个思路利用载体的冗余和人类感知的局限性在同一条信号通道里再开一条秘密的小通道。2.2 你需要准备哪些工具做文本隐写术的入门门槛非常低大部分操作只需要三样东西一个文本编辑器、一个Python运行环境、一个能查看Unicode码位的工具。文本编辑器建议选VS Code或者Sublime Text它们能通过插件或自带功能显示不可见字符。VS Code里可以按“CtrlShiftP”打开命令面板搜索“Toggle Render Whitespace”来显示空格和制表符但针对零宽字符这种Unicode字符最好还是装个“Hex Editor”插件或者直接用Python打印出每个字符的码位。Python环境推荐直接用Anaconda或官方Python 3.9代码上不需要额外安装第三方库。我所有示例只用标准库做二进制转换和文件读写方便你直接复现。查看Unicode码位这个步骤非常重要。大多数“隐藏失败”的问题根源就是嵌进去的字符被系统或平台悄悄“规范化”或“过滤”了。判断方法很简单把接收到的文本复制回来逐字符打印它的十六进制码位如果码位和你嵌入时不一致说明中间有环节动了手脚。这招在排查线上问题时能省下大量时间。3. 五种可落地的文本隐写术实战3.1 零宽字符隐写最轻量也最常用零宽字符是目前应用最广、也最容易被忽视的一类隐写载体。它的特点是本身不占任何视觉宽度在网页、聊天软件、Word文档里都几乎不可见但在程序眼里它们就是实实在在的字符。常用零宽字符包括字符Unicode码位说明U200B零宽空格最常用的隐藏位0U200C零宽不连字常用的隐藏位1U200D零宽连字可辅助做隐藏位UFEFF零宽不换行空格部分场景用作标记位U2060单词连接符与UFEFF类似更推荐我自己的编码方案非常简单把要隐藏的内容先转成二进制字符串然后用U200B代表二进制0用U200C代表二进制1逐个字符嵌入到载体文本的任意位置。提取的时候反向扫描遇到U200B就记一个0遇到U200C就记一个1最后把二进制转回文本。下面是完整的可运行代码def text_to_binary(text: str) - str: 把字符串转成二进制字符串 # 使用 UTF-8 编码每个字节转成8位二进制拼接起来 raw text.encode(utf-8) return .join(f{byte:08b} for byte in raw) def binary_to_text(binary_str: str) - str: 把二进制字符串转回字符串 # 每8位一组转成字节再按 UTF-8 解码 byte_array bytearray() for i in range(0, len(binary_str), 8): byte_array.append(int(binary_str[i:i 8], 2)) return byte_array.decode(utf-8) def hide_with_zero_width(carrier: str, secret: str) - str: 把 secret 嵌入 carrier 文本中 binary text_to_binary(secret) # 用零宽字符替换01这里用 U200B 表示0U200C 表示1 hidden_units .join( \u200B if bit 0 else \u200C for bit in binary ) # 在载体文本开头插入隐藏内容你也可以选择插在中间 return carrier[:2] hidden_units carrier[2:] def extract_with_zero_width(stego_text: str) - str: 从载体文本中提取隐藏内容 binary_chars [] for ch in stego_text: if ch \u200B: binary_chars.append(0) elif ch \u200C: binary_chars.append(1) # 其他字符直接跳过 return binary_to_text(.join(binary_chars)) # 测试 carrier 本周五下午三点会议室开会请准时参加。 secret LEAK-2024-036 stego hide_with_zero_width(carrier, secret) print(repr(stego)) print(提取结果:, extract_with_zero_width(stego))这里有个容易踩的细节一定要先对secret做UTF-8编码再转二进制不然中文内容会丢。Python里字符串默认是Unicode直接对每个字符用ord()转码位再二进制也能用但提取时稍不注意就会编码错乱。用UTF-8字节序列是最稳妥的方案。零宽字符方案最大的优势是隐蔽性好、对原文排版毫无影响文本在视觉上、复制粘贴后的“外观上”看不出任何差异。但它也有致命弱点很多平台会主动清洗零宽字符。比如某些IM软件的聊天记录、数据库入库时的字符过滤甚至微信在部分场景下会把这些字符干掉。所以你如果要用零宽字符一定要先在目标渠道测试一次“隐写后-复制-提取”全流程不要等发出去了才发现字符被吞。3.2 同形字符替换让字母“李代桃僵”同形字符替换的思路和零宽字符完全不同。它不是靠“加不可见字符”而是把载体里原本的合法字符偷偷替换成另一个肉眼分辨不出的同形字符。最经典的案例是英文和西里尔字母之间的混淆拉丁小写字母aU0061和西里尔小写字母аU0430在很多常用字体里渲染出来几乎一模一样拉丁字母oU006F和西里尔字母оU043E也极其相似。实际操作中我的做法是把一段英文文本中的某些特定字母按照约定好的映射表替换成同形的西里尔字母。比如我要嵌入二进制位0时把第3个字符从英文a换成西里尔а要嵌入1时把第7个字符从英文e换成西里尔е。因为每次只替换少数几个字符整体文本的视觉冲击非常小几乎不会引起注意。下面是一个简单实现实现了按隐藏信息替换文本中特定英文字母的逻辑# 同形字符映射表拉丁 - 西里尔相似形 HOMOGLYPH_MAP { a: а, # U0061 - U0430 e: е, # U0065 - U0435 o: о, # U006F - U043E p: р, # U0070 - U0440 c: с, # U0063 - U0441 x: х, # U0078 - U0445 } REVERSE_MAP {v: k for k, v in HOMOGLYPH_MAP.items()} def hide_with_homoglyph(text: str, secret: str) - str: 使用同形字符替换来嵌入信息 binary text_to_binary(secret) result list(text) idx 0 bit_pos 0 while bit_pos len(binary) and idx len(result): ch result[idx].lower() if ch in HOMOGLYPH_MAP: bit binary[bit_pos] if bit 1: # 用同形字符替换原始字符 result[idx] HOMOGLYPH_MAP[ch] # 0 则保持不变 bit_pos 1 idx 1 if bit_pos len(binary): raise ValueError(载体文本长度不足以承载隐写信息) return .join(result) def extract_with_homoglyph(text: str) - str: 提取同形字符隐写信息 bits [] for ch in text: lower_ch ch.lower() if lower_ch in HOMOGLYPH_MAP: # 如果当前字符是西里尔同形字符说明原始位是1 if ch ! lower_ch: bits.append(1) else: bits.append(0) return binary_to_text(.join(bits))注意这里隐藏信息是用“是否被替换”来表示的替换成同形字符代表二进制1保持原样代表二进制0。提取时只要扫描整个文本里所有“候选字符”对照码位就能还原出完整二进制串。这个方案的优点是字符不会被平台“过滤删除”因为替代后的字符仍然是合法的可见字符大多数系统不会没事清洗正常字母。但同形字符方案的坑也很明显第一它会轻微改变文本外观虽然肉眼难以察觉但在等宽字体或某些安全软件的高亮模式下码位异常会被标记出来第二它只适合英文、西里尔字母这类有丰富同形字符的语言中文场景基本使不上力第三如果接收方系统做了Unicode规范化NFKC等某些西里尔字符可能被转换回拉丁字母隐写内容就失效了。所以使用前要确认目标系统是否做了字符规范化处理。3.3 乱序重排隐写用词序传递密文前两种方法本质上是在“字符层”做文章第三种方法则升级到了“词汇层”。乱序重排隐写的核心逻辑是在不改变句子核心语义的前提下调整部分词语的位置利用“调整后的词序”来表示隐藏信息。举例来说正常人会写“今天下午我们开会讨论方案”你把它改成“我们开会讨论方案今天下午”——虽然读起来稍微有点别扭但一般人不会太在意更不会想到词序调整是一种信号。如果需要更强的隐蔽性还可以把这种调整伪装成“口语化表达”“方言习惯”或“个人写作风格”这就非常难被识破了。严格来说要编程实现任意文本的词序重排隐写比较复杂因为需要语言模型来判断哪些词可以互换而不影响语义。我在实际项目中用过一个简化版方案针对“有固定语序模板的句子”特别好用比如通知类、结果类、日志类文案。操作方式是预先定义一组候选词槽每个词槽有两个备选词A和B选A代表0选B代表1。发送方根据隐写内容决定选词接收方根据实际出现的词反推二进制。这很像“谜语人”的协议预置方式但用在固定格式文本里效果出奇地好。# 固定模板的词序/选词隐写示例 # 对每个需要隐藏的 bit用不同词语表达同一个意思。 STEGO_DICT [ (收到, 已阅), # bit 0 - 收到, bit 1 - 已阅 (请尽快处理, 烦请处理), # bit 0 - 请尽快处理, bit 1 - 烦请处理 (谢谢, 感谢), # bit 0 - 谢谢, bit 1 - 感谢 ] def hide_with_synonym(template_parts: list, secret: str) - str: binary text_to_binary(secret) if len(binary) len(template_parts): raise ValueError(模板词槽不够用) message [] for i, part in enumerate(template_parts): if i len(binary): choices STEGO_DICT[i] message.append(choices[int(binary[i])]) else: message.append(part) return .join(message) def extract_with_synonym(message: str) - str: bits [] for word in message.split(): for choices in STEGO_DICT: if word choices[0]: bits.append(0) elif word choices[1]: bits.append(1) return binary_to_text(.join(bits))这个方案最大的优点是对抗“字符级清洗”的能力极强无论平台怎么过滤不可见字符、做Unicode规范化、转换字体只要词语本身还在信息就在。缺点是要求载体文本必须适配预设词槽灵活性差一些。在实际中我很少用纯自动化的词序替换去发自由文本而是把这个方案用在“消息摘要”“评价语”“群机器人回复”这类半固定内容上。比如给一个自动回复机器人配置几组“同义短语”根据隐藏的渠道编号选择不同措辞用户完全察觉不到异常。这种方式比零宽字符要稳得多缺点是预设成本高。3.4 标点符号与全角半角隐写毫不起眼的“变数”中文文本里的标点符号是隐写的天然宝库因为中英文标点之间的差异非常隐蔽尤其对非技术背景的读者来说更是如此。全角逗号“”是UFF0C半角逗号“,”是U002C两者在中文排版里视觉差异极小全角句号“。”是U3002英文句点“.”是U002E混用也经常被忽略。利用这种差异做隐写比零宽字符更抗过滤因为标点符号是正常文本的一部分平台不会轻易删掉逗号句号。具体设计时我一般会固定让句子末尾的标点在“全角”和“半角”之间切换全角表示1半角表示0。也就是把原本“今天开会请准时。”这句话里的标点做一点微调变成“今天开会,请准时.”肉眼几乎无感程序却可以逐句提取。下面是一个基于中文标点切换的隐写实现def hide_with_punctuation(text: str, secret: str) - str: 利用全角/半角标点隐写信息 binary text_to_binary(secret) chars list(text) punct_indices [ i for i, ch in enumerate(chars) if ch in 。、 ] if len(punct_indices) len(binary): raise ValueError(标点数量不足无法承载隐写信息) for bit_pos, idx in enumerate(punct_indices): ch chars[idx] if bit_pos len(binary): if binary[bit_pos] 1: # 1 表示全角标点 chars[idx] ch # 原样保留全角 else: # 0 表示切换成半角 if ch : chars[idx] , elif ch 。: chars[idx] . elif ch : chars[idx] ! elif ch : chars[idx] ? return .join(chars) def extract_with_punctuation(text: str) - str: bits [] for ch in text: if ch : bits.append(1) elif ch ,: bits.append(0) elif ch 。: bits.append(1) elif ch .: bits.append(0) return binary_to_text(.join(bits))这里要特别小心中文语境下全角标点才是规范写法如果大面积替换成半角专业读者可能会觉得排版怪怪的。所以控制“被修改的标点数量”非常重要只嵌入较短的信息比如哈希值前8位对阅读体验的影响可以降到最低。我实测下来标点方案在公众号文章、Word文档、PDF转文本等场景下表现都不错兼容性也远比零宽字符好。缺点很明显隐藏容量受限。一篇长文里标点数量再多也是有限的嵌入几十个字符还行想塞一段完整备注就有点吃力。另外如果接收方对文本做了“标点自动校正”比如某些编辑器的“中文排版优化”隐藏信息就会被打乱提取出来是一堆乱码。3.5 排版格式隐写空格、缩进也能当信道最后这个方法更冷门一点但在我处理纯文本文档时非常实用利用空格的数量和位置来传递信息。比如在行尾多加几个空格或者在某些换行符前后插入特定数量的空格视觉上几乎无法察觉但通过程序逐字符检查可以精确还原二进制序列。这个方法在代码文件、配置文件、纯文本记事本中尤其好用。因为很多编辑器默认不显示行尾空格你加再多的空格对方打开文件也看不见。Markdown渲染时行尾空格有时甚至会直接忽略这正好成为隐蔽的通信信道。相比零宽字符空格在大多数文本处理管道里都被当作“合法字符”保留下来被清洗的概率明显更低。下面是一个利用行尾空格数量做隐写的方案def hide_with_trailing_spaces(text: str, secret: str) - str: 在每行末尾追加不同数量的空格来隐写信息 binary text_to_binary(secret) lines text.split(\n) # 用2个空格表示03个空格表示1必须确保每行部分能被识别 encoded_lines [] for i, line in enumerate(lines): if i len(binary): if binary[i] 0: encoded_lines.append(line ) # 2个空格 else: encoded_lines.append(line ) # 3个空格 else: encoded_lines.append(line) return \n.join(encoded_lines) def extract_with_trailing_spaces(text: str) - str: lines text.split(\n) bits [] for line in lines: stripped line.rstrip( ) extra_space_count len(line) - len(stripped) if extra_space_count 2: bits.append(0) elif extra_space_count 3: bits.append(1) # 其他空格数忽略 return binary_to_text(.join(bits))使用这个方法要注意几点第一不是所有平台都会保留行尾空格部分前后端框架会对文本做trim这会导致提取失败第二如果你把文本复制到富文本编辑器里空格很可能会被吃掉第三一些代码规范工具比如prettier会自动去掉行尾空格代码仓库环境下基本用不了。所以行尾空格法最适合的场景是纯文本格式的备份文件、日志系统里的备注字段、以及可以提供原文的API接口数据。它最大的价值是“即使不渲染、不解析格式只看原始文本流也能提取”非常硬核。缺点是对格式规范要求高稍微被其他工具“好心修正”一下信道上就一片空白了。4. 五种方法对比与选型建议4.1 隐藏容量、视觉隐蔽性、存活率一览做一个这五种方案的直接对比大家在选型时能少走弯路隐写方案隐藏容量视觉隐蔽性平台存活率适用文本类型主要风险零宽字符高可嵌入较长内容极高完全不可见低易被清洗网页、IM、聊天、文档平台过滤、编辑器显示异常同形字符替换中受候选字母数量限制较高几乎无感中偏高正常字母不易被删英文文本、代码注释Unicode规范化会破坏信息词序/选词隐写低依赖模板设计较高行文稍自然高词语本身不特殊固定话术、机器人回复模板成本高扩展性差标点/全半角隐写中受标点数量限制中细心可见中偏高标点较稳定中文文章、公告、报告自动标点纠正会破坏信息行尾空格隐写中高行数即容量较高肉眼难发现中依赖保留空白代码、配置文件、日志工具自动trim导致失效这里我特别提醒“存活率”是决定一次隐写方案成败的关键指标。很多教程只演示怎么嵌入却忽略了平台的清洗策略。我建议任何方案在上线前先把目标路径完整测一遍发送端写入、接收端读取、数据库存储、API传输、前端渲染最后用提取代码验证信息是否完整。不要问为什么你的代码在本机提取没问题、发出去就失效答案通常就藏在这条链路的某个中间环节里。4.2 不同业务场景下的推荐组合从“防泄露溯源”的角度讲不同场景侧重点不一样我分享几组常用的组合策略。第一种是“公众号/网页文章防扫描搬运”。这种情况推荐用零宽字符标点全半角双通道备份。零宽字符能嵌入完整的泄露者ID或链接标点通道则嵌入文章版本号或发布时间。就算平台把零宽字符清洗了标点通道还能兜底。我之前处理过一个案例对方从公众号复制文章后直接发到微博结果零宽字符全没了但标点变体还在最终靠标点提取出的时间戳锁定了发布版本。第二种是“对外发送的Word/PDF合同”。由于PDF转文本时会丢失很多格式信息零宽字符有时也能保留但行尾空格大概率会丢失。我的做法是在合同正文里用同形字符替换几个英文缩写字母来嵌入客户编号再用行尾空格在附录里嵌入完整操作记录。这样即使对方转存为PDF同形字符仍然可提取。第三种是“内部聊天群消息”。微信、钉钉这类IM对零宽字符不太友好但对正常字母、标点的保留率很高。在群里发的通知用标点全半角或者词序/选词隐写效果更好。尤其适合在“已读确认”类的消息里嵌入员工的工号信息可以反查出截图是谁传出去的。没有万能方案只有适合场景的方案。我的原则是永远不要把鸡蛋放在一个信道里重要内容至少用两套以上的隐写方案叠加互为备份。5. 常见坑位与排查技巧实录5.1 隐写内容莫名其妙消失问题出在哪儿我碰到最多的反馈就是“我明明嵌入了隐写内容一复制到某个平台就没了”。排查这类问题我的经验顺序是先查编码、再查清洗规则、最后查编辑器显示。第一步确认代码里的字符在目标系统里是否被转义或replace。比如某些安全组件会把\u200B这类不可见字符视为非法输入直接过滤第二步检查前后端是否对文本做了trim、strip、unicode normalize。代码里看不出问题的话可以写个循环把接收到的文本每个字符的码位打印出来对照嵌入时使用的码位表一步就能定位丢失发生在哪个环节第三步打开编辑器的“显示不可见字符”功能如果连编辑器都不显示那就是编辑器配置问题不是隐写失效。这一步排查口诀是先文件后网络先存储后渲染。把文本拿到本地落一份原始文件再走一遍网络传输分别做提取测试就能圈定故障范围。5.2 隐藏内容提取乱码是编码位序踩了坑提取出来是乱码通常有三个原因二进制位序反转、分组长度错误、字符编码不匹配。位序反转很好理解就是我在嵌入时先发高位但提取时按低位读得到的01串完全反向解码出来自然是一堆乱码。解决办法是嵌入和提取协议里固定用“每个字符按8位二进制高位在前”的规则不要两边各写各的。分组长度错误大多是中文字符的UTF-8是3个字节如果你设计时误以为中文是2个字节按16位一组去切分肯定乱码。最稳妥的就是统一用我在示例里写的“text_to_binary/binary_to_text”一个字节就是8位不多不少。字符编码不匹配则发生在“接收端运行时不是UTF-8”的情况下比如数据库用的latin1存进去再读出来就废了。5.3 如何验证你的隐写是否真正生效很多人写完代码觉得“本机能提取成功”这是不对的。我的交付标准是嵌入、传播、提取三步闭环。具体说就是先写一个自动化测试脚本模拟“嵌入-保存文件-粘贴复制-发送到目标平台-从目标平台复制出来-提取”的完整链路。在这个测试脚本里我会额外做两件事第一攻击者视角测试——把文本里的标点全部替换成规范标点或者把所有空格去掉看提取还能不能成功第二干扰项测试——在文本任意位置手动插入几个空格、换行或标点看提取程序是否能正确跳过干扰字符、仍然还原原始信息。一个好的隐写实现应该具备一定的容错性至少不能因为一个标点被改动就整体崩溃。我在开发中还会给每种方案做一份“信号强度日志”记录嵌入长度、可提取率、视觉无感程度。别嫌麻烦等你要在生产和对方扯皮时这份日志就是你证明“内容是从哪儿泄露的”最硬气的证据。6. 一点个人的经验总结折腾文本隐写术这几年我最深的体会是隐写术的核心不在于“藏得多高级”而在于“藏完之后能被稳定地取出来”。很多刚从理论上手的开发者会把精力全放在编码算法的复杂度上忽略了真实传输链路上的各种清洗、转换、规范化和编辑器“好心帮忙”结果一到线上就翻车。反而是那些看似简陋的方案比如标点切换、行尾空格在真实世界里活得最久。另外一个我这些年坚持的原则是隐写只是手段取证链路要前置。真正到了追责阶段你需要的不只是“我藏了一段ID”而是“我把这段ID藏在了哪儿、经过哪些渠道、谁能看到”。所以每次正式启用一套隐写方案我都不光写嵌入代码还会配套写清楚提取协议、版本号、样例数据。防泄露这件事永远是要做在事前的。如果你看完也想在自己的项目里试试我的建议是从零宽字符标点全半角入手这两个方案代码量最小、效果最直观跑通一遍就能理解隐写术的核心逻辑。等你真正理解了“字符码位即信道”这个思想后面再接触同形字符、词序隐写就是水到渠成的事了。
返回列表