全所为模仿作品中,多少会有密文的出现,为了能够更方便地创作或者解读,能够使用自动化工具提升效率是很重要的事情。我因此写下此文,给大家提供一些常见的 SBAN 密文 / 解读的思路。
这篇文章默认读者拥有基础的 Python 开发能力。如果你读不懂代码,大可以将代码直接复制,去请教当下的各种 AI 工具。
教育枠. - 任意的进制转换
在 Python 中储存效率最高的数字是 int,我们肉眼所见的十进制,在计算机底层是二进制。因此,我们的进制转换围绕 int 展开,包括从其他进制转换到 int 和从 int 输出各种进制的内容。
从任意进制到 int
一般来说,我们可以通过指定 int 构造函数的 base 属性来定义转换的进制:
print(int("123", base=4))
# 结果:27
print(int("123", base=10))
# 结果:123
print(int("Ff", base=16))
# 结果:255,告诉我们这个函数是大小写无关的
但假如我们有,或者需要更一般的密码本,视作 N 进制,来将其他内容转换为数字,那么我们就需要自己构造函数来解决这样的问题:
def code_to_int(raw: str, codec: str) -> int:
result = 0
for r in raw:
result *= len(codec)
result += codec.index(r)
return result
if __name__ == "__main__":
print(code_to_int("ababa", "abcdefghij"))
这里相当于是说,abcdefghij 是一个密码本,相当于是十进制的,因为密码本有十项,分别代表 0-9 的数字。
从 int 到任意进制
在 Python 中,并没有给出一种方便快捷的方式来将数字转换成任意进制。对于一些常见的进制,Python 还是自带的:
print(hex(100))
# 结果:0x64,是字符串类型的十六进制
print(oct(100))
# 结果:0o144,是字符串类型的八进制
print(bin(100))
# 结果:0b1100100,是字符串类型的二进制
因此,对于其他进制(说的就是你,七进制),或者自定义的密码本,我们就需要自己构造一个函数:
def int_to_code(num: int, codec: str) -> str:
result = ""
while num:
result = codec[num % len(codec)] + result
num //= len(codec)
return result
if __name__ == "__main__":
print(int_to_code(100, "0123456"))
# 结果:202
アブジェ枠. - 编码转换
编码是全所为模仿偷懒中,最常用的方法。这中间就涉及到文本的编码。简单来说,编码是一套规则,用于定义计算机中的 0/1 串怎么对应到我们常见的文字。
不少朋友会想到 Unicode,因为 Unicode 被称为万国码,编码了计算机能显示的尽可能多种类的文字。但是,Unicode 并不是在实际中能够开箱即用的。简单来说,是因为 Unicode 对一个字的编码并不是等长的,所以你不知道相邻的字节中,哪些和哪些应该分成一个字。
Unicode 的编码和解码
如果你需要查看一个字的 Unicode 编码,或者从 Unicode 编码反推文字,你可以使用 Python 自带的 chr 和 ord 函数:
print(ord("你")) # 20320
print(hex(ord("你")) # 0x4f60
print(chr(20320)) # 你
print(chr(int("4f60", base=16))) # 你
因此,如果你是解读者,假如对面使用 16 进制编码,而且你发现并不符合你见过的常见编码方案,你可以尝试使用手动分段的方式来编码 Unicode:
def encode_unicode(raw: str) -> bytes:
result = b""
for r in raw:
result += ord(r).to_bytes(2) # 代表两个字节宽度
return result
encode_unicode("你好,世界!")
# b'O`Y}\xff\x0cN\x16uL\xff\x01'
# 这种一团乱麻的四不像。。。
def decode_unicode(raw: bytes) -> str:
result = ""
for i in range(0, len(raw), 2):
# 代表截取两个字节
result += chr(int.from_bytes(raw[i: i+2]))
return result
decode_unicode(b'O`Y}\xff\x0cN\x16uL\xff\x01')
# '你好,世界!'
相当于说,对于编码后的字节串,每两个字节固定编码一个字符。对于英语来说,如果也是每两个字节编码一个字符,会带来很大的浪费。
这里用到的 int.from_bytes 和 int.to_bytes 两个函数值得拿出来说说。这是最简单的将 int 和 bytes 互相转换的方法。它有一些其他值得关注的参数可以设置:
signed
signed 意味着有符号的。如果将 signed 设置为 True,意味着编码出来的字节串代表有符号整数。
# 会报错的写法:
# print((-1).to_bytes(2))
# Traceback (most recent call last):
# File "<python-input-36>", line 1, in <module>
# (-1).to_bytes(2)
# ~~~~~~~~~~~~~^^^
# OverflowError: can't convert negative int to unsigned
# 一般的写法:
print((-1).to_bytes(2, signed=True))
# b'\xff\xff'
print((-1).to_bytes(8, signed=True))
# b'\xff\xff\xff\xff\xff\xff\xff\xff'
假如有出题者很坏,全部取了反码,那么这个在需要按位取反的时候,非常有用:
# 假如已经解读出了字节串
# 19 77 6e 1b 47 72 19 67 50 18 44 66
raw = b"\x19\x77\x6e\x1b\x47\x72\x19\x67\x50\x18\x44\x66"
num = int.from_bytes(raw)
num = ~num
final = num.to_bytes(13, signed=True) # 多出来一位,用于记录负数
final = final[1:] # 把它切掉就好
print(final)
# b'\xe6\x88\x91\xe4\xb8\x8d\xe6\x98\xaf\xe7\xbb\x99'
print(final.decode())
# 我不是给
Bytes 和 str 的转换
正如上面所见,有 str.encode() 和 bytes.decode() 两个方法存在,我们可以对字符串进行编码和解码:
byte_raw = "東方紅魔郷".encode("shift-jis")
wrong_encoding = byte_raw.decode("gbk")
print(wrong_encoding)
# 搶曽峠杺嫿
相当于说,str 类型携带的是文本的信息(里面存着的是一个一个的 Unicode 编码之类的东西),而 bytes 则携带的不是文本信息,而是纯粹的二进制流。使用错误的方式解码会带来乱码。
一般情况下,当在 Python 中遇到无法解码的情况,会报错,但是,我们可以强制它不报错,来看到损坏了的字符串:
# 会报错的语句
# "東方紅魔郷".encode().decode("gbk")
# Traceback (most recent call last):
# File "<python-input-80>", line 1, in <module>
# "東方紅魔郷".encode().decode("gbk")
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
# UnicodeDecodeError: 'gbk' codec can't decode byte 0xb7 in position 14: incomplete multibyte sequence
# decoding with 'gbk' codec failed
print("東方紅魔郷".encode().decode("gbk", "replace"))
# '鏉辨柟绱呴瓟閮�'
在这种情况下,出现的 � 字符代表着这个字符的二进制信息已经丢失,就算重新编码,它会被替换成「错误字符」的编码,而不是原字符的编码。所以在这里,就没办法无损转化回原来的信息。
常见的编码类型
如果你的出题者比较良心,那么他们通常会使用常见的编码,下面是一些常见编码的列举和推断技巧:
ASCII
对于纯英文情景,ASCII 码的内容都落在 20~7E 之间。
51 75 65 72 79
Q u e r y
UTF-8
对于 UTF-8 编码的不论是中文还是日文,都基本是三个字节一个字,可以看得出来它有类似于三为周期的循环。而且每个循环看起来都是 E 开头的
E5 BE 88 很 E3 82 B3 コ
E4 B9 85 久 E3 83 BC ー
E4 BB A5 以 E3 83 89 ド
E5 89 8D 前 E3 81 A7 で
EF BC 8C , E3 81 99 す
GB2312 和 GBK
这是中文电脑操作系统中常用的编码,特点是两个字节一个字符,支持中文。
D6 D8 C7 EC BB F0 B9 F8
重 庆 火 锅
Big5
这个编码主要支持繁体中文。如果有台湾的 Sban 模仿者,他们或许会使用这个编码来藏密文:
A3 74 A3 75 A3 76 A3 77 AA 60 AD B5
ㄅ ㄆ ㄇ ㄈ 注 音
Shift-JIS
日文区 Sban 常用编码。
83 41 83 43 83 58 81 41 88 a4 82 b7
ア イ ス 、 愛 す
拓展 - 编码推测
要推测一段 bytes 的编码,一个叫 chardet 的外置库很有用。当然,记得先安装:
pip install chardet
然后就可以尝试检测一段数据的编码。但是,正确率其实不高,不如我们自己一个一个编码尝试:
import chardet
print(chardet.detect("你好,世界".encode()))
# {'encoding': 'utf-8', 'confidence': 0.99, 'language': ''}
print(chardet.detect("你好,世界".encode("gbk")))
# {'encoding': 'TIS-620', 'confidence': 0.21589272722169808, 'language': 'Thai'}
print(chardet.detect("世界".encode("shift-jis")))
# {'encoding': None, 'confidence': 0.0, 'language': None}