中文字符对齐之我见

本文最后更新于 超过 3 年前,文中所描述的信息可能已发生改变。

众所周知,汉字在等宽字体里的宽度是英文字母的两倍,然而一些编程语言(比如 Python)在计算字符串长度的时候仍然会把汉字也当作 1。所以在许多生成字符列表的程序中,一旦源字符串包含中文,大概率就会出现无法正常对齐的现象。

其实在日常使用中,我们很少会遇到这种现象,毕竟国内主流的程序员群体早就习惯了在英文环境下编程,大家上一次遇见这种跟中文有关的错误可能还是大一学 C 语言时的“烫烫烫烫烫烫烫”。但很不凑巧,在短短三个月之内,我就遇到了两次这样的问题。

第一次遇见这样的问题,是用 everyclass-lite (我写的课表爬虫,用于在每课还未更新最新课表时查询课表) 查询课表时。我为了能在自己的 QQ 机器人上随时查询课表,就必须先把字符表格(又称 AsciiTable)转换成图片,再把让机器人将图片发送给我(QQ 的消息气泡机制造成了直接发字符表格会产生极其严重的错位)。

不过那一次的问题比较简单就能修复,只是用 PILLOW 库生成图片时的字体大小没调好,导致汉字大小与英文字母大小不一致而已,只要简单修改下字体大小就顺利解决了。

第二次遇见这样的问题则是这几天,使用 NoneBot 的命令行工具 nb-cli 时遇到的。nb-cli 有个功能是 List All Published Plugins ,将会列出插件商店中所有插件的名称、链接、简介。如果插件的名称或简介含有汉字,插件列表就无法正常对齐,出现一定程度的错位。

修复前:

shell
服务器状态查看 (nonebot-plugin-status)                       - 通过戳一戳获取服务器状态
HarukaBot (haruka-bot)                                - 将B站UP主的动态和直播信息推送至QQ
rauthman (nonebot-plugin-rauthman)                    - 基于规则的授权管理
NoneBot离线文档 (nonebot-plugin-docs)                     - 在本地浏览NoneBot文档
Sentry日志监控 (nonebot-plugin-sentry)                    - 使用Sentry监控机器人日志并处理报错
前端测试机器人插件 (nonebot-plugin-test)                       - 在浏览器中测试你的 NoneBot 机器人
定时任务 (nonebot-plugin-apscheduler)                     - APScheduler 定时任务插件
图片搜索 (nonebot-plugin-picsearcher)                     - 从基本上所有你想的出名字的搜图平台找图片
通用数据库连接 (nonebot-plugin-navicat)                      - 连接至各种数据库,为其他插件导出连接对象
translator (nonebot-plugin-translator)                - 多语种翻译插件
模拟抽卡 (nonebot-plugin-simdraw)                         - 根据配置的手游卡池信息模拟抽卡
nonebot-plugin-wordbank (nonebot-plugin-wordbank)     - 无数据库的轻量问答插件,支持模糊问答
冷却事件 (nonebot-plugin-cooldown)                        - 为用户调用功能添加冷却时间(调用频率限制)功能
mqtt接入 (nonebot-plugin-mqtt)                          - 接入mqtt网络,订阅和发布消息
消息交互式 python 解释器 (nonebot-plugin-ipypreter)           - 消息交互式 python 解释器
songpicker2 (nonebot-plugin-songpicker2)              - 点播歌曲,支持候选菜单、热评显示,数据源为网易云
风格化字符串管理 (nonebot-plugin-styledstr)                   - 通过字符串标签管理字符串资源
Arcaea 查分器 (nonebot-plugin-arcaea)                    - Arcaea 查分器,可以实现 best30 | recent | songinfo 之类的查询功能并支持 DIY
hk-reporter (nonebot-hk-reporter)                     - 订阅如微博,bilibili,rss的更新消息
网易云无损音乐下载 (nonebot-plugin-ncm)                        - 网易云无损音乐下载
nonebot-plugin-cocdicer (nonebot-plugin-cocdicer)     - COC跑团骰子娘
跑团记录记录器 (nonebot-plugin-trpglogger)                   - 记录跑团记录并上传
nonebot-plugin-r6s (nonebot-plugin-r6s)               - 查询彩虹六号玩家信息
猜猜看 (nonebot-plugin-guess)                            - 多次互动猜名字游戏,自带猜城市名,可定制
缩写查询器 (nonebot_plugin_abbrreply)                      - 输入拼音首字母,猜测文字
biliav小程序转换器 (nonebot_plugin_biliav)                  - 将用户发的av号或者bv号转成小程序返回
插件管理器 (nonebot-plugin-manager)                        - 基于 import hook 的插件管理

