使用零宽字符对文本加密的实现
发布于
本文为 www.52pojie.cn 首发
《使用零宽字符对文本加密的实现》
@TLHorse
前言
说来话长。其实就是前几天我看到了一篇介绍Unicode的文章,里面介绍Unicode字符的广泛性。其中有一类字符叫做零宽字符,它们在电脑上输入,不可见,也不可打印,甚至输入都不会占空间,作用是控制文字排列或解决个别语言中的排版问题。
常见的零宽字符有以下六种:
| 中文名 | 英文名 | U+ | 作用 |
|---|---|---|---|
| 零宽度空格符 | zero-width space | U+200B | 用于较长单词的换行分隔。 |
| 零宽度非断空格符 | zero width no-break space | U+FEFF | 用于阻止特定位置的换行分隔。 |
| 零宽度连字符 | zero-width joiner | U+200D | 用于阿拉伯文与印度语系等文字中,使不会发生连字的字符间产生连字效果。 |
| 零宽度断字符 | zero-width non-joiner | U+200C | 用于阿拉伯文、德文、印度语系等文字中,阻止会发生连字的字符间的连字效果。 |
| 左至右符 | left-to-right mark | U+200E | 用于在混合文字方向的多种语言文本中(例:混合左至右书写的英语与右至左书写的希伯来语),规定排版文字书写方向为左至右。 |
| 右至左符 | right-to-left mark | U+200F | 用于在混合文字方向的多种语言文本中,规定排版文字书写方向为右至左。 |
使用这些零宽字符可以实现发空白消息、发空白朋友圈等效果。不妨动脑筋一想,我们可以把这几种字符组合起来,就可以实现加密字符串的效果。
基本思路
这篇文章咱们只介绍一个初步实现,说不定以后我会出后续,给应用添加更多功能。下面是加解密的流程图。
总之我认为解密过程较为繁琐。
核心实现
激动人心的时刻到啦。让我们一步步编写。 首先在全局定义三个常量:
ZW_ONE = u"\u200b" # 用来翻译1
ZW_ZERO = u"\u200c" # 用来翻译0
ZW_SEP = u"\u200d" # 用来翻译字符之间的间隔
加密函数 str2zwstr(origin)
首先我们新建一个空数组,用来存储字符串每一项,并且遍历明文,使明文的每个字符先用ord()转换成十进制数字,再用bin()转换成二进制。再转换成字符串格式,最后去掉0b前缀:
bin_text = []
for char in origin:
bin_text.append(str(bin(ord(char))).lstrip('0b'))
再创建一个空字符串,用来存储最终的结果:
final_str = ""
之后进行两次遍历,先浅层遍历bin_text,然后为每一项深层遍历:
for item in bin_text: # 遍历大数组每一项
for binchar in item: # 遍历每一项中的0和1
final_str += ZW_ONE if binchar == "1" else ZW_ZERO # 把0、1分别翻译成两种零宽度字符串
final_str += ZW_SEP # 每一项(字符)结束后,插入一个分隔符号
final_str.rstrip(ZW_SEP) # 去掉
return final_str
最后返回final_str即可。
整体代码:
def str2zwstr(origin):
bin_text = []
for char in origin:
bin_text.append(str(bin(ord(char))).lstrip('0b'))
final_str = ""
for item in bin_text:
for binchar in item:
final_str += ZW_ONE if binchar == "1" else ZW_ZERO
final_str += ZW_SEP
final_str.rstrip(ZW_SEP)
return final_stry
解密函数zwstr2str(enc_str)
首先我建议大家再看看基本思路中的流程图。解密不大相同。因为一开始我们要把翻译后的数据存储到第一个字符上,但是遇到分隔符后,我们又得新建一个字符,并把接下来的翻译后的数据存储到第二个字符串上,因此我们要编写一个函数apponlast(arr, sth),每次运行,都将sth拼接到arr的最后一项中,如果arr的项数为0,即新增一个元素。
我这里用的代码极为简洁:
def apponlast(arr, sth):
la = len(arr) # la 是 arr 的长度
if la: arr[la-1] += sth # 如果长度不为0,那么就把 arr 的最后一项与 sth 字符串拼接
else: arr.append(sth) # 如果数组里没有元素,新建一个元素
之后编写解密函数:
def zwstr2str(enc_str):
arr_oz = [] # 由0和1构成的字符串构成的数组
for char in enc_str: # 在密文里遍历
if char == ZW_ONE: apponlast(arr_oz, "1") # 如果是\200b,翻译成1
elif char == ZW_ZERO: apponlast(arr_oz, "0") # 如果是\200c,翻译成0
elif char == ZW_SEP: arr_oz.append("") # 如果是分隔符,把数组新建一项,重新开始循环
else: print("Input contains non-ZW string. Aborted."); getinput() # 如果密文中有非零宽字符,终止解密并回到程序主函数(我们一会要编写)
for idx in range(0, len(arr_oz)-1): # 遍历这个由0和1构成的字符串构成的数组
arr_oz[idx] = chr(int(arr_oz[idx], 2)) # 把每一项先转换为int(注意二进制参数),然后用chr转换为字符
return "".join(arr_oz) # 拼接
完善程序
接下来我们为程序添加一个主函数,并且加些花哨的功能。
if __name__ == '__main__':
pbanner()
getinput()
pbanner()用来打印banner:
def pbanner():
banner = colored(f"""
______
/___ /\\ Zerowidth String Encoder | @TLHorse from 52pojie
// / / \\\\ Type in then ENTER. The encoded string
\\\\ / /__// will be copied & printed.
\/_____/ Commands | ::openweb:: ::banner:: ::quit:: ::switchmode::
""", 'yellow')
print(banner)
需要说明一下,上面的colored函数需要依赖一个第三方库termcolor,可以打印出彩色字符串。
我们在全局设置两个变量:
MODE_ENCODE = True # 用来记录模式是加密还是解密
LAST_RESULT = "" # 用来记录上次操作的结果
getinput是一个递归,可以像命令行一样获取用户输入,代码比较复杂,功能很多,本来是有注释的,结果浏览器编辑的时候不小心给关了,没有恢复成功。大家自己摸索吧:
def getinput():
global MODE_ENCODE, LAST_RESULT
info = ""
if MODE_ENCODE: info = colored("ENCODE", 'red')
else: info = colored("DECODE", 'green')
input_str = input(f'[{info}] ')
if input_str == '::openweb::': os.system('open https://www.52pojie.cn') # 打开吾爱网页
elif input_str == '::banner::': pbanner() #打印banner
elif input_str == '::quit::': sys.exit(0) #退出程序
elif input_str == '::switchmode::': MODE_ENCODE = False if MODE_ENCODE else True #切换加解密
elif input_str == '::cp::': os.system(f'echo {LAST_RESULT} | pbcopy') #复制结果
if input_str.startswith("::") and input_str.endswith("::"): getinput() #检测是否为命令
out = str2zwstr(input_str) if MODE_ENCODE == True else zwstr2str(input_str)
print(colored(' >>> "', 'green')+ out + colored('"', 'green'))
LAST_RESULT = out
getinput()
别忘了import进类库:
import os, sys
from termcolor import colored
大功告成!
后记
首先,想说明一点,文中的结果复制功能是基于pbcopy命令的,这个只有Linux和Unix有,Windows没有。所以Windows小伙伴们记得使用pyperclip库实现复制功能。
其次,我也不是计算机专业人士,所以代码的繁琐与不妥当之处欢迎在评论区指正。
所有代码长这样:
import os, sys
from termcolor import colored
ZW_ONE = u"\u200b"
ZW_ZERO = u"\u200c"
ZW_SEP = u"\u200d"
MODE_ENCODE = True
LAST_RESULT = ""
def pbanner():
banner = colored(f"""
______
/___ /\\ Zerowidth String Encoder | @TLHorse from 52pojie
// / / \\\\ Type in then ENTER. The encoded string will be printed.
\\\\ / /__// Commands | ::openweb:: ::banner:: ::quit:: ::switchmode::
\/_____/ ::cp::
""", 'yellow')
print(banner)
def apponlast(arr, sth):
la = len(arr)
if la: arr[la-1] += sth
else: arr.append(sth)
def str2zwstr(origin):
bin_text = []
for char in origin:
bin_text.append(str(bin(ord(char))).lstrip('0b'))
final_str = ""
for item in bin_text:
for binchar in item:
final_str += ZW_ONE if binchar == "1" else ZW_ZERO
final_str += ZW_SEP
final_str.rstrip(ZW_SEP)
return final_str
def zwstr2str(enc_str):
arr_oz = []
for char in enc_str:
if char == ZW_ONE: apponlast(arr_oz, "1")
elif char == ZW_ZERO: apponlast(arr_oz, "0")
elif char == ZW_SEP: arr_oz.append("")
else: print("Input contains non-ZW string. Aborted."); getinput()
for idx in range(0, len(arr_oz)-1):
arr_oz[idx] = chr(int(arr_oz[idx], 2))
return "".join(arr_oz)
def getinput():
global MODE_ENCODE, LAST_RESULT
info = ""
if MODE_ENCODE: info = colored("ENCODE", 'red')
else: info = colored("DECODE", 'green')
input_str = input(f'[{info}] ')
if input_str == '::openweb::': os.system('open https://www.52pojie.cn')
elif input_str == '::banner::': pbanner()
elif input_str == '::quit::': sys.exit(0)
elif input_str == '::switchmode::': MODE_ENCODE = False if MODE_ENCODE else True
elif input_str == '::cp::': os.system(f'echo {LAST_RESULT} | pbcopy')
if input_str.startswith("::") and input_str.endswith("::"): getinput()
out = str2zwstr(input_str) if MODE_ENCODE == True else zwstr2str(input_str)
print(colored(' >>> "', 'green')+ out + colored('"', 'green'))
LAST_RESULT = out
getinput()
if __name__ == '__main__':
pbanner()
getinput()
最后我先说明,当我把论坛上的文章贴到博客里,我惊奇地发现论坛阅读量少得可怜。但我写文章终究不是给别人看的。