写代码的人大概都有过这种时刻:调试一个二进制协议,日志里冒出一串0x0A、0x0D,脑子里飞速回忆“回车是13还是10”;或者处理一段文本,需要判断某个字符到底落在数字区、大写字母区还是小写字母区。这时候去搜一张 ASCII 码表,图片模糊、水印挡字、还得放大看,体验相当糟糕。其实 ASCII 码速查表这种东西,最靠谱的做法是自己用 Python 生成一份——字符与码值的映射是确定的,chr()和ord()现算就行,输出成什么格式完全由你决定。整个过程三十来行核心代码,跑一次三分钟,生成的文件可以打印贴在显示器旁边,也可以塞进项目文档里当附录。这篇就把我自己的生成脚本从头拆一遍,包含数据结构设计、中英混排对齐的坑、四种输出格式的完整实现,以及几个只有实际跑过才会遇到的问题。不管你是刚学 Python 的新手,还是想给团队做一份编码对照资料的老人,都能直接拿走改。
1. 这张表为什么值得自己生成,而不是下载一张图
1.1 背 ASCII 表的真实成本与收益比
先说结论:绝大多数人背 ASCII 表的方式是错的。我见过不少初学者拿张表从头念,从 0 念到 127,念完第二天忘一半。原因很简单,这 128 个数字对人类的工作记忆来说太多了,而且是纯粹的机械记忆,没有逻辑挂钩点。
但反过来说,完全不记也不行。真实场景里,你需要能做到“看到 0x41 知道是 A”“看到 65 知道是大写字母起点”,这种程度的敏感度省下来的时间非常可观。我的经验是把记忆量压缩到十几个“锚点”,剩下的靠规律推。而规律这种东西,恰恰需要一张排布合理的表来辅助——这就是自己生成的价值:市面上现成的表,排版逻辑未必符合你的记忆习惯,而自己写脚本,想让它是分组视图就是分组视图,想加一列“常见用途”就加一列。
还有一层现实考虑:网上流传的 ASCII 表质量参差不齐,有的只列到 127,有的把 128 到 255 也混进去叫“ASCII 扩展表”,还有的把控制字符的缩写写错。自己从chr()生成,源头就是 Python 解释器本身,不存在抄错的可能。
1.2 真正该记住的是分区规律,不是 128 个数字
ASCII 的可打印区间 32 到 126,其实是一段非常有秩序的分区。我把它拆成这么几块来记:
| 区间 | 内容 | 记忆锚点 |
|---|---|---|
| 0 到 31 | 控制字符 | 只记 9、10、13 三个常用的 |
| 32 | 空格 | 可打印区起点 |
| 33 到 47 | 标点符号 | 感叹号开头,斜杠收尾 |
| 48 到 57 | 数字 0 到 9 | ord('0')等于 48,这是黄金锚点 |
| 58 到 64 | 标点符号 | 冒号、分号、小于号、等号、大于号、问号、@ |
| 65 到 90 | 大写字母 A 到 Z | ord('A')等于 65 |
| 91 到 96 | 标点符号 | 方括号、反斜杠、脱字符、下划线、反引号 |
| 97 到 122 | 小写字母 a 到 z | ord('a')等于 97 |
| 123 到 126 | 标点符号 | 花括号、竖线、波浪号 |
| 127 | DEL | 删除控制字符 |
有了这张分区表,加上三个锚点 48、65、97,剩下的全部可以推:ord('7')就是 48+7=55,ord('C')就是 65+2=67,ord('c')就是 97+2=99。而且你会发现大写和小写之间差了 32,这个 32 正好是一位二进制的差距(0x20),所以大小写转换在底层就是一个位运算。这种“规律 + 锚点”的记忆方式,比死记硬背高效一个数量级。
那自己生成的表要怎么排布,才能服务这套规律?这就是后面要讨论的设计问题。
2. 生成方案的三次选型
2.1 数据源:手写字典还是 chr() 现算
第一个决策点是字符数据从哪来。有三个选项。
选项一,纯现算:用chr(i)拿到字符,用i作为码值,描述信息靠区间判断生成。代码最短,但控制字符没法直接显示(很多控制字符打印出来是空白或者干脆触发终端行为,比如 7 会让蜂鸣器响,8 会退格),所以还需要一张小的控制字符名称表来兜底。
选项二,全手写字典:把 128 个条目一条条写进字典,包含码值、字符、十六进制、中文说明。优点是描述可以写得非常细,缺点是要打 128 行,手打容易出错,而且这本身就是机械劳动——我们本来就是为了避免机械劳动才写脚本的。
选项三,混合:可打印字符用chr()现算,控制字符用一张 33 条的小表描述。这是我在实际项目里最终采用的方式。
提示:混合方案的关键认知是,ASCII 里真正需要“人工知识”的只有 33 个控制字符(0 到 31 加 127),剩下 95 个可打印字符的信息完全可以从码值推导。把人工输入量从 128 压到 33,出错概率也跟着降了一个数量级。
2.2 输出载体:终端、Markdown、HTML、CSV 怎么选
第二个决策点是输出成什么。这取决于你打算怎么用。
- 终端直接看:适合快速查阅和调试时临时扫一眼,缺点是无法打印,也没法加超链接。
- Markdown 文件:适合塞进项目仓库当文档附录,可以被渲染、被检索、被版本管理。
- HTML 页面:适合打印。可以精确控制字号、网格、分页,还能加悬停高亮,做成一张真正的“墙贴表”。
- CSV 表格:适合二次加工,比如导入电子表格软件做筛选排序,或者被其他脚本读取。
我的做法是四种都生成,反正逻辑复用,每个渲染函数只负责把同一份数据换一种外壳。这样一份脚本吃四种产出,成本几乎为零。
2.3 排版逻辑:线性铺开还是按功能分区
第三个决策点是表的组织方式。最朴素的做法是从 0 到 127 一条一条往下排,好处是查码值方便(按十进制顺序),坏处是看不到分区规律——数字 0 到 9、字母 A 到 Z 这些有意义的块被切散了。
我最终采用的是双视图:主视图按码值线性铺开,保证查表能力;同时在表头用颜色或者分隔线标出分区边界。具体实现上,给每个条目算一个“类别”字段,这个字段既用来着色,也用来在 HTML 版里做分组标题。
线性视图还有一个隐藏好处:当你要批量输出、按终端宽度自适应分栏时,线性序列是最容易切成等长块的。如果是分组视图,每组长度不一致,分栏逻辑会复杂不少。
3. 核心代码拆解:从 chr() 到一张能贴墙上的表
3.1 控制字符命名表:那 33 条必须先写死的知识
控制字符的官方缩写是有标准答案的,不能瞎编。我整理的这张表涵盖了 0 到 31 和 127:
CONTROL = { 0: ("NUL", "空字符"), 1: ("SOH", "标题开始"), 2: ("STX", "正文开始"), 3: ("ETX", "正文结束"), 4: ("EOT", "传输结束"), 5: ("ENQ", "请求"), 6: ("ACK", "确认响应"), 7: ("BEL", "响铃"), 8: ("BS", "退格"), 9: ("HT", "水平制表符"), 10: ("LF", "换行"), 11: ("VT", "垂直制表符"), 12: ("FF", "换页"), 13: ("CR", "回车"), 14: ("SO", "移出"), 15: ("SI", "移入"), 16: ("DLE", "数据链路转义"), 17: ("DC1", "设备控制1"), 18: ("DC2", "设备控制2"), 19: ("DC3", "设备控制3"), 20: ("DC4", "设备控制4"), 21: ("NAK", "拒绝接收"), 22: ("SYN", "同步空闲"), 23: ("ETB", "传输块结束"), 24: ("CAN", "取消"), 25: ("EM", "介质中断"), 26: ("SUB", "替换"), 27: ("ESC", "转义"), 28: ("FS", "文件分隔符"), 29: ("GS", "分组分隔符"), 30: ("RS", "记录分隔符"), 31: ("US", "单元分隔符"), 127: ("DEL", "删除"), }这 33 条里,日常真正会打交道的不超过 6 个:9 制表、10 换行、13 回车、27 转义、0 空字符、127 删除。其余大部分是早期通信协议时代的遗产,现代开发里基本见不到。
注意:不同系统对换行符的处理不一样。文本层面的“换行”习惯用 10,但在一些网络协议里换行往往记作 13 加 10 两个字符。写解析逻辑时一定要区分清楚,别拿着 10 去匹配 13 加 10,这是新手最常翻的车之一。
有了这张表,再加两个辅助函数就能把任意码值转成“可展示的一行”:
def display_char(code: int) -> str: """返回用于展示的字符,控制字符用尖括号包裹缩写。""" if code in CONTROL: return f"<{CONTROL[code][0]}>" if code == 32: return "SP" return chr(code) def describe(code: int) -> str: """返回中文类别描述。""" if code in CONTROL: return CONTROL[code][1] if code == 32: return "空格" if 48 <= code <= 57: return "数字" if 65 <= code <= 90: return "大写字母" if 97 <= code <= 122: return "小写字母" return "标点符号"空格这里我特意用SP两个字母表示,而不是直接输出一个空格字符。原因很实际:在控制台里打印一个空格,你根本看不出那里有个字符还是排版本身留的空,查表时容易误判。用一个显式标记,可读性立刻上一个台阶。
3.2 中英混排的宽度对齐:ljust 为什么不管用
这是一个不写代码就想不到的坑。Python 的str.ljust(10)是按字符个数补空格的,不是按显示宽度。而中文字符在等宽终端里通常占两列,所以当你的表里既有英文缩写又有中文描述时,ljust出来的结果一定是歪的。
正确的做法是先算显示宽度,再按宽度补空格:
import unicodedata def display_width(text: str) -> int: """计算字符串在等宽终端中的显示宽度。""" width = 0 for ch in text: if unicodedata.east_asian_width(ch) in ("W", "F"): width += 2 else: width += 1 return width def pad(text: str, width: int) -> str: """按显示宽度右补空格。""" gap = width - display_width(text) return text + " " * max(gap, 0)east_asian_width返回W(宽)和F(全角)都按 2 列算,其他按 1 列算。这个判断覆盖了绝大多数常见场景:汉字、全角标点、日文假名都是 W,拉丁字母和半角数字都是 Na 或 H,按 1 算没问题。
实测下来,这套逻辑处理“ 空字符”这种混排内容是准的,处理纯 ASCII 也准,唯一需要额外留心的是组合字符和部分特殊符号,但 ASCII 表这个场景完全用不到。
3.3 终端渲染:按列数自适应分栏
终端版的难点不在对齐,在于怎么把 128 条塞进一行 80 或 100 列里。我的做法是算好单格宽度,再算每行能放几组:
def render_terminal(per_line: int = 4) -> None: cell_width = 26 header = "十进制 十六 字符 说明" print(header) print("-" * (cell_width * per_line)) for start in range(0, 128, per_line): row = [] for code in range(start, min(start + per_line, 128)): cell = ( f"{code:>3} {code:02X} " f"{pad(display_char(code), 5)}" f"{pad(describe(code), 12)}" ) row.append(cell) print("".join(row)){code:02X}是十六进制格式化,两位大写,不足补零,chr(10)出来就是0A。这里不用hex(),因为hex(10)返回0xa,带前缀、小写、还只有一位,还需要额外裁剪,不如直接用格式化字符串干净。
每行放几组这个参数,可以按终端宽度反推。一个单元格大约 26 个字符宽,100 列的终端最多放 3 组还不显挤,用默认的 4 组在 110 列以上的窗口比较舒服。如果你不确定窗口多宽,可以用标准库去问:
import shutil cols = shutil.get_terminal_size().columns per_line = max(1, cols // 26)这样就不用纠结了,脚本自己适配。
3.4 HTML 渲染:打印出来才是它的归宿
HTML 版是我用得最多的一种,因为最终是要打印贴墙的。核心思路是把每个条目做成一张小卡片,用 CSS Grid 排布,一页正好能放完。
import html CSS = """ body { font-family: system-ui, sans-serif; margin: 24px; } .grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 6px; } .card { border: 1px solid #ccc; border-radius: 4px; padding: 4px; font-size: 12px; } .ctrl { background: #f2f2f2; } .letter { background: #eef6ff; } .digit { background: #eefaf0; } .punct { background: #fffdf0; } .dec { font-weight: bold; margin-right: 4px; } .hex { color: #777; margin-right: 4px; } .ch { font-family: monospace; font-size: 14px; } .desc { display: block; color: #555; font-size: 11px; } @media print { .card { break-inside: avoid; } } """ def build_html(path: str = "ascii_sheet.html") -> None: cards = [] for code in range(128): if code in CONTROL: cls = "ctrl" elif 48 <= code <= 57: cls = "digit" elif 65 <= code <= 90 or 97 <= code <= 122: cls = "letter" else: cls = "punct" cards.append( f'<div class="card {cls}">' f'<span class="dec">{code}</span>' f'<span class="hex">{code:02X}</span>' f'<span class="ch">{html.escape(display_char(code))}</span>' f'<span class="desc">{html.escape(describe(code))}</span>' f"</div>" ) doc = ( '<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8">' "<title>ASCII 速查表</title>" f"<style>{CSS}</style></head><body>" '<div class="grid">' + "".join(cards) + "</div></body></html>" ) with open(path, "w", encoding="utf-8") as f: f.write(doc)这里有两个细节值得单独说。第一是html.escape(),因为<、>、&、"这四个字符在 HTML 里有特殊含义,而我们展示控制字符用的尖括号恰好是<和>,不转义的话页面结构直接崩掉,显示出来的东西和预期完全两样。第二是break-inside: avoid,它保证打印时一张卡片不会被撕到两页上,否则一张墙贴表会出现半截卡片,非常难看。
CSS 里那四种背景色不是装饰,是信息编码:灰色代表控制字符(不可见,只能靠名字识别),绿色代表数字区,蓝色代表字母区,浅黄代表标点区。人眼对色块的分区识别速度远快于读文字,贴上墙之后扫一眼就知道该往哪个方向找。
3.5 CSV 与 Markdown:给机器读的版本
CSV 版的用途是把表交给别的程序或者电子表格处理,所以列要开得全一些:
import csv def build_csv(path: str = "ascii_sheet.csv") -> None: with open(path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow( ["十进制", "十六进制", "八进制", "二进制", "字符", "类别", "说明"] ) for code in range(128): writer.writerow([ code, f"{code:02X}", oct(code)[2:], f"{code:08b}", display_char(code), describe(code), CONTROL[code][1] if code in CONTROL else "", ])encoding="utf-8-sig"这个参数很多人会忽略。普通的utf-8写出来的文件,在一些电子表格软件里打开会变成乱码,原因是软件在猜编码时缺少依据。加了-sig之后,文件头会多写三个字节的标识,软件一看就知道这是 UTF-8,中文就不会乱了。这三个字节对 Python 自己读取没有影响,utf-8编解码器会自动跳过。
Markdown 版则是给仓库文档用的:
def build_markdown(path: str = "ascii_sheet.md") -> None: lines = ["| 十进制 | 十六 | 字符 | 说明 |", "| --- | --- | --- | --- |"] for code in range(128): ch = display_char(code).replace("|", "\\|") lines.append(f"| {code} | {code:02X} | `{ch}` | {describe(code)} |") with open(path, "w", encoding="utf-8") as f: f.write("\n".join(lines))这里replace("|", "\\|")是必须的,因为竖线本身的字符就是|,它会把 Markdown 表格的列切断。对 124 这个码值来说,这一行不处理就会渲染错乱。
4. 三分钟跑通全流程:从建文件到拿到成品
4.1 环境准备:比你想象的还简单
整个脚本只依赖标准库,csv、html、unicodedata、shutil全部内置于 Python 3,不需要装任何第三方包。版本要求也不高,f-string是 3.6 引入的,shutil.get_terminal_size更早就有,实际上 Python 3.6 以上都能跑。
文件结构建议就两个文件,把渲染逻辑和入口分开,方便以后改:
ascii_tool/ ├── ascii_sheet.py # 主逻辑 └── out/ # 输出目录,脚本自动创建入口函数长这样:
import os def main() -> None: os.makedirs("out", exist_ok=True) render_terminal() build_html(os.path.join("out", "ascii_sheet.html")) build_csv(os.path.join("out", "ascii_sheet.csv")) build_markdown(os.path.join("out", "ascii_sheet.md")) print("\n已生成:HTML / CSV / Markdown 三个版本") if __name__ == "__main__": main()跑起来就是一行命令:
python ascii_sheet.py4.2 先来一个 30 秒速食版
如果你现在就想看结果,不想建文件,直接命令行一行搞定可打印区:
python -c "print('\n'.join(f'{i:3d} {i:02X} {chr(i)}' for i in range(32, 127)))"这行命令在 Linux 和 macOS 的终端、以及 Windows 的 PowerShell 里都能跑。它只输出十进制、十六进制和字符三列,128 行里的可打印部分一次刷完。缺点是控制字符区没有,中文描述也没有,只能应急看。
对 Windows 的经典命令提示符,如果输出里出现问号方块或者乱码,先把代码页切到 UTF-8 再跑:
chcp 65001或者在 Python 里对标准输出重设编码:
import sys sys.stdout.reconfigure(encoding="utf-8")reconfigure是 3.7 才有的方法,老版本可以用io.TextIOWrapper包一层。这个坑后面还会细说。
4.3 终端版的实际观感与参数调整
我实际跑出来的终端版大致是这样(只截了前几行):
十进制 十六 字符 说明 ------------------------------------------------------------ 0 00 <NUL> 空字符 1 01 <SOH> 标题开始 2 02 <STX> 正文开始 3 03 <ETX> 正文结束 4 04 <EOT> 传输结束 5 05 <ENQ> 请求可以看到控制字符那一行因为名字短、中文短,右侧留白比较明显,而到了字母区就会变得紧凑。如果你觉得四列太挤,把per_line改成 3 或者 2,胎宽 80 的终端用 3 列刚好;如果屏幕够宽,也可以上 5 列,但五行之后就容易串行了,眼睛跟不住。
这里有个小技巧:把终端输出的内容直接重定向到文件,就得到了一份纯文本版,可以放进任何不支持富文本的地方:
python ascii_sheet.py > out/ascii_sheet.txt注意这时候shutil.get_terminal_size()拿不到真实宽度,会返回默认值或环境变量里的值,所以自适应分栏可能不准,建议先设好COLUMNS环境变量再重定向。
4.4 HTML 打印:一次调好,长期受益
生成 HTML 之后用浏览器打开,直接打印。我踩过的两个点:
第一是页面缩放。默认打印会把内容缩到纸张宽度,8 列网格在 A4 上大约是每张卡片 2 厘米宽,字符看得清,中文描述会小到模糊。我的处理是把网格改成 6 列,字号提到 13px,这样 A4 纵向正好一页装完,中文也清晰。
第二是边距。浏览器默认打印边距比较宽,128 张卡片会被挤到两页以上。用@page规则压一下:
@page { margin: 10mm; }这样一页能从容放下,而且四周留白刚好够贴胶带。
4.5 CSV 拿去二次加工
CSV 版最大的价值是能被当成数据源。比如你想做一个“查码值”的小工具,直接读 CSV 建索引比在代码里硬编码字典更灵活。或者你想统计一下可打印字符里各类标点占多少,几行代码就能出结果:
import csv from collections import Counter with open("out/ascii_sheet.csv", encoding="utf-8-sig") as f: rows = list(csv.DictReader(f)) counter = Counter(r["类别"] for r in rows) for k, v in counter.most_common(): print(f"{k}: {v}")跑出来能直观看到标点符号占了很大比重,这也解释了为什么写文本解析时正则表达式那么复杂——光标点就几十个,每个在正则里还可能有特殊含义。
5. 踩过的坑与排查速查表
5.1 编码报错:UnicodeEncodeError 是怎么回事
最常见的报错长这样:
UnicodeEncodeError: 'gbk' codec can't encode character '\u2588'这是因为在某些 Windows 环境下,标准输出的默认编码是 GBK,而你试图打印的字符不在 GBK 字符集里。注意 ASCII 表本身的所有字符都在 GBK 范围内,所以这个错误通常不是出在表内容上,而是出在你后来加的装饰字符上——比如用方块符号画分隔线,或者用特殊符号标重点。
解决办法就是前面说的sys.stdout.reconfigure(encoding="utf-8")。加上之后所有输出统一走 UTF-8,问题消失。但要注意,重设之后如果终端本身不支持 UTF-8 显示,会看到乱码而不是报错,那是显示层的问题,文件内容是对的。
5.2 128 到 255 那些码值到底算什么
很多人会顺手把range(128)改成range(256),觉得多一倍的字符更全。这在 Python 3 里会得到一些很奇怪的结果:chr(128)到chr(159)在 Unicode 里是控制字符区,打印出来不可见;chr(160)是不间断空格;再往上才是一些带音标的字母和符号。
根本原因是:ASCII 只有 0 到 127。128 到 255 那段,业界习惯叫“扩展 ASCII”,但它从来不是标准,不同代码页对同一字节的解释完全不同。Python 3 的chr()是按 Unicode 码点解释的,所以chr(233)得到的是é,而在某些传统的单字节编码里 233 对应的完全是另一个字符。
提示:如果你的目的是理解“字节和字符的关系”,那需要的是另一套实验——用
encode()和decode()观察同一个字符在不同编码下的字节序列。chr()只处理码点,处理不了编码。这两件事经常被混为一谈,实际上差得很远。
我的建议是老老实实停在 127,把“扩展 ASCII 不是 ASCII”这个概念一开始就建立清楚,比生成一张看似完整实则误导的表有用得多。
5.3 常见问题与排查思路速查
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 终端输出表格歪斜 | 用了ljust按字符数补空格 | 改用display_width计算真实宽度 |
| 控制字符那几行打印后终端行为异常 | 直接输出了原始控制字符 | 统一用display_char转成<NUL>形式 |
| HTML 页面结构错乱 | <>没转义 | 所有动态内容过一遍html.escape |
| Markdown 表格列错位 | 竖线字符未转义 | replace("|", "\\|") |
| CSV 打开中文乱码 | 缺少 BOM | 用utf-8-sig编码写入 |
| 打印后卡片被切到两页 | 没有分页控制 | 加break-inside: avoid |
chr()报 ValueError | 参数越界 | 范围必须是 0 到 0x10FFFF |
| 重定向输出后分栏错乱 | 取不到终端宽度 | 显式设置COLUMNS环境变量 |
十六进制显示成0xa | 用了内置hex() | 改用f"{code:02X}" |
| 空格字符在表里看不出来 | 空格本身无视觉 | 用SP两个字母代替 |
这张表里我最想强调的是第一行和最后一行。对齐问题几乎每个人写表格输出时都会撞一次,而“空格看不见”这个问题更隐蔽——表生成出来看着挺正常,但查表的时候会误以为 32 那个位置是空行,直到某天调试时才反应过来那里有个字符。
5.4 一个容易忽略的细节:脚本自身的可读性
生成脚本往往只用一次,很多人写得比较随意,变量名a、b、c一堆。我的建议是至少把三个东西命名清楚:display_char(负责把码值变成可展示的字符串)、describe(负责生成中文类别)、pad(负责宽度对齐)。这三个函数的职责边界一旦明确,后面想加第五种输出格式(比如 PDF 或者纯文本报告)只需要再写一个渲染函数,不用碰数据层。
我后来给团队做了一份加了“常见用途”列的内部版,比如 9 标“数据分隔”、10 标“行结束”、13 标“光标归位”、27 标“终端控制序列前缀”,就是在这个结构上加了一个字典,改动了不到二十行。结构清晰带来的复用收益,比想象中大。
6. 把这张表用起来:从 ASCII 到编码直觉的延伸
6.1 转义字符、URL 编码里到处都是 ASCII 的影子
生成完表之后你会发现,很多平时见到的编码现象突然就解释得通了。
编程语言里的转义序列\n、\t、\r,对应的就是 10、9、13 这三个控制字符。\0对应 0,\\对应 92,\"对应 34。这些不是随便定的,而是把不可打印或者有语法含义的字符,用反斜杠加字母的方式重新表达。
URL 编码里的百分号形式,比如%20代表空格、%0A代表换行,本质上是把不可见字符用两位十六进制写出来。你在表上查到空格是 32,转成十六进制就是 20,对上了。这也是为什么 URL 里空格不能直接出现——会被当成地址分隔符,必须转义。
再看一些常见的序列化格式,比如表单提交里空格常被写成加号,那是一种历史沿革下来的简写约定,本质仍然是 32 这个码值。把这些现象串起来之后,你对“编码”这件事的直觉会明显不同:不再是一堆需要记的规则,而是同一套数字在换衣服。
6.2 二次加工的几个方向
这张表生成出来不是终点,它是个基础件。我在不同场景下做过这几种变形:
加一列反向查询。把表按字符排序(字符本身作为主键),做成一个char -> code的字典,调试时看到异常字符能立刻反查。注意排序时中文描述会影响结果,只按字符列的码点排。
做一个小型命令行查询工具。传入一个字符返回码值,传入一个数字返回字符,省去打开浏览器的步骤:
import sys arg = sys.argv[1] if len(arg) == 1: print(f"{arg!r} -> 十进制 {ord(arg)}, 十六进制 {ord(arg):02X}") else: code = int(arg, 0) # 支持 65 和 0x41 两种写法 print(f"{code} -> {chr(code)!r}")int(arg, 0)这个写法很实用,它能自动识别0x前缀,所以0x41和65都能传进去。
给新人做一份“编码常识”讲义。把表、分区规律、常见转义、几个典型编码场景串成一页,比扔一个链接过去有效得多。我做过一次,反馈里最多的一句是“原来大小写差 32 是这么回事”。
做校验用。写解析器的时候经常要判断“这个字节是不是可打印字符”,直接拿范围判断就行:32 <= b <= 126。如果表里带了类别标签,还可以细到“是不是数字”“是不是字母”,代码读起来更清楚。
我个人在实际使用中的体会是,这类“看起来只是个小工具”的脚本,真正的价值往往不在脚本本身,而在于写它的过程强迫你把一块知识从头捋了一遍。查一张现成的表,你只得到答案;自己生成一张表,你会顺手把控制字符的名字、十六进制格式、对齐规则、编码边界这些问题全部碰一遍。这几个点单独拎出来都不复杂,但凑在一起,就构成了对“文本在计算机里怎么表示”这件事最基本的直觉。脚本跑完只要三分钟,这份直觉能用很多年。
最后分享一个随手就能验证的小技巧:拿到任何一段看似乱码的文本,先别急着换编码,先把它逐字符打印出来,看看每个字符的码值落在哪个区间。落在 0 到 31 的就是控制字符在捣乱,落在 128 以上的多半是编码解码不匹配。用表一对照,大部分“乱码之谜”当场就能定位到具体是哪个字符出了问题,比盲猜编码效率高得多。