身为一个严重的强迫症患者(仅限计算机相关的事物),我对于这种错位是深恶痛绝的,于是我便想自行修复这个问题。

又众所周知,gbk 编码的字符串中汉字占据的长度是英文字母的两倍,所以我简单粗暴地通过计算字符串被 gbk 编码后的长度得出了正确的长度,然后根据正确的长度算出了正确的填充空格数,然后就成功了。

代码片段:

python
summary_lines=[]
while len(summary.encode('gbk')) > target_width:
    summary_lines.append(summary[:target_width])
    summary = summary[len(summary_lines) * target_width + 1:]
if not summary_lines:summary_lines=[summary]

修复后:

shell
服务器状态查看 (nonebot-plugin-status)                  - 通过戳一戳获取服务器状态
HarukaBot (haruka-bot)                                  - 将B站UP主的动态和直播信息推送至QQ
rauthman (nonebot-plugin-rauthman)                      - 基于规则的授权管理
NoneBot离线文档 (nonebot-plugin-docs)                   - 在本地浏览NoneBot文档
Sentry日志监控 (nonebot-plugin-sentry)                  - 使用Sentry监控机器人日志并处理报错
前端测试机器人插件 (nonebot-plugin-test)                - 在浏览器中测试你的 NoneBot 机器人
定时任务 (nonebot-plugin-apscheduler)                   - APScheduler 定时任务插件
图片搜索 (nonebot-plugin-picsearcher)                   - 从基本上所有你想的出名字的搜图平台找图片
通用数据库连接 (nonebot-plugin-navicat)                 - 连接至各种数据库,为其他插件导出连接对象
translator (nonebot-plugin-translator)                  - 多语种翻译插件
模拟抽卡 (nonebot-plugin-simdraw)                       - 根据配置的手游卡池信息模拟抽卡
nonebot-plugin-wordbank (nonebot-plugin-wordbank)       - 无数据库的轻量问答插件,支持模糊问答
冷却事件 (nonebot-plugin-cooldown)                      - 为用户调用功能添加冷却时间(调用频率限制)功能
mqtt接入 (nonebot-plugin-mqtt)                          - 接入mqtt网络,订阅和发布消息
消息交互式 python 解释器 (nonebot-plugin-ipypreter)     - 消息交互式 python 解释器
songpicker2 (nonebot-plugin-songpicker2)                - 点播歌曲,支持候选菜单、热评显示,数据源为网易云
风格化字符串管理 (nonebot-plugin-styledstr)             - 通过字符串标签管理字符串资源
Arcaea 查分器 (nonebot-plugin-arcaea)                   - Arcaea 查分器,可以实现 best30 | recent | songinfo 之类的查询功能并支持 DIY
hk-reporter (nonebot-hk-reporter)                       - 订阅如微博,bilibili,rss的更新消息
网易云无损音乐下载 (nonebot-plugin-ncm)                 - 网易云无损音乐下载
nonebot-plugin-cocdicer (nonebot-plugin-cocdicer)       - COC跑团骰子娘
跑团记录记录器 (nonebot-plugin-trpglogger)              - 记录跑团记录并上传
nonebot-plugin-r6s (nonebot-plugin-r6s)                 - 查询彩虹六号玩家信息
猜猜看 (nonebot-plugin-guess)                           - 多次互动猜名字游戏,自带猜城市名,可定制
缩写查询器 (nonebot_plugin_abbrreply)                   - 输入拼音首字母,猜测文字
biliav小程序转换器 (nonebot_plugin_biliav)              - 将用户发的av号或者bv号转成小程序返回
插件管理器 (nonebot-plugin-manager)                     - 基于 import hook 的插件管理

当然,我估计你们看到的依旧是不对齐的,这是因为 Chromium 内核在渲染文本时如果把汉字和英文字母强制按 1:2 的宽度比来渲染,要么中文变得非常宽,要么就是英文字母变得非常窄,所以一般来说供人阅读的文字都不会强制渲染为 1:2 的宽度。一般只有命令行为了便于程序员检查缩进,才会默认使用等宽字体来渲染。

image-20210313223322545命令行中正常对齐

事实上,这种对齐问题不仅是汉字上独有的,而是 CJK(中日韩统一表意文字)普遍具有的问题。我也不知道其他程序员是怎么解决这种问题,反正我是用这种蛋疼的方法解决了。

事后我知道了可以用 chr(12288) ,即用全角空格代替半角空格来填充,我是个傻逼。

NoneBot 开发日志#1 浅谈插件
Java、Python 作业整合